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,671 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
require_relative "sql_query_builder"
|
|
4
|
+
require_relative "postgres_era/lineage"
|
|
5
|
+
require_relative "postgres_era/lineage_manager"
|
|
6
|
+
require_relative "../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../query_specification/common/order_by"
|
|
8
|
+
require_relative "../../runtime/errors"
|
|
9
|
+
require_relative "../../runtime/event"
|
|
10
|
+
require_relative "../../runtime/instance"
|
|
11
|
+
require_relative "../../runtime/registry"
|
|
12
|
+
|
|
13
|
+
module Hecks
|
|
14
|
+
module Adapters
|
|
15
|
+
# The enforcement-grade persistence adapter — and the only one that
|
|
16
|
+
# declares the LINEAGE capability: it may act on shape drift
|
|
17
|
+
# (translate, fork, merge) where every other adapter can only refuse
|
|
18
|
+
# toward it. Sibling to the plain `Postgres` adapter (postgres.rb),
|
|
19
|
+
# which is the same database with none of this machinery — pick
|
|
20
|
+
# PostgresEra only once a domain actually needs to survive a shape
|
|
21
|
+
# change live. See docs/implemented/postgres-era-adapter-split-plan.md for why
|
|
22
|
+
# the two are split and what each one carries.
|
|
23
|
+
#
|
|
24
|
+
# Storage model (see postgres_era/lineage.rb for the DDL):
|
|
25
|
+
# - One journal per DOMAIN, list-partitioned by era, one ordinal
|
|
26
|
+
# sequence spanning partitions. Appends go there; nothing updates
|
|
27
|
+
# or deletes a journal row (immutability by privilege — UPDATE and
|
|
28
|
+
# DELETE revoked; a deployment's app role connects as a non-owner).
|
|
29
|
+
# - Per aggregate, the HEAD is derived: era 1 reads a plain view
|
|
30
|
+
# (latest save per id); later eras read a view overlaying the
|
|
31
|
+
# materialized, translated ancestor tail with live current-era
|
|
32
|
+
# rows. `project` is therefore a no-op — old entries are never
|
|
33
|
+
# rewritten, and the head is never a table anything writes.
|
|
34
|
+
# - State is ONE jsonb column. jsonb normalizes key order (and drops
|
|
35
|
+
# duplicate keys), so anything comparing stored state — the corpus
|
|
36
|
+
# history gate above all — must compare CANONICALIZED state, never
|
|
37
|
+
# raw bytes; `bin/canonicalise` deep-sorts keys, which is exactly
|
|
38
|
+
# why the gate survives this normalization.
|
|
39
|
+
# - Query pushdown is the shared SqlQueryBuilder: every declared
|
|
40
|
+
# operator compiles fully into SQL, or the query refuses loudly.
|
|
41
|
+
class PostgresEra
|
|
42
|
+
include SqlQueryBuilder
|
|
43
|
+
|
|
44
|
+
attr_reader :aggregate
|
|
45
|
+
|
|
46
|
+
def persistence_capabilities = [:atomic_put]
|
|
47
|
+
|
|
48
|
+
# The capability idiom: only PostgresEra answers true, and only
|
|
49
|
+
# PostgresEra carries an era_check! for the boot gate to delegate to.
|
|
50
|
+
def self.lineage_capable? = true
|
|
51
|
+
|
|
52
|
+
# TENANT-CAPABLE — see Runtime::TenantCheck's own header for the
|
|
53
|
+
# full reasoning. `connect_for`'s own `schema:` setting (the
|
|
54
|
+
# Storehouse shared-instance mechanism, already built, already
|
|
55
|
+
# proven for eras) is what keeps two tenant boots' tables apart:
|
|
56
|
+
# each boot's own `SET search_path` means every unqualified
|
|
57
|
+
# reference this adapter and its lineage classes construct
|
|
58
|
+
# resolves into that boot's own schema, never another tenant's —
|
|
59
|
+
# proven for real, not assumed, by tenant_isolation_spec.rb, the
|
|
60
|
+
# same discipline lineage_capable? already holds itself to.
|
|
61
|
+
def self.tenant_capable? = true
|
|
62
|
+
|
|
63
|
+
def self.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil)
|
|
64
|
+
LineageManager.check!(
|
|
65
|
+
registry: registry, bluebook: bluebook, current_text: current_text,
|
|
66
|
+
settings: settings, directory: directory
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.connect_for(name, settings)
|
|
71
|
+
# LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize:
|
|
72
|
+
# a domain that never wires PostgresEra should never need the gem.
|
|
73
|
+
require "pg"
|
|
74
|
+
|
|
75
|
+
declared = settings[:database] || settings["database"]
|
|
76
|
+
if declared.to_s.empty?
|
|
77
|
+
raise Runtime::WiringError,
|
|
78
|
+
"#{name} binds PostgresEra, which needs a database connection, " \
|
|
79
|
+
"but its world declares no \"database\"."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
connection =
|
|
83
|
+
if declared.start_with?("postgres://", "postgresql://")
|
|
84
|
+
PG.connect(declared)
|
|
85
|
+
else
|
|
86
|
+
PG.connect(dbname: declared)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# SHARED-INSTANCE ISOLATION. A domain that declares `schema` is
|
|
90
|
+
# sharing its Postgres instance with other domains (the
|
|
91
|
+
# storehouse) — every unqualified table/view/function reference
|
|
92
|
+
# this adapter and its lineage classes ever construct resolves
|
|
93
|
+
# through search_path, so this one SET is what makes ALTER
|
|
94
|
+
# TABLE ... SET SCHEMA migrations transparent to the rest of the
|
|
95
|
+
# adapter. A domain with no `schema` setting keeps Postgres's
|
|
96
|
+
# own default search_path (public), same as before this existed.
|
|
97
|
+
schema = settings[:schema] || settings["schema"]
|
|
98
|
+
if schema.to_s != ""
|
|
99
|
+
# THE SCHEMA ITSELF, IDEMPOTENTLY — a domain naming a `schema:`
|
|
100
|
+
# nobody has created yet used to fail on its FIRST table-
|
|
101
|
+
# creation attempt with Postgres's own "no schema has been
|
|
102
|
+
# selected to create in", found live provisioning tenant_
|
|
103
|
+
# isolation_spec.rb's own multi-schema fixture by hand before
|
|
104
|
+
# this existed. `CREATE SCHEMA IF NOT EXISTS` is exactly the
|
|
105
|
+
# same self-healing idempotency this adapter's own table/era
|
|
106
|
+
# provisioning already holds itself to (see the comment right
|
|
107
|
+
# below on `client_min_messages`) — a schema that already
|
|
108
|
+
# exists is the ORDINARY case for every boot after the first,
|
|
109
|
+
# not news.
|
|
110
|
+
connection.exec("CREATE SCHEMA IF NOT EXISTS #{connection.quote_ident(schema)}")
|
|
111
|
+
connection.exec("SET search_path TO #{connection.quote_ident(schema)}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# QUIET ON PURPOSE. Provisioning re-runs its own idempotent
|
|
115
|
+
# `CREATE ... IF NOT EXISTS` checks on every boot — a schema that
|
|
116
|
+
# already exists is the ORDINARY case, not news, and Postgres
|
|
117
|
+
# surfaces every one as a NOTICE by default. `bin/set-password`
|
|
118
|
+
# boots a real registry just to mint an Identity, and nobody
|
|
119
|
+
# setting a password needs to see a page of "relation ...
|
|
120
|
+
# already exists, skipping" to do it. WARNING and above (real
|
|
121
|
+
# problems) still surface.
|
|
122
|
+
connection.exec("SET client_min_messages = warning")
|
|
123
|
+
connection
|
|
124
|
+
rescue PG::Error => error
|
|
125
|
+
raise Runtime::WiringError,
|
|
126
|
+
"cannot bind PostgresEra at #{declared} for #{name}: #{error.message.strip}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
130
|
+
@aggregate = aggregate
|
|
131
|
+
@db = self.class.connect_for(aggregate.name, settings)
|
|
132
|
+
# The domain names the journal (one journal per lineage). The
|
|
133
|
+
# factory injects it; a directly-instantiated adapter (specs,
|
|
134
|
+
# consoles) journals under the aggregate's own name.
|
|
135
|
+
@domain = (settings[:domain] || settings["domain"] || aggregate.storage_name).to_s
|
|
136
|
+
@lineage = Lineage.new(@db, @domain)
|
|
137
|
+
@lineage.ensure_base!
|
|
138
|
+
# The era gate resolves which era this boot IS (an old checkout
|
|
139
|
+
# boots a held-but-superseded era and keeps writing its own
|
|
140
|
+
# partition); a directly-instantiated adapter defaults to the
|
|
141
|
+
# newest.
|
|
142
|
+
@era = settings[:era] || settings["era"] || @lineage.current_era
|
|
143
|
+
# Unconditional and idempotent, regardless of era — belt-and-
|
|
144
|
+
# suspenders self-healing (compile_head! already ensures this for
|
|
145
|
+
# a freshly-minted era's own name; ensure_first_head! for era 1's)
|
|
146
|
+
# against any boot-ordering surprise, at the cost of one
|
|
147
|
+
# CREATE TABLE IF NOT EXISTS nobody pays for twice.
|
|
148
|
+
@lineage.ensure_head_snapshot!(table, @era)
|
|
149
|
+
@lineage.ensure_first_head!(table) if @era == 1
|
|
150
|
+
# THE READ-CACHE SIDE OF THE ERA WORKAROUND (Track C,
|
|
151
|
+
# docs/implemented/postgres-era-adapter-split-plan.md §3) — one row-cache
|
|
152
|
+
# table per `where`-field this aggregate's own declared queries
|
|
153
|
+
# (and its entities' own) actually use, derived automatically
|
|
154
|
+
# (principle 3 — no bluebook keyword), self-healing and
|
|
155
|
+
# idempotent like everything else booted here. `@field_caches`
|
|
156
|
+
# maps field -> cache-table name; `query` below consults it to
|
|
157
|
+
# decide whether a declared query can skip the DISTINCT ON
|
|
158
|
+
# reduction entirely.
|
|
159
|
+
@field_caches = ensure_field_caches!
|
|
160
|
+
create_event_table!
|
|
161
|
+
create_saga_table!
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def table = @aggregate.storage_name
|
|
165
|
+
|
|
166
|
+
def find(id)
|
|
167
|
+
result = @db.exec_params(%(SELECT id, state FROM #{quoted_head} WHERE id = $1), [id.to_s])
|
|
168
|
+
return nil if result.ntuples.zero?
|
|
169
|
+
|
|
170
|
+
instance(result[0])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# order_by IS A RUNTIME VALUE, not framework-authored bluebook source
|
|
174
|
+
# like every other caller of order_expression — a query param off an
|
|
175
|
+
# HTTP request, in the console's case. Whitelisted against the
|
|
176
|
+
# aggregate's own real attributes (plus its lifecycle field) before
|
|
177
|
+
# it ever reaches order_expression, unlike a declared query's
|
|
178
|
+
# order_by, which the language itself already only lets name a real
|
|
179
|
+
# attribute at parse time. Without this, an unknown field wouldn't
|
|
180
|
+
# error — query_expression degrades a nil attribute to a harmless
|
|
181
|
+
# no-op path — it would just silently sort by nothing.
|
|
182
|
+
def all(order_by: nil, direction: :asc)
|
|
183
|
+
return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
|
|
184
|
+
|
|
185
|
+
name = order_by.to_s.split(".").first
|
|
186
|
+
raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
187
|
+
|
|
188
|
+
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
189
|
+
@db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def count = @db.exec(%(SELECT COUNT(*) FROM #{quoted_head}))[0]["count"].to_i
|
|
193
|
+
|
|
194
|
+
# THE TWO-PHASE SHORTCUT (Track C, docs/implemented/postgres-era-adapter-
|
|
195
|
+
# split-plan.md §3). `SqlQueryBuilder#query` (`super`, unmodified per
|
|
196
|
+
# principle 2) always runs correctly here — it filters against
|
|
197
|
+
# `head_view`, which is already the fully-reduced current state —
|
|
198
|
+
# but for a domain that has minted a second era, that reduction
|
|
199
|
+
# itself is the expensive part, and no index on the jsonb path
|
|
200
|
+
# changes that (see field_cache.rb's own header for the SQL-
|
|
201
|
+
# semantics reason why). When every `where` clause this query
|
|
202
|
+
# declares either targets a cached field or is eligible to (an
|
|
203
|
+
# ordinary comparator, not a null-vs-value special case — see
|
|
204
|
+
# `cache_eligible?`), skip the reduction: look candidate ids up in
|
|
205
|
+
# the cache table(s) first (cheap, indexed, no reduction involved),
|
|
206
|
+
# then read ONLY those ids' current state from `head_view` — safe
|
|
207
|
+
# THROUGH the reduction because `id` is its own partition key. Any
|
|
208
|
+
# clause that ISN'T cache-eligible (an uncached field, or a null
|
|
209
|
+
# comparison) is simply re-checked against `head_view` in the
|
|
210
|
+
# second phase, exactly as `super` would have checked it anyway —
|
|
211
|
+
# this can only ever NARROW what phase two has to look at, never
|
|
212
|
+
# change what a clause means.
|
|
213
|
+
#
|
|
214
|
+
# FALLS BACK TO `super` WHENEVER NO CLAUSE CAN BE ACCELERATED — a
|
|
215
|
+
# query with no `where` at all (order_by-only — no cache table
|
|
216
|
+
# exists for these, see field_cache.rb), a query whose only clauses
|
|
217
|
+
# target fields with no cache table, or a domain that has never
|
|
218
|
+
# minted a second era at all (`@field_caches` is never empty just
|
|
219
|
+
# because era 1 has no reduction to skip — the cache tables still
|
|
220
|
+
# exist and still accelerate era 1 the same way, but the fallback
|
|
221
|
+
# path is already just as cheap there since head_view IS the
|
|
222
|
+
# snapshot table verbatim for era 1; skipping straight to `super`
|
|
223
|
+
# in that case would be a valid FUTURE optimization, not attempted
|
|
224
|
+
# here to keep this one code path correct for every era uniformly).
|
|
225
|
+
def query(declared, args = {}, context: {})
|
|
226
|
+
return super if @field_caches.empty? || declared.wheres.empty?
|
|
227
|
+
|
|
228
|
+
evaluated = declared.wheres.map { |clause| [clause, query_value(clause.value, args)] }
|
|
229
|
+
cached, uncached = evaluated.partition { |clause, value| cache_eligible?(clause, value) }
|
|
230
|
+
return super if cached.empty?
|
|
231
|
+
|
|
232
|
+
ids = cache_phase(cached)
|
|
233
|
+
return [] if ids.empty?
|
|
234
|
+
|
|
235
|
+
head_phase(declared, uncached, ids, args)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the
|
|
239
|
+
# ordinal is assigned by the column's own `nextval()` default, inside
|
|
240
|
+
# this same statement, so the lock has to already be held before that
|
|
241
|
+
# default evaluates. A DIFFERENT key from `mint_era!`/`merge_tail!`'s
|
|
242
|
+
# `hecks_eras:domain` : this serializes plain writes against EACH
|
|
243
|
+
# OTHER, never against a mint. See postgres/lineage.rb's own comment
|
|
244
|
+
# for why only that half of the race is closed.
|
|
245
|
+
# The journal insert and the snapshot upsert/delete happen in the
|
|
246
|
+
# SAME transaction — real ACID atomicity, not the append-then-
|
|
247
|
+
# project two-step a file-based adapter needs a crash-recovery
|
|
248
|
+
# replay for (see Heki). If this transaction commits, the snapshot
|
|
249
|
+
# is already exactly as current as the journal; if it doesn't,
|
|
250
|
+
# neither happened. `project` stays uninvolved on purpose — it
|
|
251
|
+
# still runs, cheaply, during AppendOnly#recover!'s full replay on
|
|
252
|
+
# every boot (see `project` below), and a second write there would
|
|
253
|
+
# make that replay pay real DB cost for a snapshot that's already
|
|
254
|
+
# correct.
|
|
255
|
+
def append(entry)
|
|
256
|
+
@db.transaction do
|
|
257
|
+
lock_writes!
|
|
258
|
+
append_and_project!(entry)
|
|
259
|
+
end
|
|
260
|
+
entry
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Outcome detection, journal append and every derived projection share
|
|
264
|
+
# the SAME transaction and domain write lock. The lineage-aware head
|
|
265
|
+
# determines whether this id is already visible; no repository `find`
|
|
266
|
+
# occurs before entering this adapter-native operation.
|
|
267
|
+
def atomic_put(entry, insert_only: false)
|
|
268
|
+
status = nil
|
|
269
|
+
@db.transaction do
|
|
270
|
+
lock_writes!
|
|
271
|
+
exists = !@db.exec_params(
|
|
272
|
+
"SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1",
|
|
273
|
+
[entry.id.to_s]
|
|
274
|
+
).ntuples.zero?
|
|
275
|
+
if insert_only && exists
|
|
276
|
+
status = :conflicted
|
|
277
|
+
next
|
|
278
|
+
end
|
|
279
|
+
status = exists ? :replaced : :inserted
|
|
280
|
+
append_and_project!(entry)
|
|
281
|
+
end
|
|
282
|
+
status
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# The head is DERIVED — projecting is reading, so there is nothing
|
|
286
|
+
# to write here. `append` above already keeps the snapshot the head
|
|
287
|
+
# view reads from current, transactionally. The instance is still
|
|
288
|
+
# built (and validated) so a save returns what every other adapter
|
|
289
|
+
# returns.
|
|
290
|
+
def project(entry)
|
|
291
|
+
return if entry.delete?
|
|
292
|
+
|
|
293
|
+
Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def entries
|
|
297
|
+
@db.exec_params(
|
|
298
|
+
"SELECT aggregate_id, operation, state, mirrors FROM #{@lineage.quoted_journal} " \
|
|
299
|
+
"WHERE aggregate = $1 ORDER BY ordinal",
|
|
300
|
+
[table]
|
|
301
|
+
).map do |row|
|
|
302
|
+
state = row["state"] && JSON.parse(row["state"])
|
|
303
|
+
Ports::Persistence::Entry.new(
|
|
304
|
+
operation: row["operation"] || "save",
|
|
305
|
+
id: row["aggregate_id"],
|
|
306
|
+
state: state&.transform_keys(&:to_sym),
|
|
307
|
+
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def reset!
|
|
313
|
+
@db.exec_params("DELETE FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table])
|
|
314
|
+
self
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def save(instance)
|
|
318
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
319
|
+
append(entry)
|
|
320
|
+
project(entry)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def delete(id)
|
|
324
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
325
|
+
append(entry)
|
|
326
|
+
true
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def record_event(event)
|
|
330
|
+
@db.exec_params(
|
|
331
|
+
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
|
|
332
|
+
[event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def events
|
|
337
|
+
@db.exec("SELECT * FROM events ORDER BY id").map do |row|
|
|
338
|
+
Runtime::Event.new(
|
|
339
|
+
name: row["name"],
|
|
340
|
+
aggregate: row["aggregate"],
|
|
341
|
+
id: row["aggregate_id"],
|
|
342
|
+
payload: JSON.parse(row["payload"], symbolize_names: true),
|
|
343
|
+
occurred_at: row["occurred_at"]
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# ── the OPTIONAL saga-persistence capability (Ports::Persistence's
|
|
349
|
+
# own three-method shape, §2) — one row per (domain, process_manager,
|
|
350
|
+
# correlation), `domain` kept as an explicit column even under
|
|
351
|
+
# schema isolation so two domains sharing one schema (neither
|
|
352
|
+
# declares its own `schema`) still isolate correctly, matching
|
|
353
|
+
# `hecks_eras`' own precedent (postgres/lineage/provisioning.rb).
|
|
354
|
+
# No advisory lock of its own: every call here already runs inside
|
|
355
|
+
# `SagaInterpreter`'s own mutex (§7) serializing IN-PROCESS writers,
|
|
356
|
+
# and gets the SAME cross-process safety an aggregate's own writes
|
|
357
|
+
# get from this adapter — no better, no worse.
|
|
358
|
+
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
359
|
+
@db.exec_params(
|
|
360
|
+
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
|
|
361
|
+
"VALUES ($1, $2, $3, $4, $5) " \
|
|
362
|
+
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
363
|
+
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, updated_at = now()",
|
|
364
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
|
|
365
|
+
)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def delete_saga(process_manager:, correlation:)
|
|
369
|
+
@db.exec_params(
|
|
370
|
+
"DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
|
|
371
|
+
[@domain, process_manager.to_s, correlation.to_s]
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def each_saga
|
|
376
|
+
return enum_for(:each_saga) unless block_given?
|
|
377
|
+
|
|
378
|
+
@db.exec_params(
|
|
379
|
+
"SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
|
|
380
|
+
[@domain]
|
|
381
|
+
).each do |row|
|
|
382
|
+
yield row["process_manager"], row["correlation"], row["state"],
|
|
383
|
+
JSON.parse(row["memory"], symbolize_names: true)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
389
|
+
def lock_writes!
|
|
390
|
+
@db.exec_params(
|
|
391
|
+
"SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
|
|
392
|
+
[@lineage.domain]
|
|
393
|
+
)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def append_and_project!(entry)
|
|
397
|
+
state_json = entry.state && JSON.generate(entry.state)
|
|
398
|
+
ordinal = @db.exec_params(
|
|
399
|
+
"INSERT INTO #{@lineage.quoted_journal} (era, aggregate, aggregate_id, operation, state, mirrors) " \
|
|
400
|
+
"VALUES ($1, $2, $3, $4, $5, $6) RETURNING ordinal",
|
|
401
|
+
[@era, table, entry.id, entry.operation, state_json,
|
|
402
|
+
entry.mirrors && JSON.generate(entry.mirrors)]
|
|
403
|
+
)[0]["ordinal"]
|
|
404
|
+
|
|
405
|
+
if entry.save?
|
|
406
|
+
@db.exec_params(
|
|
407
|
+
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
408
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state " \
|
|
409
|
+
"WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
410
|
+
[entry.id, ordinal, state_json]
|
|
411
|
+
)
|
|
412
|
+
# SAME TRANSACTION, SAME ORDINAL — every field cache stays
|
|
413
|
+
# exactly as current as the snapshot it's derived from, for
|
|
414
|
+
# the identical reason `postgres_era.rb`'s own header comment
|
|
415
|
+
# gives for the journal/snapshot pair: if this transaction
|
|
416
|
+
# commits, every cache row is already correct; if it doesn't,
|
|
417
|
+
# none of them changed.
|
|
418
|
+
@field_caches.each do |field, cache_table|
|
|
419
|
+
@lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
|
|
420
|
+
end
|
|
421
|
+
else
|
|
422
|
+
@db.exec_params("DELETE FROM #{quoted_head_snapshot} WHERE id = $1", [entry.id])
|
|
423
|
+
@field_caches.each_value { |cache_table| @lineage.delete_field_cache_row!(cache_table, entry.id) }
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
ordinal
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# ── SqlQueryBuilder's dialect hooks ─────────────────────────────
|
|
430
|
+
|
|
431
|
+
def select_list = "id, state"
|
|
432
|
+
def from_relation = quoted_head
|
|
433
|
+
def dialect_name = "PostgresEra"
|
|
434
|
+
def empty_in_clause = "FALSE"
|
|
435
|
+
|
|
436
|
+
def placeholder(binds, value)
|
|
437
|
+
binds << value
|
|
438
|
+
"$#{binds.size}"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def contains_clause(expression, placeholder)
|
|
442
|
+
"position(#{placeholder} in #{expression}) > 0"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def list_contains_clause(column, member, placeholder)
|
|
446
|
+
target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
|
|
447
|
+
elements = "jsonb_array_elements(state #> ARRAY[#{text_literal(column)}]::text[]) AS elem"
|
|
448
|
+
"EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def plain_column(name) = jsonb_path([name])
|
|
452
|
+
|
|
453
|
+
def nested_expression(name, path, member)
|
|
454
|
+
segments = path.empty? ? [name, (member || "value").to_s] : [name, *path]
|
|
455
|
+
jsonb_path(segments)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def comparable_expression(expression, value)
|
|
459
|
+
value.is_a?(Numeric) ? "(#{expression})::numeric" : expression
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def execute_query(sql, binds)
|
|
463
|
+
@db.exec_params(sql, binds).map { |row| instance(row) }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# ── the rest of the dialect ─────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
def instance(row)
|
|
469
|
+
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row["state"]))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def decode(state_json)
|
|
473
|
+
# Deep symbols, exactly what the Sqlite adapter's per-column
|
|
474
|
+
# `symbolize_names:` decode produces — value-object members and
|
|
475
|
+
# list elements arrive symbol-keyed either way.
|
|
476
|
+
JSON.parse(state_json, symbolize_names: true)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
|
|
480
|
+
def quoted_head = quote_ident(@lineage.head_view(table))
|
|
481
|
+
def quoted_head_snapshot = quote_ident(@lineage.head_snapshot(table, @era))
|
|
482
|
+
|
|
483
|
+
def order_expression(field)
|
|
484
|
+
expression = query_expression(field)
|
|
485
|
+
numeric_field?(field) ? "(#{expression})::numeric" : expression
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Postgres defaults to NULLS LAST on ASC; the port's in-memory
|
|
489
|
+
# semantics (NullPolicy.order, which SQLite's own default happens
|
|
490
|
+
# to match) put null rows FIRST ascending and LAST
|
|
491
|
+
# descending. Compile the placement explicitly so a declared query
|
|
492
|
+
# answers identically no matter which adapter serves it.
|
|
493
|
+
def order_clause(order_by, policy)
|
|
494
|
+
direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
|
|
495
|
+
nulls = case policy&.mode.to_s
|
|
496
|
+
when "first" then " NULLS FIRST"
|
|
497
|
+
when "last" then " NULLS LAST"
|
|
498
|
+
else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
|
|
499
|
+
end
|
|
500
|
+
"#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# One shared walk decides numericness at ANY depth — this used to
|
|
504
|
+
# inspect only the first nested segment, so a two-level path
|
|
505
|
+
# (pizza.price_cents.cents) skipped the ::numeric cast and ordered
|
|
506
|
+
# as text: "900" above "1200".
|
|
507
|
+
def numeric_field?(field)
|
|
508
|
+
name, *path = field.to_s.split(".")
|
|
509
|
+
QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
|
|
510
|
+
@aggregate.value_object(type)
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# ARRAY[...] of individually-escaped literals, never the hand-rolled
|
|
515
|
+
# '{a,b,c}' array-literal SYNTAX — a segment is a field or
|
|
516
|
+
# value-object member name, and while today's callers only ever
|
|
517
|
+
# pass schema-declared names, this method has no way to know
|
|
518
|
+
# that, and the '{...}' form has no escaping at all: a segment
|
|
519
|
+
# containing a single quote closes the string early and whatever
|
|
520
|
+
# follows becomes live SQL. Measured, not assumed — a crafted
|
|
521
|
+
# field name of `x}' = '' OR $1::text = $1::text -- ` made a
|
|
522
|
+
# `where(secret: "public")` clause return every row regardless,
|
|
523
|
+
# against the OLD form; the ARRAY[] form below closes it, verified
|
|
524
|
+
# against the identical payload.
|
|
525
|
+
def jsonb_path(segments)
|
|
526
|
+
"state #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
|
|
530
|
+
|
|
531
|
+
# ── the field-cache read shortcut (Track C) ─────────────────────
|
|
532
|
+
|
|
533
|
+
# EVERY declared `where`-field this aggregate's own queries and its
|
|
534
|
+
# entities' own queries use, minus anything a cache table can't
|
|
535
|
+
# represent (see `cacheable_field?`) — never `order_by`-only
|
|
536
|
+
# fields, which never needed a cache in the first place (sorting
|
|
537
|
+
# the reduced output was never blocked by the reduction; only
|
|
538
|
+
# filtering was — see field_cache.rb's own header).
|
|
539
|
+
def ensure_field_caches!
|
|
540
|
+
cached_where_fields.to_h do |field|
|
|
541
|
+
[field, @lineage.ensure_field_cache!(table, @era, field, query_expression(field))]
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def cached_where_fields
|
|
546
|
+
declared_queries.flat_map { |q|
|
|
547
|
+
q.wheres.map { |clause|
|
|
548
|
+
clause.field.to_s
|
|
549
|
+
}
|
|
550
|
+
}.uniq.select { |field| cacheable_field?(field) }
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def declared_queries
|
|
554
|
+
@aggregate.queries + @aggregate.entities.flat_map(&:queries)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# LIST-TYPED FIELDS ARE EXCLUDED, same boundary the plain `Postgres`
|
|
558
|
+
# and `Sqlite`/`D1` adapters independently landed on for their own
|
|
559
|
+
# automatic indexing: `contains` means element membership, and a
|
|
560
|
+
# (id, ordinal, ONE value) cache row has nowhere to put more than
|
|
561
|
+
# one element. Everything else — a plain scalar, the lifecycle
|
|
562
|
+
# field, or a non-list value-object member path — reduces to
|
|
563
|
+
# exactly one comparable value per id, which is the one shape this
|
|
564
|
+
# cache table represents.
|
|
565
|
+
def cacheable_field?(field)
|
|
566
|
+
name = field.to_s.split(".").first
|
|
567
|
+
return true if @aggregate.lifecycle&.field.to_s == name
|
|
568
|
+
|
|
569
|
+
attribute = @aggregate.attribute(name)
|
|
570
|
+
!attribute.nil? && !attribute.list?
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# A clause can be served by the cache when its field has a cache
|
|
574
|
+
# table AND it isn't the null-vs-value special case
|
|
575
|
+
# `QuerySpecification::Common::NullPolicy` intercepts before
|
|
576
|
+
# `where_clause` ever runs (see `query`'s own header comment) — a
|
|
577
|
+
# clause that fails either check simply flows to `head_phase`
|
|
578
|
+
# unaccelerated, exactly as `super` would have evaluated it.
|
|
579
|
+
def cache_eligible?(clause, value)
|
|
580
|
+
@field_caches.key?(clause.field.to_s) &&
|
|
581
|
+
QuerySpecification::Common::NullPolicy.sql_predicate(query_expression(clause.field, value: value), clause.op,
|
|
582
|
+
value).nil?
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# PHASE ONE — candidate ids, no reduction touched. One SELECT per
|
|
586
|
+
# cached clause against its own narrow (id, ordinal, value) table,
|
|
587
|
+
# `INTERSECT`ed into the set that satisfies every cached clause at
|
|
588
|
+
# once. Reuses `where_clause` (SqlQueryBuilder, private, already
|
|
589
|
+
# mixed into this class) UNCHANGED against the cache table's own
|
|
590
|
+
# `value` column instead of a jsonb path expression — the exact
|
|
591
|
+
# same operator compilation (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`)
|
|
592
|
+
# a live query already gets against the real column, so a cached
|
|
593
|
+
# field supports every comparator `super` would have, not just the
|
|
594
|
+
# `eq` example in the plan doc's own illustration.
|
|
595
|
+
def cache_phase(cached)
|
|
596
|
+
binds = []
|
|
597
|
+
clauses = cached.map do |clause, value|
|
|
598
|
+
cache_table = @lineage.field_cache(table, @era, clause.field.to_s)
|
|
599
|
+
"SELECT id FROM #{quote_ident(cache_table)} WHERE #{where_clause(clause.op.to_s, quote_ident('value'), value, binds,
|
|
600
|
+
field: clause.field)}"
|
|
601
|
+
end
|
|
602
|
+
@db.exec_params(clauses.join("\nINTERSECT\n"), binds).map { |row| row["id"] }
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# PHASE TWO — `head_view`, restricted to phase one's candidate ids
|
|
606
|
+
# PLUS whatever clauses phase one couldn't accelerate, applied
|
|
607
|
+
# exactly the way `SqlQueryBuilder#query` (`super`) already applies
|
|
608
|
+
# every clause today: against the fully-reduced view, which is
|
|
609
|
+
# already correct regardless of caching (see this file's own
|
|
610
|
+
# `query` comment — a cache is a SPEED shortcut, never a
|
|
611
|
+
# correctness fix; head_view was always safe to filter directly,
|
|
612
|
+
# just expensive to reduce in the first place). Duplicates a small
|
|
613
|
+
# slice of `SqlQueryBuilder#query`'s own tail assembly (order_by/
|
|
614
|
+
# limit/offset) rather than reaching into it — principle 2
|
|
615
|
+
# (docs/implemented/postgres-era-adapter-split-plan.md): SqlQueryBuilder stays
|
|
616
|
+
# untouched by the era workaround, so this stays local to
|
|
617
|
+
# PostgresEra rather than growing a shared hook only one adapter
|
|
618
|
+
# would ever call.
|
|
619
|
+
def head_phase(declared, uncached, ids, args)
|
|
620
|
+
binds = []
|
|
621
|
+
clauses = ["id IN (#{ids.map { |id| placeholder(binds, id) }.join(', ')})"]
|
|
622
|
+
uncached.each do |clause, value|
|
|
623
|
+
expression = query_expression(clause.field, value: value)
|
|
624
|
+
if (null_predicate = QuerySpecification::Common::NullPolicy.sql_predicate(expression, clause.op, value))
|
|
625
|
+
clauses << null_predicate.first
|
|
626
|
+
next
|
|
627
|
+
end
|
|
628
|
+
clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
sql = +"SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
|
|
632
|
+
sql << if declared.order_by
|
|
633
|
+
" ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
|
|
634
|
+
else
|
|
635
|
+
" ORDER BY id"
|
|
636
|
+
end
|
|
637
|
+
sql << " LIMIT #{placeholder(binds, query_value(declared.limit.value, args).to_i)}" if declared.limit
|
|
638
|
+
sql << unbounded_limit if !declared.limit && declared.offset
|
|
639
|
+
sql << " OFFSET #{placeholder(binds, query_value(declared.offset.value, args).to_i)}" if declared.offset
|
|
640
|
+
execute_query(sql, binds)
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def create_event_table!
|
|
644
|
+
@db.exec(<<~SQL)
|
|
645
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
646
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
647
|
+
name text NOT NULL,
|
|
648
|
+
aggregate text NOT NULL,
|
|
649
|
+
aggregate_id text NOT NULL,
|
|
650
|
+
payload jsonb,
|
|
651
|
+
occurred_at text
|
|
652
|
+
)
|
|
653
|
+
SQL
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def create_saga_table!
|
|
657
|
+
@db.exec(<<~SQL)
|
|
658
|
+
CREATE TABLE IF NOT EXISTS hecks_saga_instances (
|
|
659
|
+
domain text NOT NULL,
|
|
660
|
+
process_manager text NOT NULL,
|
|
661
|
+
correlation text NOT NULL,
|
|
662
|
+
state text NOT NULL,
|
|
663
|
+
memory jsonb NOT NULL,
|
|
664
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
665
|
+
PRIMARY KEY (domain, process_manager, correlation)
|
|
666
|
+
)
|
|
667
|
+
SQL
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|