hecks 0.2.0 → 1.0.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 +427 -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 +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -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 +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -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 +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -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 +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -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 +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -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 +78 -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 +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -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 +430 -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 +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -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 +173 -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 +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -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 +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -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 +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -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 +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -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 +66 -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 +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -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 +166 -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 +341 -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 +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -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 +145 -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/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -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 +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -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 +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -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 +333 -0
- data/lib/hecks/language/port.bluebook +120 -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 +174 -0
- data/lib/hecks/ports/access_control.port +9 -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 +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -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 +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -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 +96 -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 +642 -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 +44 -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 +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -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 +411 -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 +92 -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 +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -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/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -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 +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -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 +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -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 +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -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 +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -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,438 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
require_relative "sql_query_builder"
|
|
4
|
+
require_relative "postgres/schema_builder"
|
|
5
|
+
require_relative "postgres/codec"
|
|
6
|
+
require_relative "../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../query_specification/common/null_policy"
|
|
8
|
+
require_relative "../../query_specification/common/order_by"
|
|
9
|
+
require_relative "../../query_specification/field_path"
|
|
10
|
+
require_relative "../../runtime/errors"
|
|
11
|
+
require_relative "../../runtime/event"
|
|
12
|
+
require_relative "../../runtime/instance"
|
|
13
|
+
|
|
14
|
+
module Hecks
|
|
15
|
+
module Adapters
|
|
16
|
+
# The plain Postgres store — flat, one table per aggregate, real
|
|
17
|
+
# typed columns for scalars and jsonb for nested/list attributes,
|
|
18
|
+
# exactly the shape `Sqlite` already made for its own table. Sibling
|
|
19
|
+
# to `PostgresEra` (postgres_era.rb), which is the same database with
|
|
20
|
+
# full lineage/era machinery on top — pick this one unless a domain
|
|
21
|
+
# actually needs to survive a live shape change. See
|
|
22
|
+
# docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
|
|
23
|
+
#
|
|
24
|
+
# No `hecks_eras`, no lineage, no advisory-lock-per-write for era
|
|
25
|
+
# tracking, no `lineage_capable?`/`era_check!` — this class simply
|
|
26
|
+
# doesn't define those methods at all, and the capability idiom
|
|
27
|
+
# elsewhere already treats their absence as "not lineage-capable".
|
|
28
|
+
#
|
|
29
|
+
# Storage shape (see postgres/schema_builder.rb for the DDL,
|
|
30
|
+
# postgres/codec.rb for the encode/decode):
|
|
31
|
+
# - One real column per attribute, typed for a scalar
|
|
32
|
+
# (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
|
|
33
|
+
# or list-typed attribute — never JSON-in-TEXT the way `Sqlite` has
|
|
34
|
+
# to, since Postgres has a native jsonb type.
|
|
35
|
+
# - `append` and `project` are two real Postgres statements — `save`/
|
|
36
|
+
# `delete` wrap them in ONE transaction, same "the journal insert
|
|
37
|
+
# and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
|
|
38
|
+
# comment gives: a crash between the two must never leave a
|
|
39
|
+
# half-written state. `append`/`project` stay plain, individually-
|
|
40
|
+
# callable methods (never wrapping their own transaction) so
|
|
41
|
+
# `AppendOnly#recover!`'s replay — `project` alone, no `append` —
|
|
42
|
+
# keeps working the same way it does on every other adapter.
|
|
43
|
+
class Postgres
|
|
44
|
+
include SqlQueryBuilder
|
|
45
|
+
include SchemaBuilder
|
|
46
|
+
include Codec
|
|
47
|
+
|
|
48
|
+
SQL_TYPES = { "Integer" => "bigint", "Float" => "double precision" }.freeze
|
|
49
|
+
|
|
50
|
+
attr_reader :aggregate
|
|
51
|
+
|
|
52
|
+
def persistence_capabilities = [:atomic_put, :optimistic_concurrency]
|
|
53
|
+
|
|
54
|
+
def self.connect_for(name, settings)
|
|
55
|
+
# LAZY, ON PURPOSE — same reasoning as PostgresEra's own
|
|
56
|
+
# connect_for: a domain that never wires Postgres should never
|
|
57
|
+
# need the gem installed.
|
|
58
|
+
require "pg"
|
|
59
|
+
|
|
60
|
+
declared = settings.key?(:database) ? settings[:database] : settings["database"]
|
|
61
|
+
if declared.to_s.empty?
|
|
62
|
+
raise Runtime::WiringError,
|
|
63
|
+
"#{name} binds Postgres, which needs a database connection, " \
|
|
64
|
+
"but its world declares no \"database\"."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
connection =
|
|
68
|
+
if declared.start_with?("postgres://", "postgresql://")
|
|
69
|
+
PG.connect(declared)
|
|
70
|
+
else
|
|
71
|
+
PG.connect(dbname: declared)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# SHARED-INSTANCE ISOLATION — same as PostgresEra's own: a
|
|
75
|
+
# domain that declares `schema` is sharing its Postgres instance
|
|
76
|
+
# with other domains, so every unqualified reference this
|
|
77
|
+
# adapter constructs resolves through search_path. A domain with
|
|
78
|
+
# no `schema` setting keeps Postgres's own default (public).
|
|
79
|
+
schema = settings.key?(:schema) ? settings[:schema] : settings["schema"]
|
|
80
|
+
connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
|
|
81
|
+
|
|
82
|
+
# QUIET ON PURPOSE — same reasoning as PostgresEra's own: a
|
|
83
|
+
# schema/table that already exists is the ORDINARY case on every
|
|
84
|
+
# boot after the first, not news.
|
|
85
|
+
connection.exec("SET client_min_messages = warning")
|
|
86
|
+
connection
|
|
87
|
+
rescue PG::Error => error
|
|
88
|
+
raise Runtime::WiringError,
|
|
89
|
+
"cannot bind Postgres at #{declared} for #{name}: #{error.message.strip}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
93
|
+
@aggregate = aggregate
|
|
94
|
+
@db = self.class.connect_for(aggregate.name, settings)
|
|
95
|
+
# THE OPTIONAL saga-persistence capability's own scoping column
|
|
96
|
+
# (§2/§4) — falls back to the aggregate's own storage name for a
|
|
97
|
+
# directly-instantiated adapter (specs), same fallback shape
|
|
98
|
+
# Sqlite's own @domain already uses.
|
|
99
|
+
@domain = (
|
|
100
|
+
if settings.key?(:domain)
|
|
101
|
+
settings[:domain]
|
|
102
|
+
elsif settings.key?("domain")
|
|
103
|
+
settings["domain"]
|
|
104
|
+
else
|
|
105
|
+
aggregate.storage_name
|
|
106
|
+
end
|
|
107
|
+
).to_s
|
|
108
|
+
|
|
109
|
+
create_aggregate_table!
|
|
110
|
+
create_entry_table!
|
|
111
|
+
create_event_table!
|
|
112
|
+
create_saga_table!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def table = @aggregate.storage_name
|
|
116
|
+
|
|
117
|
+
def find(id)
|
|
118
|
+
result = @db.exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
|
|
119
|
+
return nil if result.ntuples.zero?
|
|
120
|
+
|
|
121
|
+
instance_from_row(result[0])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# order_by IS A RUNTIME VALUE — see Sqlite#all's own reasoning;
|
|
125
|
+
# whitelisted the identical way before it ever reaches
|
|
126
|
+
# order_expression.
|
|
127
|
+
def all(order_by: nil, direction: :asc)
|
|
128
|
+
order_sql = "ORDER BY id"
|
|
129
|
+
if order_by
|
|
130
|
+
name = order_by.to_s.split(".").first
|
|
131
|
+
raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
132
|
+
|
|
133
|
+
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
134
|
+
order_sql = "ORDER BY #{order_clause(spec, nil)}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@db.exec("SELECT * FROM #{quoted_table} #{order_sql}").map { |row| instance_from_row(row) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def count = @db.exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
|
|
141
|
+
|
|
142
|
+
def append(entry)
|
|
143
|
+
@db.exec_params(
|
|
144
|
+
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
|
|
145
|
+
# `mirrors` (unlike `state`) is a NULLABLE column — an absent
|
|
146
|
+
# mirrors hash must bind a real SQL NULL, not the four-character
|
|
147
|
+
# JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
|
|
148
|
+
# check against it would never match. Same guard `sqlite.rb`/
|
|
149
|
+
# `d1.rb`/`postgres_era.rb` already use for their own journal's
|
|
150
|
+
# `mirrors` column.
|
|
151
|
+
[entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
|
|
152
|
+
)
|
|
153
|
+
entry
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# `expected_version:` requests optimistic-concurrency CAS (see
|
|
157
|
+
# `persistence_capabilities`/`Ports::Persistence::AppendOnly#save`).
|
|
158
|
+
# `hecks_version` is ADAPTER BOOKKEEPING — never in `persisted_fields`
|
|
159
|
+
# (Codec), so it never reaches `decode`'s domain-state hash. It goes
|
|
160
|
+
# in the INSERT column list at `1` (a genuinely new row) and bumps by
|
|
161
|
+
# one in the `ON CONFLICT DO UPDATE` branch; when `expected_version`
|
|
162
|
+
# is given, that UPDATE branch additionally requires
|
|
163
|
+
# `hecks_version = expected_version` to apply at all — Postgres's own
|
|
164
|
+
# `INSERT ... ON CONFLICT DO UPDATE ... WHERE`, which gates only
|
|
165
|
+
# whether the CONFLICT branch's update applies. A genuinely new row
|
|
166
|
+
# never reaches that branch at all, so it always inserts regardless
|
|
167
|
+
# of this WHERE. `RETURNING hecks_version` plus `ntuples.zero?` is
|
|
168
|
+
# how a real version mismatch is told apart from an ordinary write:
|
|
169
|
+
# zero rows back means the conflict branch's WHERE excluded the row
|
|
170
|
+
# entirely — the version had already moved — so `nil` is returned
|
|
171
|
+
# for the caller (`AppendOnly#save`) to treat as "stale, no-op".
|
|
172
|
+
def project(entry, expected_version: nil)
|
|
173
|
+
return @db.exec_params("DELETE FROM #{quoted_table} WHERE id = $1", [entry.id]) if entry.delete?
|
|
174
|
+
|
|
175
|
+
instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
176
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s } + ["hecks_version"])
|
|
177
|
+
values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) } + [1]
|
|
178
|
+
updates = persisted_fields.map { |field| "#{quote_ident(field[:name])} = EXCLUDED.#{quote_ident(field[:name])}" } +
|
|
179
|
+
["hecks_version = #{quoted_table}.hecks_version + 1"]
|
|
180
|
+
|
|
181
|
+
sql = "INSERT INTO #{quoted_table} (#{columns.map { |c| quote_ident(c) }.join(', ')}) " \
|
|
182
|
+
"VALUES (#{(1..columns.size).map { |n| "$#{n}" }.join(', ')}) " \
|
|
183
|
+
"ON CONFLICT (id) DO UPDATE SET #{updates.join(', ')}"
|
|
184
|
+
if expected_version
|
|
185
|
+
values += [expected_version]
|
|
186
|
+
sql += " WHERE #{quoted_table}.hecks_version = $#{values.size}"
|
|
187
|
+
end
|
|
188
|
+
sql += " RETURNING hecks_version"
|
|
189
|
+
|
|
190
|
+
result = @db.exec_params(sql, values)
|
|
191
|
+
return nil if result.ntuples.zero?
|
|
192
|
+
|
|
193
|
+
instance.version = result[0]["hecks_version"].to_i
|
|
194
|
+
instance
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def entries
|
|
198
|
+
@db.exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
|
|
199
|
+
state = JSON.parse(row["state"])
|
|
200
|
+
Ports::Persistence::Entry.new(
|
|
201
|
+
operation: row["operation"] || "save",
|
|
202
|
+
id: row["aggregate_id"],
|
|
203
|
+
state: state&.transform_keys(&:to_sym),
|
|
204
|
+
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def reset!
|
|
210
|
+
@db.exec("DELETE FROM #{quoted_table}")
|
|
211
|
+
@db.exec("DELETE FROM #{quoted_entry_table}")
|
|
212
|
+
self
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# ONE TRANSACTION, not the plain append-then-project two-step a
|
|
216
|
+
# file-based adapter needs a crash-recovery replay for (Heki) —
|
|
217
|
+
# real Postgres ACID atomicity is sitting right there, so a crash
|
|
218
|
+
# between the journal insert and the table upsert must not leave
|
|
219
|
+
# the two disagreeing. `append`/`project` themselves stay plain,
|
|
220
|
+
# transaction-free methods (see the class comment above) — the
|
|
221
|
+
# transaction lives here, the one caller that runs both together.
|
|
222
|
+
def save(instance)
|
|
223
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
224
|
+
@db.transaction { append(entry); project(entry) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Classification, journal append and snapshot replacement are one
|
|
228
|
+
# Postgres transaction. A row lock cannot serialize two first writers —
|
|
229
|
+
# there is no row to lock yet — so a transaction-scoped advisory lock on
|
|
230
|
+
# schema/table + aggregate id owns that missing-row race as well. Once a
|
|
231
|
+
# writer acquires it, the preceding writer has committed and status can
|
|
232
|
+
# be read from the materialized table without a runtime-side find.
|
|
233
|
+
def atomic_put(entry, insert_only: false)
|
|
234
|
+
status = nil
|
|
235
|
+
@db.transaction do
|
|
236
|
+
@db.exec_params(
|
|
237
|
+
"SELECT pg_advisory_xact_lock(" \
|
|
238
|
+
"hashtext(current_schema() || ':' || $1), hashtext($2))",
|
|
239
|
+
[table, entry.id.to_s]
|
|
240
|
+
)
|
|
241
|
+
exists = !@db.exec_params(
|
|
242
|
+
"SELECT 1 FROM #{quoted_table} WHERE id = $1",
|
|
243
|
+
[entry.id.to_s]
|
|
244
|
+
).ntuples.zero?
|
|
245
|
+
if insert_only && exists
|
|
246
|
+
status = :conflicted
|
|
247
|
+
next
|
|
248
|
+
end
|
|
249
|
+
status = exists ? :replaced : :inserted
|
|
250
|
+
append(entry)
|
|
251
|
+
project(entry)
|
|
252
|
+
end
|
|
253
|
+
status
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def delete(id)
|
|
257
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
258
|
+
@db.transaction { append(entry); project(entry) }
|
|
259
|
+
true
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def record_event(event)
|
|
263
|
+
@db.exec_params(
|
|
264
|
+
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
|
|
265
|
+
[event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def events
|
|
270
|
+
@db.exec("SELECT * FROM events ORDER BY id").map do |row|
|
|
271
|
+
Runtime::Event.new(
|
|
272
|
+
name: row["name"],
|
|
273
|
+
aggregate: row["aggregate"],
|
|
274
|
+
id: row["aggregate_id"],
|
|
275
|
+
payload: JSON.parse(row["payload"], symbolize_names: true),
|
|
276
|
+
occurred_at: row["occurred_at"]
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# ── the OPTIONAL saga-persistence capability (§2) — same DDL and
|
|
282
|
+
# shape as PostgresEra's own (postgres_era.rb), not lineage-
|
|
283
|
+
# specific, copied verbatim.
|
|
284
|
+
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
285
|
+
@db.exec_params(
|
|
286
|
+
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
|
|
287
|
+
"VALUES ($1, $2, $3, $4, $5, $6) " \
|
|
288
|
+
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
289
|
+
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
|
|
290
|
+
"completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
|
|
291
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def delete_saga(process_manager:, correlation:)
|
|
296
|
+
@db.exec_params(
|
|
297
|
+
"DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
|
|
298
|
+
[@domain, process_manager.to_s, correlation.to_s]
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def each_saga
|
|
303
|
+
return enum_for(:each_saga) unless block_given?
|
|
304
|
+
|
|
305
|
+
@db.exec_params(
|
|
306
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1",
|
|
307
|
+
[@domain]
|
|
308
|
+
).each do |row|
|
|
309
|
+
yield row["process_manager"], row["correlation"], row["state"],
|
|
310
|
+
JSON.parse(row["memory"], symbolize_names: true),
|
|
311
|
+
JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private
|
|
316
|
+
|
|
317
|
+
# ── SqlQueryBuilder's dialect hooks ─────────────────────────────
|
|
318
|
+
|
|
319
|
+
def select_list = "*"
|
|
320
|
+
def from_relation = quoted_table
|
|
321
|
+
def dialect_name = "Postgres"
|
|
322
|
+
def empty_in_clause = "FALSE"
|
|
323
|
+
|
|
324
|
+
def placeholder(binds, value)
|
|
325
|
+
binds << value
|
|
326
|
+
"$#{binds.size}"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def contains_clause(expression, placeholder)
|
|
330
|
+
"position(#{placeholder} in #{expression}) > 0"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# THE LIST COLUMN ITSELF IS THE JSONB ARRAY — no reaching into a
|
|
334
|
+
# shared blob a jsonb path has to walk into first (PostgresEra's
|
|
335
|
+
# own version does, since every attribute there shares one `state`
|
|
336
|
+
# column). Here, `column` names a real column of its own, already
|
|
337
|
+
# jsonb, already the array.
|
|
338
|
+
def list_contains_clause(column, member, placeholder)
|
|
339
|
+
target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
|
|
340
|
+
elements = "jsonb_array_elements(#{quote_ident(column)}) AS elem"
|
|
341
|
+
"EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def plain_column(name) = quote_ident(name)
|
|
345
|
+
|
|
346
|
+
# PostgresEra's own `jsonb_path` walks `[name, *path]` into ONE
|
|
347
|
+
# shared `state` column — the attribute name is PART of the path
|
|
348
|
+
# there. Here the attribute name IS THE COLUMN: the path into it
|
|
349
|
+
# is whatever is LEFT after the column, never the column name
|
|
350
|
+
# repeated inside its own path.
|
|
351
|
+
def nested_expression(name, path, member)
|
|
352
|
+
segments = path.empty? ? [(member || "value").to_s] : path
|
|
353
|
+
jsonb_path(name, segments)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Scalar, non-value-object attributes get a REAL typed column
|
|
357
|
+
# (bigint/double precision/text) — comparing and sorting one needs
|
|
358
|
+
# no cast at all, unlike PostgresEra's shared jsonb `state` blob,
|
|
359
|
+
# where even a top-level scalar only ever comes out of `#>>` as
|
|
360
|
+
# text. A jsonb-extracted value (a value-object member reached
|
|
361
|
+
# through `nested_expression`/`jsonb_path` above) still comes out
|
|
362
|
+
# of `#>>` as text the exact same way PostgresEra's own does, and
|
|
363
|
+
# still needs the same `::numeric` cast to compare/sort
|
|
364
|
+
# numerically rather than lexicographically. `jsonb_extraction?`
|
|
365
|
+
# tells the two apart by inspecting the expression `query_expression`
|
|
366
|
+
# already built — never a second, hand-rolled field walk that
|
|
367
|
+
# could disagree with the one the SQL actually uses.
|
|
368
|
+
def comparable_expression(expression, value)
|
|
369
|
+
value.is_a?(Numeric) && jsonb_extraction?(expression) ? "(#{expression})::numeric" : expression
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def execute_query(sql, binds)
|
|
373
|
+
@db.exec_params(sql, binds).map { |row| instance_from_row(row) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Stamps `.version` (adapter bookkeeping, never domain state — see
|
|
377
|
+
# `Instance`'s own comment) from the row's `hecks_version` column on
|
|
378
|
+
# every Instance this adapter builds from a real stored row, so a
|
|
379
|
+
# later `save`'s optimistic-concurrency CAS has something to check
|
|
380
|
+
# against.
|
|
381
|
+
def instance_from_row(row)
|
|
382
|
+
instance = Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
383
|
+
instance.version = row["hecks_version"].to_i
|
|
384
|
+
instance
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# ── the rest of the dialect ─────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
|
|
390
|
+
def quoted_table = quote_ident(table)
|
|
391
|
+
def entry_table = "#{table}_entries"
|
|
392
|
+
def quoted_entry_table = quote_ident(entry_table)
|
|
393
|
+
|
|
394
|
+
def jsonb_extraction?(expression) = expression.include?("#>>")
|
|
395
|
+
|
|
396
|
+
def order_expression(field)
|
|
397
|
+
expression = query_expression(field)
|
|
398
|
+
jsonb_extraction?(expression) && numeric_field?(field) ? "(#{expression})::numeric" : expression
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Postgres defaults to NULLS LAST on ASC — same override
|
|
402
|
+
# PostgresEra's own order_clause carries, so a declared query
|
|
403
|
+
# answers identically no matter which adapter serves it (the
|
|
404
|
+
# port's in-memory semantics, which Sqlite's own default happens
|
|
405
|
+
# to match, put null rows FIRST ascending and LAST descending).
|
|
406
|
+
def order_clause(order_by, policy)
|
|
407
|
+
direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
|
|
408
|
+
nulls = case policy&.mode.to_s
|
|
409
|
+
when "first" then " NULLS FIRST"
|
|
410
|
+
when "last" then " NULLS LAST"
|
|
411
|
+
else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
|
|
412
|
+
end
|
|
413
|
+
"#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Same walk PostgresEra's own numeric_field? uses — decides
|
|
417
|
+
# numericness at ANY depth from the declared shape itself, not a
|
|
418
|
+
# runtime value.
|
|
419
|
+
def numeric_field?(field)
|
|
420
|
+
name, *path = field.to_s.split(".")
|
|
421
|
+
QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
|
|
422
|
+
@aggregate.value_object(type)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# ARRAY[...] of individually-escaped literals — same escaping
|
|
427
|
+
# PostgresEra's own jsonb_path carries and the same reason: a
|
|
428
|
+
# hand-rolled '{a,b,c}' array literal has no escaping at all, and
|
|
429
|
+
# a segment is a field or value-object member name this method has
|
|
430
|
+
# no way to know is always schema-declared.
|
|
431
|
+
def jsonb_path(column, segments)
|
|
432
|
+
"#{quote_ident(column)} #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ADR 0033 — the `PostgresEra` Ruby CLASS moved into the loadable era
|
|
2
|
+
# persistence plugin (`lib/hecks/ports/persistence/plugins/era/`); this
|
|
3
|
+
# declaration stays here, unmoved. `Adapters::Folder#load_library` globs
|
|
4
|
+
# `.adapter` files by DIRECTORY, not by require path, on every boot,
|
|
5
|
+
# regardless of which adapters a given domain actually binds — the same
|
|
6
|
+
# reason every other adapter's own `.adapter` file lives beside this one.
|
|
7
|
+
# Pure metadata (which port, which fields) — no era-specific code, and
|
|
8
|
+
# harmless for a domain that never binds `persisted_by "PostgresEra"`.
|
|
9
|
+
# `registry.adapter_class("PostgresEra")` is only ever resolved for a
|
|
10
|
+
# domain that actually binds to it, at which point the plugin must
|
|
11
|
+
# already be `require`d for the class to exist.
|
|
12
|
+
Hecks.adapter "PostgresEra" do
|
|
13
|
+
port "persistence"
|
|
14
|
+
field :database
|
|
15
|
+
field :role
|
|
16
|
+
field :schema
|
|
17
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "prism"
|
|
2
|
+
require_relative "../../bluebook/expression/canonical_form"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class NotExtractable < StandardError; end
|
|
7
|
+
|
|
8
|
+
module Prism
|
|
9
|
+
TREES = {}
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def canonical(block)
|
|
14
|
+
body = body_source(block)
|
|
15
|
+
body && canonicalise(body)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def body_source(block)
|
|
19
|
+
file, line = block.source_location
|
|
20
|
+
return nil unless file && File.readable?(file)
|
|
21
|
+
|
|
22
|
+
node = block_node_at(file, line)
|
|
23
|
+
node&.body&.slice
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def block_node_at(file, line)
|
|
27
|
+
found = nil
|
|
28
|
+
walk(tree_for(file)) do |node|
|
|
29
|
+
next unless node.is_a?(::Prism::BlockNode)
|
|
30
|
+
next unless node.location.start_line == line
|
|
31
|
+
|
|
32
|
+
found ||= node
|
|
33
|
+
end
|
|
34
|
+
found
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tree_for(file)
|
|
38
|
+
# ::Prism.parse_file(file) reads the file itself, at the C
|
|
39
|
+
# extension level — bypassing Ruby's own File/IO layer entirely.
|
|
40
|
+
# That's invisible to anything that virtualizes the filesystem at
|
|
41
|
+
# the Ruby level instead of the OS level (e.g. tebako's memfs,
|
|
42
|
+
# used to press a hecks-based app into a single executable —
|
|
43
|
+
# see domain/README.md's "Deploying" section in lifeadelics for
|
|
44
|
+
# why that matters). ::Prism.parse(File.read(file)) parses the
|
|
45
|
+
# exact same bytes, just read through Ruby's File.read first,
|
|
46
|
+
# which those tools do intercept.
|
|
47
|
+
TREES[file] ||= ::Prism.parse(File.read(file)).value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `TREES` caches for the life of the PROCESS, keyed by path, with
|
|
51
|
+
# no staleness check — correct for every ordinary caller (a file
|
|
52
|
+
# loads once per process: one `bin/ir` run, one rspec worker,
|
|
53
|
+
# never edited out from under it), but WRONG for anything that
|
|
54
|
+
# legitimately reloads an edited file in-process: a stale cached
|
|
55
|
+
# tree reports a `given`/`ensures` block at its OLD line number,
|
|
56
|
+
# which no longer matches the freshly re-executed file's own
|
|
57
|
+
# `block.source_location` — surfacing as "did not survive
|
|
58
|
+
# extraction" on a perfectly valid file. Found for real building
|
|
59
|
+
# `Hecks::Codemod` (lib/hecks/codemod.rb), which used to
|
|
60
|
+
# reach into `TREES.clear` directly — a private implementation
|
|
61
|
+
# detail poked from outside. `forget`/`forget_all` are the real
|
|
62
|
+
# API so nothing else that reloads an edited file in-process has
|
|
63
|
+
# to know `TREES` exists at all.
|
|
64
|
+
def forget(file) = TREES.delete(file)
|
|
65
|
+
|
|
66
|
+
def forget_all = TREES.clear
|
|
67
|
+
|
|
68
|
+
def walk(node, &visit)
|
|
69
|
+
return unless node.is_a?(::Prism::Node)
|
|
70
|
+
|
|
71
|
+
visit.call(node)
|
|
72
|
+
node.compact_child_nodes.each { |child| walk(child, &visit) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def canonicalise(source)
|
|
76
|
+
Bluebook::Expression::CanonicalForm.apply(source)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# The real `identity_generation` fulfillment — an actual random
|
|
6
|
+
# UUID, for an actual deployment. `SequentialIdentity` is the
|
|
7
|
+
# deterministic sibling specs/fuzzing bind instead.
|
|
8
|
+
module SecureRandomIdentity
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def uuid = SecureRandom.uuid
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|