hecks 0.2.0 → 0.3.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative "../../../../runtime/registry"
|
|
2
|
+
require_relative "../../../../runtime/storage_shape"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class PostgresEra
|
|
7
|
+
class Lineage
|
|
8
|
+
module MintTransaction
|
|
9
|
+
# ── the mint transaction ───────────────────────────────────────
|
|
10
|
+
#
|
|
11
|
+
# One transaction: the new era row (held text + minted name +
|
|
12
|
+
# cut watermark), the new partition, and every aggregate's
|
|
13
|
+
# recompiled matview + head view. The advisory lock is the
|
|
14
|
+
# writer fence — two concurrent minters serialize, and the
|
|
15
|
+
# second finds the era already held. Populating the matview
|
|
16
|
+
# inside the transaction means a convert meeting an unmapped
|
|
17
|
+
# value REFUSES the whole mint, loudly, before anything boots.
|
|
18
|
+
def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
|
|
19
|
+
@db.exec("BEGIN")
|
|
20
|
+
# A concurrent minter blocks briefly, then refuses with a name
|
|
21
|
+
# rather than hanging: mint is non-interactive (the human
|
|
22
|
+
# decision already happened in the audit tool), so the lock is
|
|
23
|
+
# only ever held for the transaction itself.
|
|
24
|
+
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
25
|
+
@db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
|
|
26
|
+
if eras.any? { |era| era[:ordinal] == ordinal }
|
|
27
|
+
@db.exec("ROLLBACK")
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
watermark = last_ordinal
|
|
32
|
+
@db.exec_params(
|
|
33
|
+
"INSERT INTO hecks_eras (domain, ordinal, hash, label, held_text, watermark, held_digest, held_projection, canon_form) " \
|
|
34
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
|
35
|
+
[@domain, ordinal, hash, label, held_text, watermark, Digest::SHA256.hexdigest(held_text),
|
|
36
|
+
projection && JSON.generate(projection), Runtime::StorageShape::FORM_VERSION]
|
|
37
|
+
)
|
|
38
|
+
archive_text!(ordinal, held_text)
|
|
39
|
+
ensure_partition!(ordinal)
|
|
40
|
+
# AFTER compile_head!, not before — this era's own snapshot
|
|
41
|
+
# table (a possibly-renamed name, per aggregate) is what
|
|
42
|
+
# grant_role! grants on, and compile_head! is what creates it.
|
|
43
|
+
# Granting first would GRANT on a relation that does not
|
|
44
|
+
# exist yet for any aggregate renamed in this very edge.
|
|
45
|
+
aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
|
|
46
|
+
grant_role!(role, aggregates: aggregates, era: ordinal) if role
|
|
47
|
+
# LAST, right before COMMIT — not merely unconditional. Once
|
|
48
|
+
# acquired, a lock is held until the TRANSACTION ends, not
|
|
49
|
+
# just for the statement that took it — so advance_era!'s
|
|
50
|
+
# DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
|
|
51
|
+
# as ALTER TABLE, and unavoidably so: Postgres has no lighter
|
|
52
|
+
# form for changing a policy, unlike the partition attach
|
|
53
|
+
# below) blocks every concurrent writer for as long as it sits
|
|
54
|
+
# BEFORE the expensive step. Ordered here, that block is the
|
|
55
|
+
# width of a few catalog statements plus the commit itself,
|
|
56
|
+
# not the width of compile_head!'s matview build. Measured:
|
|
57
|
+
# moving this above compile_head! (an earlier ordering, caught
|
|
58
|
+
# only once a genuine concurrent-write test was built rather
|
|
59
|
+
# than assumed) reintroduced exactly the mint-stops-the-world
|
|
60
|
+
# cost ensure_partition!'s build-then-ATTACH exists to avoid.
|
|
61
|
+
#
|
|
62
|
+
# UNCONDITIONAL regardless of position — this is the line that
|
|
63
|
+
# drops writing to the old schema. It does not wait for a role
|
|
64
|
+
# to be configured on THIS boot, because the cutoff is a fact
|
|
65
|
+
# about the era, not about who happened to mint it: an old
|
|
66
|
+
# checkout's role, granted by some earlier boot this one knows
|
|
67
|
+
# nothing about, must lose write access the instant this
|
|
68
|
+
# transaction commits.
|
|
69
|
+
advance_era!(ordinal)
|
|
70
|
+
@db.exec("COMMIT")
|
|
71
|
+
true
|
|
72
|
+
rescue PG::LockNotAvailable
|
|
73
|
+
@db.exec("ROLLBACK") rescue nil
|
|
74
|
+
raise Runtime::WiringError,
|
|
75
|
+
"cannot mint era #{ordinal} of #{@domain}: another mint holds the domain lock — " \
|
|
76
|
+
"waited 10s; try again shortly"
|
|
77
|
+
rescue PG::Error => error
|
|
78
|
+
@db.exec("ROLLBACK") rescue nil
|
|
79
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{error.message.strip}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Base privileges for a deployment's app role — a NON-owner,
|
|
83
|
+
# which may append and read once the shared era fence below
|
|
84
|
+
# admits it, and owns nothing. Idempotent, and unconcerned with
|
|
85
|
+
# WHICH era is current: that is advance_era!'s job, not this
|
|
86
|
+
# one's, so a role can be onboarded at any time without
|
|
87
|
+
# disturbing who may write what right now.
|
|
88
|
+
#
|
|
89
|
+
# Owner-only work, so a non-owner app boot skips it: the
|
|
90
|
+
# provisioner has already granted that role, and re-affirming is
|
|
91
|
+
# idempotent anyway.
|
|
92
|
+
#
|
|
93
|
+
# `aggregates:`/`era:` cover the read-cache side of the same
|
|
94
|
+
# story: unlike the journal (immutable, owner-provisioned once),
|
|
95
|
+
# each aggregate's head_snapshot table is a table an app role
|
|
96
|
+
# must itself INSERT/UPDATE/DELETE into — PostgresEra#append writes
|
|
97
|
+
# it directly, not through a view — so it needs real DML grants,
|
|
98
|
+
# not just the SELECT a derived read surface would need. `era:`
|
|
99
|
+
# is the ordinal THIS role is about to write under (the one
|
|
100
|
+
# hold_first!/mint_era! just made current, or the superseded one
|
|
101
|
+
# a stale checkout still speaks) — head_snapshot is era-scoped,
|
|
102
|
+
# so granting on the wrong era's table would grant on a table
|
|
103
|
+
# this role will never touch. Every caller passes its own
|
|
104
|
+
# bluebook's aggregates and resolved era — including a
|
|
105
|
+
# held-but-superseded checkout, since a role connecting to it
|
|
106
|
+
# for the first time (a new instance of an old checkout) has no
|
|
107
|
+
# privileges yet either.
|
|
108
|
+
def grant_role!(role, aggregates: [], era: nil)
|
|
109
|
+
return unless provisioner?
|
|
110
|
+
|
|
111
|
+
quoted_role = quote(role)
|
|
112
|
+
@db.exec("GRANT INSERT, SELECT ON #{quoted_journal} TO #{quoted_role}")
|
|
113
|
+
@db.exec("GRANT USAGE ON SEQUENCE #{quote(sequence)} TO #{quoted_role}")
|
|
114
|
+
return unless era
|
|
115
|
+
|
|
116
|
+
aggregates.each do |aggregate|
|
|
117
|
+
storage_name = aggregate.storage_name
|
|
118
|
+
@db.exec("GRANT SELECT, INSERT, UPDATE, DELETE ON #{quote(head_snapshot(storage_name, era))} TO #{quoted_role}")
|
|
119
|
+
@db.exec("GRANT SELECT ON #{quote(head_view(storage_name))} TO #{quoted_role}") if view_exists?(head_view(storage_name))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# THE current-era fence — ONE policy, shared by every granted
|
|
124
|
+
# role, not one per role. Advancing it is what drops writing to
|
|
125
|
+
# the old schema the instant the new one materializes: the
|
|
126
|
+
# moment this commits, no role — old or new, whether or not it
|
|
127
|
+
# did anything to earn this mint — may insert anything but the
|
|
128
|
+
# era named here. There is no persisted fork: a checkout that
|
|
129
|
+
# keeps a role fenced to a stale era does not exist in this
|
|
130
|
+
# design, because there is no such thing as a role fenced to an
|
|
131
|
+
# era at all — only the ONE era everyone currently shares.
|
|
132
|
+
#
|
|
133
|
+
# A ROW POLICY, not a partition grant. Postgres checks INSERT
|
|
134
|
+
# privilege on the partitioned PARENT for a routed insert and
|
|
135
|
+
# never consults the partition's own grants, so a per-partition
|
|
136
|
+
# GRANT/REVOKE is inert in both directions: grant only on the
|
|
137
|
+
# partition and nobody can write at all; grant on the parent and
|
|
138
|
+
# they may write into EVERY era, ancestors included. Measured,
|
|
139
|
+
# not reasoned about — see the spec, which writes through the
|
|
140
|
+
# fence rather than asserting the catalog.
|
|
141
|
+
#
|
|
142
|
+
# The table owner bypasses RLS by default, and that is load
|
|
143
|
+
# bearing: mint and merge run as the owner and must be able to
|
|
144
|
+
# write any era (the merge re-enters a winner's state into the
|
|
145
|
+
# CURRENT era, and compile_head! reads every ancestor).
|
|
146
|
+
#
|
|
147
|
+
# CALL ONLY WITH THE NEW CURRENT ORDINAL — from hold_first! (era
|
|
148
|
+
# 1) or mint_era! (era N). Calling this with a SUPERSEDED
|
|
149
|
+
# ordinal — from a boot that merely recognizes an old checkout —
|
|
150
|
+
# would roll the fence backward and silently reopen the old
|
|
151
|
+
# schema for everyone. That path grants a role's PRIVILEGES
|
|
152
|
+
# (grant_role!) and stops there on purpose.
|
|
153
|
+
def advance_era!(ordinal)
|
|
154
|
+
@db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
|
|
155
|
+
@db.exec(
|
|
156
|
+
"CREATE POLICY hecks_current_era ON #{quoted_journal} FOR INSERT TO PUBLIC " \
|
|
157
|
+
"WITH CHECK (era = #{ordinal.to_i})"
|
|
158
|
+
)
|
|
159
|
+
@db.exec("DROP POLICY IF EXISTS hecks_read_all ON #{quoted_journal}")
|
|
160
|
+
@db.exec("CREATE POLICY hecks_read_all ON #{quoted_journal} FOR SELECT TO PUBLIC USING (true)")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
class PostgresEra
|
|
4
|
+
class Lineage
|
|
5
|
+
module Provisioning
|
|
6
|
+
# Provisioning is the OWNER's job, and a deployment's app role is
|
|
7
|
+
# deliberately not the owner — it may append and read, and it
|
|
8
|
+
# owns nothing. By the time such a role connects, the base is
|
|
9
|
+
# already built, so its boot verifies rather than builds.
|
|
10
|
+
#
|
|
11
|
+
# Without this guard the per-era fence below is unreachable: the
|
|
12
|
+
# ALTER TABLE and REVOKE here are owner-only, so the very role
|
|
13
|
+
# grant_era! exists to constrain could never finish booting
|
|
14
|
+
# ("must be owner of table hecks_eras"). A fence nothing can
|
|
15
|
+
# reach is not a fence.
|
|
16
|
+
def ensure_base!
|
|
17
|
+
rename_domain! if @formerly_known_as
|
|
18
|
+
return unless provisioner?
|
|
19
|
+
|
|
20
|
+
@db.exec(<<~SQL)
|
|
21
|
+
CREATE TABLE IF NOT EXISTS hecks_eras (
|
|
22
|
+
domain text NOT NULL,
|
|
23
|
+
ordinal int NOT NULL,
|
|
24
|
+
hash text,
|
|
25
|
+
label text,
|
|
26
|
+
held_text text NOT NULL,
|
|
27
|
+
watermark bigint,
|
|
28
|
+
PRIMARY KEY (domain, ordinal)
|
|
29
|
+
)
|
|
30
|
+
SQL
|
|
31
|
+
@db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text")
|
|
32
|
+
@db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb")
|
|
33
|
+
# which canonical-form version minted this name — see
|
|
34
|
+
# Runtime::StorageShape::FORM_VERSION; rows minted before the
|
|
35
|
+
# column carry NULL, read as an implicit 1
|
|
36
|
+
@db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int")
|
|
37
|
+
# every frozen text version, archived where an edit cannot
|
|
38
|
+
# reach it — the recovery the hard reattest refusal points at
|
|
39
|
+
@db.exec(<<~SQL)
|
|
40
|
+
CREATE TABLE IF NOT EXISTS hecks_era_texts (
|
|
41
|
+
domain text NOT NULL,
|
|
42
|
+
ordinal int NOT NULL,
|
|
43
|
+
digest text NOT NULL,
|
|
44
|
+
held_text text NOT NULL,
|
|
45
|
+
archived_at timestamptz NOT NULL DEFAULT now(),
|
|
46
|
+
PRIMARY KEY (domain, ordinal, digest)
|
|
47
|
+
)
|
|
48
|
+
SQL
|
|
49
|
+
@db.exec(<<~SQL)
|
|
50
|
+
CREATE TABLE IF NOT EXISTS hecks_approvals (
|
|
51
|
+
domain text NOT NULL,
|
|
52
|
+
from_label text NOT NULL,
|
|
53
|
+
to_label text NOT NULL,
|
|
54
|
+
edge_digest text NOT NULL,
|
|
55
|
+
reviewed_ordinal bigint NOT NULL,
|
|
56
|
+
approved_at timestamptz NOT NULL DEFAULT now()
|
|
57
|
+
)
|
|
58
|
+
SQL
|
|
59
|
+
@db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}")
|
|
60
|
+
# GENERATED ALWAYS AS IDENTITY is the intent, but identity
|
|
61
|
+
# columns on partitioned tables need Postgres 17 — an owned
|
|
62
|
+
# sequence default is the same spanning ordinal on any
|
|
63
|
+
# supported server.
|
|
64
|
+
@db.exec(<<~SQL)
|
|
65
|
+
CREATE TABLE IF NOT EXISTS #{quoted_journal} (
|
|
66
|
+
ordinal bigint NOT NULL DEFAULT nextval('#{sequence}'),
|
|
67
|
+
era int NOT NULL,
|
|
68
|
+
aggregate text NOT NULL,
|
|
69
|
+
aggregate_id text NOT NULL,
|
|
70
|
+
operation text NOT NULL DEFAULT 'save',
|
|
71
|
+
state jsonb,
|
|
72
|
+
mirrors jsonb
|
|
73
|
+
) PARTITION BY LIST (era)
|
|
74
|
+
SQL
|
|
75
|
+
ensure_partition!(1)
|
|
76
|
+
# Immutability by privilege: nothing updates or deletes journal
|
|
77
|
+
# rows. The owner's implicit rights remain (Postgres has no way
|
|
78
|
+
# to revoke them from the owner itself); a deployment's app
|
|
79
|
+
# role connects as a NON-owner and gets exactly INSERT, per
|
|
80
|
+
# era, at mint time.
|
|
81
|
+
@db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC")
|
|
82
|
+
# RLS goes on AT PROVISIONING, never mid-life — enabling it
|
|
83
|
+
# later would deny every role that has no policy yet, on
|
|
84
|
+
# whatever the shape of the schema happened to be at that
|
|
85
|
+
# moment.
|
|
86
|
+
#
|
|
87
|
+
# FORCE, not merely ENABLE: without it, the table OWNER is
|
|
88
|
+
# exempt from every policy here, by Postgres default — which
|
|
89
|
+
# would leave the schema writable forever to whoever holds
|
|
90
|
+
# the owner's credentials, the one connection this whole
|
|
91
|
+
# design cannot fence. Checked, not assumed: mint_era! never
|
|
92
|
+
# inserts into the journal at all (only hecks_eras/
|
|
93
|
+
# hecks_era_texts, neither RLS-protected), and merge_tail!'s
|
|
94
|
+
# one journal INSERT targets the CURRENT era, which the fence
|
|
95
|
+
# already admits for anyone with base privileges — so FORCE
|
|
96
|
+
# costs the owner nothing operations here actually need.
|
|
97
|
+
#
|
|
98
|
+
# This still exempts an actual Postgres SUPERUSER (or any
|
|
99
|
+
# role granted BYPASSRLS) unconditionally — FORCE only
|
|
100
|
+
# narrows what ENABLE already narrows for the owner
|
|
101
|
+
# specifically, and superuser bypass sits above both. Running
|
|
102
|
+
# migrations as a real superuser (self-hosted Postgres, most
|
|
103
|
+
# commonly) leaves this gap open regardless; a managed
|
|
104
|
+
# provider's admin account is typically NOT a superuser, and
|
|
105
|
+
# is exactly what FORCE closes.
|
|
106
|
+
#
|
|
107
|
+
# GUARDED, not reissued unconditionally — measured, not
|
|
108
|
+
# assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`
|
|
109
|
+
# takes AccessExclusiveLock EVEN WHEN THE SETTING IS ALREADY
|
|
110
|
+
# CORRECT (Postgres does not skip the lock just because the
|
|
111
|
+
# statement would be a no-op). ensure_base! runs on EVERY
|
|
112
|
+
# boot by the owning role, not only the first — so an
|
|
113
|
+
# unconditional reissue here would mean every ordinary
|
|
114
|
+
# reboot of the deployment's own identity re-freezes every
|
|
115
|
+
# concurrent writer, on any era, for as long as that ALTER
|
|
116
|
+
# TABLE has to wait its turn. Read the current state first;
|
|
117
|
+
# touch the catalog only on the boot that actually needs to.
|
|
118
|
+
# pg_table_is_visible, NOT a bare relname match — a shared
|
|
119
|
+
# instance (storehouse) can hold a same-named journal table
|
|
120
|
+
# per schema; catalog lookups here must resolve the SAME way
|
|
121
|
+
# search_path resolves an unqualified SQL reference, or a
|
|
122
|
+
# sibling domain's table satisfies a query meant for this
|
|
123
|
+
# domain's own.
|
|
124
|
+
current = @db.exec_params(
|
|
125
|
+
"SELECT relrowsecurity, relforcerowsecurity FROM pg_class " \
|
|
126
|
+
"WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
|
|
127
|
+
)[0]
|
|
128
|
+
@db.exec("ALTER TABLE #{quoted_journal} ENABLE ROW LEVEL SECURITY") unless current["relrowsecurity"] == "t"
|
|
129
|
+
@db.exec("ALTER TABLE #{quoted_journal} FORCE ROW LEVEL SECURITY") unless current["relforcerowsecurity"] == "t"
|
|
130
|
+
install_transforms!
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A DOMAIN'S OWN IDENTITY CHANGED — bridge its history under the
|
|
134
|
+
# new name, before `provisioner?`/the CREATE TABLE IF NOT EXISTS
|
|
135
|
+
# block below ever run. That ordering is load-bearing, not
|
|
136
|
+
# tidiness: `provisioner?` and every statement in ensure_base!
|
|
137
|
+
# test for the journal under `journal` — the NEW name — and a
|
|
138
|
+
# freshly-renamed domain looks, to those checks, exactly like a
|
|
139
|
+
# domain that has never been provisioned at all. Left where it
|
|
140
|
+
# was written, ensure_base! would happily CREATE TABLE IF NOT
|
|
141
|
+
# EXISTS a brand-new, empty journal under the new name before
|
|
142
|
+
# this method ever got a chance to run — and the ALTER TABLE ...
|
|
143
|
+
# RENAME below would then fail, renaming onto a name that
|
|
144
|
+
# already exists.
|
|
145
|
+
#
|
|
146
|
+
# Three-way precheck, cheapest first:
|
|
147
|
+
# 1. no hecks_eras table at all yet — a genuinely fresh
|
|
148
|
+
# database; nothing to bridge, fall through to the
|
|
149
|
+
# ordinary provisioning path.
|
|
150
|
+
# 2. hecks_eras already has rows under the NEW name — this
|
|
151
|
+
# rename already ran (a prior boot, possibly this one on a
|
|
152
|
+
# retry); idempotent no-op.
|
|
153
|
+
# 3. hecks_eras has rows under the OLD name — run the
|
|
154
|
+
# migration.
|
|
155
|
+
# Anything else (no rows under either name) falls through
|
|
156
|
+
# harmlessly — `formerly_known_as` pointing at a name with no
|
|
157
|
+
# held history is not an error, just inert.
|
|
158
|
+
def rename_domain!
|
|
159
|
+
return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"]
|
|
160
|
+
return if @db.exec_params(
|
|
161
|
+
"SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@domain]
|
|
162
|
+
).ntuples.positive?
|
|
163
|
+
return if @db.exec_params(
|
|
164
|
+
"SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@formerly_known_as]
|
|
165
|
+
).ntuples.zero?
|
|
166
|
+
|
|
167
|
+
old_journal = "hecks_journal_#{Naming.snake(@formerly_known_as)}"
|
|
168
|
+
old_sequence = "#{old_journal}_ordinal"
|
|
169
|
+
ordinals = @db.exec_params(
|
|
170
|
+
"SELECT ordinal FROM hecks_eras WHERE domain = $1 ORDER BY ordinal", [@formerly_known_as]
|
|
171
|
+
).map { |row| row["ordinal"].to_i }
|
|
172
|
+
|
|
173
|
+
@db.exec("BEGIN")
|
|
174
|
+
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
175
|
+
# Fixed order — old before new, the `eras:` family before the
|
|
176
|
+
# `ordinal:` family — so two rename attempts (or a rename
|
|
177
|
+
# racing a mint/plain-write on either name) can only ever
|
|
178
|
+
# queue behind one another, never deadlock.
|
|
179
|
+
[
|
|
180
|
+
"hecks_eras:#{@formerly_known_as}", "hecks_eras:#{@domain}",
|
|
181
|
+
"hecks_ordinal:#{@formerly_known_as}", "hecks_ordinal:#{@domain}"
|
|
182
|
+
].each do |key|
|
|
183
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext($1))", [key])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
@db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}")
|
|
187
|
+
# An owned SERIAL/IDENTITY sequence moves automatically with
|
|
188
|
+
# its table and errors if renamed explicitly — this one is a
|
|
189
|
+
# plain CREATE SEQUENCE the journal's DEFAULT merely points
|
|
190
|
+
# at (see ensure_base!), so it does need its own rename, and
|
|
191
|
+
# the column default survives untouched: Postgres stores
|
|
192
|
+
# nextval('...') as a regclass reference internally, not
|
|
193
|
+
# literal text.
|
|
194
|
+
@db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}")
|
|
195
|
+
# ALTER TABLE ... RENAME on the parent does NOT cascade to
|
|
196
|
+
# child partition names — each one needs its own explicit
|
|
197
|
+
# rename, sourced from the ordinals held under the OLD name,
|
|
198
|
+
# captured above before the UPDATE below flips the column.
|
|
199
|
+
ordinals.each do |ordinal|
|
|
200
|
+
old_partition = "#{old_journal}_era_#{ordinal}"
|
|
201
|
+
@db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
%w[hecks_eras hecks_era_texts hecks_approvals].each do |table|
|
|
205
|
+
@db.exec_params("UPDATE #{table} SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as])
|
|
206
|
+
end
|
|
207
|
+
# Unlike its siblings, hecks_attestations is not created in
|
|
208
|
+
# ensure_base! at all — only lazily, on first reattest! — so a
|
|
209
|
+
# domain that never needed one must not be forced through an
|
|
210
|
+
# UPDATE against a table that doesn't exist.
|
|
211
|
+
@db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as]) if @db.exec_params("SELECT to_regclass($1)", ["hecks_attestations"])[0]["to_regclass"]
|
|
212
|
+
|
|
213
|
+
@db.exec("COMMIT")
|
|
214
|
+
rescue PG::LockNotAvailable
|
|
215
|
+
@db.exec("ROLLBACK") rescue nil
|
|
216
|
+
raise Runtime::WiringError,
|
|
217
|
+
"cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \
|
|
218
|
+
"one of the domain locks — waited 10s; try again shortly"
|
|
219
|
+
rescue PG::Error => error
|
|
220
|
+
@db.exec("ROLLBACK") rescue nil
|
|
221
|
+
raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{error.message.strip}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Nothing provisioned yet — build it. Provisioned and owned —
|
|
225
|
+
# keep it current. Provisioned by SOMEONE ELSE — this is an app
|
|
226
|
+
# role, and the owner has already done this work.
|
|
227
|
+
def provisioner?
|
|
228
|
+
rows = @db.exec_params(
|
|
229
|
+
"SELECT pg_get_userbyid(relowner) = current_user AS owned FROM pg_class " \
|
|
230
|
+
"WHERE relname = $1 AND pg_table_is_visible(oid)",
|
|
231
|
+
[journal]
|
|
232
|
+
)
|
|
233
|
+
rows.ntuples.zero? || rows[0]["owned"] == "t"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# BUILD, THEN ATTACH — never CREATE ... PARTITION OF. The two
|
|
237
|
+
# produce the same partition; only the lock differs, and that
|
|
238
|
+
# difference is the whole availability story of a mint:
|
|
239
|
+
#
|
|
240
|
+
# CREATE TABLE ... PARTITION OF → AccessExclusiveLock (parent)
|
|
241
|
+
# CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
|
|
242
|
+
#
|
|
243
|
+
# AccessExclusive conflicts with every insert in the hierarchy —
|
|
244
|
+
# routed through the parent OR addressed to an existing leaf —
|
|
245
|
+
# so attaching the new era inside the mint transaction stopped
|
|
246
|
+
# every writer for the WHOLE mint, tail materialization
|
|
247
|
+
# included. ShareUpdateExclusive conflicts with neither, so the
|
|
248
|
+
# old checkout keeps writing its own era straight through the
|
|
249
|
+
# build and only pauses for the head swap at the end.
|
|
250
|
+
#
|
|
251
|
+
# That is what makes the fork real DURING a mint rather than
|
|
252
|
+
# merely before and after one. Measured, and pinned by the spec
|
|
253
|
+
# — which writes through a live mint rather than reading a lock
|
|
254
|
+
# mode out of the catalog.
|
|
255
|
+
def ensure_partition!(era)
|
|
256
|
+
return if partition_attached?(era)
|
|
257
|
+
|
|
258
|
+
@db.exec(<<~SQL)
|
|
259
|
+
CREATE TABLE IF NOT EXISTS #{quote(partition(era))} (
|
|
260
|
+
LIKE #{quoted_journal} INCLUDING DEFAULTS
|
|
261
|
+
)
|
|
262
|
+
SQL
|
|
263
|
+
@db.exec(<<~SQL)
|
|
264
|
+
ALTER TABLE #{quoted_journal}
|
|
265
|
+
ATTACH PARTITION #{quote(partition(era))} FOR VALUES IN (#{era.to_i})
|
|
266
|
+
SQL
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Attached, not merely present: a crash between the CREATE and
|
|
270
|
+
# the ATTACH leaves a table that is not yet part of the journal,
|
|
271
|
+
# and the next boot must finish the job rather than skip it.
|
|
272
|
+
def partition_attached?(era)
|
|
273
|
+
@db.exec_params(
|
|
274
|
+
"SELECT 1 FROM pg_inherits i " \
|
|
275
|
+
"JOIN pg_class child ON child.oid = i.inhrelid " \
|
|
276
|
+
"JOIN pg_class parent ON parent.oid = i.inhparent " \
|
|
277
|
+
"WHERE child.relname = $1 AND parent.relname = $2 " \
|
|
278
|
+
"AND pg_table_is_visible(child.oid) AND pg_table_is_visible(parent.oid)",
|
|
279
|
+
[partition(era), journal]
|
|
280
|
+
).ntuples.positive?
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
class PostgresEra
|
|
4
|
+
class Lineage
|
|
5
|
+
# THE ONE CHUNKED, LOCK-FREE, RESUMABLE BACKFILL LOOP — shared by
|
|
6
|
+
# `backfill_head_snapshot!` (era 1's existing one-shot blocking
|
|
7
|
+
# backfill, retrofit) and every field-cache table's own initial
|
|
8
|
+
# backfill (new). Governing principle 1 (docs/implemented/postgres-era-adapter-
|
|
9
|
+
# split-plan.md): no operation this plan touches may hold a lock
|
|
10
|
+
# across a scan whose duration scales with table size — a single
|
|
11
|
+
# `INSERT ... SELECT` over the whole journal (what
|
|
12
|
+
# `backfill_head_snapshot!` used to be) is exactly that, and so is
|
|
13
|
+
# a naive "populate every cache row in one statement" field-cache
|
|
14
|
+
# backfill.
|
|
15
|
+
#
|
|
16
|
+
# THE SHAPE: read one bounded chunk (real rows, real ordinals) with
|
|
17
|
+
# a plain SELECT — no lock held across it, so an ordinary reader or
|
|
18
|
+
# writer is never blocked by a backfill in progress — then upsert
|
|
19
|
+
# that chunk under the SAME transactionally-scoped advisory lock +
|
|
20
|
+
# ordinal-guard idiom `append`'s own snapshot upsert already uses
|
|
21
|
+
# (`WHERE ordinal < EXCLUDED.ordinal`), then persist a cursor
|
|
22
|
+
# before moving to the next chunk. Repeat until a chunk reads back
|
|
23
|
+
# short of a full page — that page was the last one.
|
|
24
|
+
#
|
|
25
|
+
# RESUMABLE, not merely restartable. A crash (or a second
|
|
26
|
+
# concurrent boot) mid-backfill leaves the cursor exactly where the
|
|
27
|
+
# last COMMITTED chunk left it — `hecks_backfill_progress` is
|
|
28
|
+
# updated in the SAME transaction as the chunk's own upsert, so
|
|
29
|
+
# cursor and data can never observably disagree (see
|
|
30
|
+
# `run_chunk!`). The next attempt reads that cursor and continues;
|
|
31
|
+
# it does not rescan what a prior attempt already committed.
|
|
32
|
+
# RESTARTABLE would also be CORRECT here (every upsert is
|
|
33
|
+
# idempotent and ordinal-guarded — rerunning an already-done chunk
|
|
34
|
+
# from id 1 changes nothing) but wastes real work on a large
|
|
35
|
+
# table; resumability is what keeps a crash near the END of a
|
|
36
|
+
# large backfill cheap to recover from instead of starting over.
|
|
37
|
+
#
|
|
38
|
+
# THE LOCK KEY PREFIX is `hecks_field_cache:` — deliberately
|
|
39
|
+
# disjoint from the three families already in use elsewhere in
|
|
40
|
+
# this adapter (`hecks_ordinal:`, `hecks_eras:`,
|
|
41
|
+
# `hecks_head_snapshot:` — see lineage.rb/head_compiler.rb/
|
|
42
|
+
# mint_transaction.rb/tail_merge.rb) so a backfill chunk NEVER
|
|
43
|
+
# contends with a plain write, a mint, or a snapshot-table's own
|
|
44
|
+
# first-creation lock. It is held for exactly ONE CHUNK's own
|
|
45
|
+
# transaction, never across the whole backfill — two concurrent
|
|
46
|
+
# backfillers of the SAME target simply take turns one chunk at a
|
|
47
|
+
# time rather than racing to duplicate work; neither blocks an
|
|
48
|
+
# unrelated reader or writer for even an instant.
|
|
49
|
+
module ResumableBackfill
|
|
50
|
+
CHUNK_SIZE = 5_000
|
|
51
|
+
|
|
52
|
+
# Idempotent, unguarded — same idiom as every other DDL helper
|
|
53
|
+
# in this file tree (`ensure_head_snapshot!` et al.): cheap,
|
|
54
|
+
# runs on every boot, only ever does real work once.
|
|
55
|
+
def ensure_backfill_progress_table!
|
|
56
|
+
@db.exec(<<~SQL)
|
|
57
|
+
CREATE TABLE IF NOT EXISTS hecks_backfill_progress (
|
|
58
|
+
target text PRIMARY KEY,
|
|
59
|
+
cursor text,
|
|
60
|
+
completed boolean NOT NULL DEFAULT false,
|
|
61
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
62
|
+
)
|
|
63
|
+
SQL
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Drives `target` (an already-created, currently-empty-or-
|
|
67
|
+
# partially-filled table) through chunks until a source read
|
|
68
|
+
# comes back short of `CHUNK_SIZE` rows. Two distinct callables,
|
|
69
|
+
# not one — a head-snapshot row and a field-cache row carry
|
|
70
|
+
# different columns (`state` jsonb vs. a single extracted
|
|
71
|
+
# `value`), so there is no one generic "upsert this row" shape
|
|
72
|
+
# to share; only the LOOP, the lock, and the cursor are generic.
|
|
73
|
+
#
|
|
74
|
+
# source_sql.call(cursor) — given the last-processed id (nil
|
|
75
|
+
# before the first chunk), returns a SQL SELECT whose result
|
|
76
|
+
# has an `id` column (text, ordered ascending) plus whatever
|
|
77
|
+
# other columns `upsert` below needs. Must read `id >
|
|
78
|
+
# cursor` (or unconditional when cursor is nil), `ORDER BY
|
|
79
|
+
# id`, `LIMIT CHUNK_SIZE` — the caller owns the actual
|
|
80
|
+
# column list/source tables; this method only owns the loop,
|
|
81
|
+
# the lock, and the cursor.
|
|
82
|
+
#
|
|
83
|
+
# upsert.call(rows) — given the PG::Result of one chunk's
|
|
84
|
+
# read, performs the actual guarded upsert into `target` and
|
|
85
|
+
# returns nothing meaningful; runs INSIDE the same
|
|
86
|
+
# transaction/advisory-lock scope as the cursor update below,
|
|
87
|
+
# so a crash between "wrote the chunk" and "advanced the
|
|
88
|
+
# cursor" is impossible — they commit together or not at
|
|
89
|
+
# all.
|
|
90
|
+
def chunked_backfill!(target, source_sql:, upsert:)
|
|
91
|
+
ensure_backfill_progress_table!
|
|
92
|
+
loop do
|
|
93
|
+
done = run_chunk!(target, source_sql: source_sql, upsert: upsert)
|
|
94
|
+
break if done
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# ONE CHUNK, ONE TRANSACTION, ONE SHORT-HELD LOCK. Re-reads
|
|
101
|
+
# progress AFTER acquiring the lock (not just before) — a second
|
|
102
|
+
# concurrent booter may have already finished this exact chunk
|
|
103
|
+
# (or the whole backfill) while this process was waiting for the
|
|
104
|
+
# lock; without the re-read, it would redundantly reprocess a
|
|
105
|
+
# chunk another process just committed. Harmless either way
|
|
106
|
+
# (idempotent, ordinal-guarded) but the re-read is what keeps
|
|
107
|
+
# two concurrent boots from both doing the full scan instead of
|
|
108
|
+
# splitting it.
|
|
109
|
+
def run_chunk!(target, source_sql:, upsert:)
|
|
110
|
+
progress = backfill_progress(target)
|
|
111
|
+
return true if progress[:completed]
|
|
112
|
+
|
|
113
|
+
completed = false
|
|
114
|
+
nested_transaction("hecks_backfill_chunk") do
|
|
115
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [target])
|
|
116
|
+
progress = backfill_progress(target)
|
|
117
|
+
if progress[:completed]
|
|
118
|
+
completed = true
|
|
119
|
+
next
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
rows = @db.exec(source_sql.call(progress[:cursor]))
|
|
123
|
+
if rows.ntuples.zero?
|
|
124
|
+
upsert_backfill_progress!(target, cursor: progress[:cursor], completed: true)
|
|
125
|
+
completed = true
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
upsert.call(rows)
|
|
130
|
+
completed = rows.ntuples < CHUNK_SIZE
|
|
131
|
+
# `PG::Result#[]` supports neither an out-of-range index NOR
|
|
132
|
+
# a negative one (unlike a plain Ruby Array) — same trap as
|
|
133
|
+
# `backfill_progress` above, the explicit last-index form.
|
|
134
|
+
last_cursor = rows[rows.ntuples - 1]["id"]
|
|
135
|
+
upsert_backfill_progress!(target, cursor: last_cursor, completed: completed)
|
|
136
|
+
end
|
|
137
|
+
completed
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `PG::Result#[]` RAISES IndexError on an out-of-range index —
|
|
141
|
+
# unlike a plain Ruby Array, it does not return nil — so the
|
|
142
|
+
# ENTIRELY ORDINARY case of "no progress row exists yet" (every
|
|
143
|
+
# target's very first check) cannot be read via a bare `[0]`.
|
|
144
|
+
# `ntuples.zero?` first, always.
|
|
145
|
+
def backfill_progress(target)
|
|
146
|
+
result = @db.exec_params(
|
|
147
|
+
"SELECT cursor, completed FROM hecks_backfill_progress WHERE target = $1", [target]
|
|
148
|
+
)
|
|
149
|
+
return { cursor: nil, completed: false } if result.ntuples.zero?
|
|
150
|
+
|
|
151
|
+
row = result[0]
|
|
152
|
+
{ cursor: row["cursor"], completed: row["completed"] == "t" }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def upsert_backfill_progress!(target, cursor:, completed:)
|
|
156
|
+
@db.exec_params(
|
|
157
|
+
"INSERT INTO hecks_backfill_progress (target, cursor, completed, updated_at) " \
|
|
158
|
+
"VALUES ($1, $2, $3, now()) " \
|
|
159
|
+
"ON CONFLICT (target) DO UPDATE SET cursor = EXCLUDED.cursor, " \
|
|
160
|
+
"completed = EXCLUDED.completed, updated_at = EXCLUDED.updated_at",
|
|
161
|
+
[target, cursor, completed]
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|