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,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
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
require_relative "../../forms/value_object_shape"
|
|
2
|
+
require_relative "../../query_specification/common/null_policy"
|
|
3
|
+
require_relative "../../query_specification/field_path"
|
|
4
|
+
require_relative "../../runtime/errors"
|
|
5
|
+
require_relative "../../runtime/value"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The one SQL query compilation, shared by the SQLite and Postgres
|
|
10
|
+
# adapters — each used to carry its own copy of this walk, near-line-
|
|
11
|
+
# identical, and the copies could only drift. Every declared operator
|
|
12
|
+
# compiles fully into SQL or the query refuses loudly; the null-policy
|
|
13
|
+
# predicate, the comma-separated `in` convention, and the identity
|
|
14
|
+
# ORDER BY fallback are spelled once, here.
|
|
15
|
+
#
|
|
16
|
+
# What a dialect actually differs on is narrow, and each adapter
|
|
17
|
+
# supplies exactly those hooks:
|
|
18
|
+
#
|
|
19
|
+
# placeholder(binds, value) — "?" or "$N", recording the bind
|
|
20
|
+
# contains_clause(expr, ph) — instr() or position(), for a SCALAR field
|
|
21
|
+
# list_contains_clause(name, member, ph) — real element containment,
|
|
22
|
+
# for a `list_of` field (json_each /
|
|
23
|
+
# jsonb_array_elements)
|
|
24
|
+
# empty_in_clause — "0" or "FALSE"
|
|
25
|
+
# comparable_expression(e, v) — ::numeric cast, where the dialect casts
|
|
26
|
+
# plain_column(name) — a real column or a jsonb path
|
|
27
|
+
# nested_expression(name, path, member) — json_extract or state #>>
|
|
28
|
+
# order_clause(order_by, policy)
|
|
29
|
+
# execute_query(sql, binds) — run it, return instances
|
|
30
|
+
# dialect_name — for the refusal message
|
|
31
|
+
module SqlQueryBuilder
|
|
32
|
+
COMPARATORS = {
|
|
33
|
+
"eq" => "=", "ne" => "<>", "gt" => ">", "gte" => ">=", "lt" => "<", "lte" => "<="
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def query(declared, args = {}, context: {})
|
|
37
|
+
sql = +"SELECT #{select_list} FROM #{from_relation}"
|
|
38
|
+
clauses = []
|
|
39
|
+
binds = []
|
|
40
|
+
declared.wheres.each do |clause|
|
|
41
|
+
value = query_value(clause.value, args)
|
|
42
|
+
expression = query_expression(clause.field, value: value)
|
|
43
|
+
if (null_predicate = QuerySpecification::Common::NullPolicy.sql_predicate(expression, clause.op, value))
|
|
44
|
+
clauses << null_predicate.first
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
|
|
48
|
+
end
|
|
49
|
+
sql << " WHERE #{clauses.join(' AND ')}" unless clauses.empty?
|
|
50
|
+
sql << if declared.order_by
|
|
51
|
+
" ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
|
|
52
|
+
else
|
|
53
|
+
" ORDER BY id"
|
|
54
|
+
end
|
|
55
|
+
sql << " LIMIT #{placeholder(binds, query_value(declared.limit.value, args).to_i)}" if declared.limit
|
|
56
|
+
# An offset with no declared limit — SQLite refuses a bare OFFSET
|
|
57
|
+
# outright (LIMIT -1 is its unbounded idiom) while Postgres accepts
|
|
58
|
+
# one, so the dialect supplies its own unbounded spelling. Found by
|
|
59
|
+
# the adapter-agreement gate on its first run, not by either
|
|
60
|
+
# adapter's own spec: each was self-consistent, they just disagreed.
|
|
61
|
+
sql << unbounded_limit if !declared.limit && declared.offset
|
|
62
|
+
sql << " OFFSET #{placeholder(binds, query_value(declared.offset.value, args).to_i)}" if declared.offset
|
|
63
|
+
execute_query(sql, binds)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def where_clause(op, expression, value, binds, field: nil)
|
|
69
|
+
case op
|
|
70
|
+
when "eq", "ne", "gt", "gte", "lt", "lte"
|
|
71
|
+
"#{comparable_expression(expression, value)} #{COMPARATORS.fetch(op)} #{placeholder(binds, value)}"
|
|
72
|
+
when "contains"
|
|
73
|
+
member = field && list_member(field)
|
|
74
|
+
if member
|
|
75
|
+
list_contains_clause(field.to_s, member, placeholder(binds, value.to_s))
|
|
76
|
+
else
|
|
77
|
+
contains_clause(expression, placeholder(binds, value.to_s))
|
|
78
|
+
end
|
|
79
|
+
when "in"
|
|
80
|
+
members = in_members(value)
|
|
81
|
+
return empty_in_clause if members.empty?
|
|
82
|
+
|
|
83
|
+
# BOTH SIDES AS TEXT. `in` is a textual reading everywhere else
|
|
84
|
+
# — Ports::Query::InMemory#holds? compares `held.to_s` against
|
|
85
|
+
# stringified members, and `in_members` above stringifies its
|
|
86
|
+
# own — so a numeric field was the one shape where the engines
|
|
87
|
+
# could not agree: `in_members` binds '100', while the column
|
|
88
|
+
# expression yields the number 100, and `100 IN ('100','300')`
|
|
89
|
+
# is false in SQLite (a json_extract result carries no column
|
|
90
|
+
# affinity to coerce it) where Memory matched. Casting the
|
|
91
|
+
# expression rather than typing the members keeps one rule for
|
|
92
|
+
# every field: no type inference, and `in` means the same thing
|
|
93
|
+
# on a String, an Integer, and a value-object member.
|
|
94
|
+
#
|
|
95
|
+
# `eq`/`gt`/`lt` never had this problem because they bind the
|
|
96
|
+
# value's OWN type and route through `comparable_expression`,
|
|
97
|
+
# which Postgres overrides to cast numerics.
|
|
98
|
+
"CAST(#{expression} AS TEXT) IN (#{members.map { |member| placeholder(binds, member) }.join(', ')})"
|
|
99
|
+
else
|
|
100
|
+
raise ArgumentError, "#{dialect_name} query adapter does not support #{op.inspect}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A REAL ARRAY ALREADY SAYS WHERE ITS MEMBERS END. Splitting one on
|
|
105
|
+
# commas re-reads a boundary it already drew — and an id is a
|
|
106
|
+
# domain value (Naming::IDENTITY_JOIN joins a composite identity's
|
|
107
|
+
# own parts, and the parts it joins are whatever an identity path
|
|
108
|
+
# pulled out of real data), so a name carrying a comma would
|
|
109
|
+
# silently become two members matching the wrong rows, or nothing.
|
|
110
|
+
# This branch used to call `value.to_s` unconditionally, which
|
|
111
|
+
# meant only a comma-joined STRING ever worked here, while
|
|
112
|
+
# Ports::Query::InMemory and QueryInterpreter's own `members`
|
|
113
|
+
# already handled a real Array — three implementations of `in`,
|
|
114
|
+
# two readings of it. Narrower than those two on one point,
|
|
115
|
+
# deliberately: they unwrap a value-object/Hash element to its
|
|
116
|
+
# own scalar first (`comparable`) before stringifying, a rule
|
|
117
|
+
# this doesn't reproduce — every real caller of `in` today, and
|
|
118
|
+
# the cross-aggregate hop this exists for, passes plain scalar
|
|
119
|
+
# ids, never a value-object element.
|
|
120
|
+
def in_members(value)
|
|
121
|
+
return value.map(&:to_s).reject(&:empty?) if value.is_a?(Array)
|
|
122
|
+
|
|
123
|
+
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Every declared field compiles into an expression over the stored
|
|
127
|
+
# shape — or the query never runs. The member-picking rules are the
|
|
128
|
+
# same in both dialects: a value-object field compares through the
|
|
129
|
+
# member the bound value (or the declared types) say is numeric,
|
|
130
|
+
# falling back to the one-field convention `value`.
|
|
131
|
+
#
|
|
132
|
+
# A REFERENCE IS AN ID, stored as a bare scalar (never wrapped —
|
|
133
|
+
# `Runtime::Value.refuse_object_reference` guarantees it), so it
|
|
134
|
+
# takes the SAME plain-column path as any other non-value-object
|
|
135
|
+
# attribute. Excluding it into the value-object member-picking logic
|
|
136
|
+
# compiles a nested read against a row that has no nested key — a
|
|
137
|
+
# path that can never match. Measured, not assumed: a `where` on a
|
|
138
|
+
# has_one/belongs_to/reference_to field returned zero rows against
|
|
139
|
+
# real data until that exclusion was removed.
|
|
140
|
+
def query_expression(field, value: nil)
|
|
141
|
+
name, *path = field.to_s.split(".")
|
|
142
|
+
attribute = @aggregate.attribute(name)
|
|
143
|
+
|
|
144
|
+
return plain_column(name) if path.empty? && @aggregate.lifecycle&.field.to_s == name
|
|
145
|
+
return plain_column(name) if path.empty? && attribute && !value_object?(attribute)
|
|
146
|
+
|
|
147
|
+
member = if path.empty? && value
|
|
148
|
+
hash = value.is_a?(Runtime::Value) ? value.to_h : value
|
|
149
|
+
numeric = hash.is_a?(Hash) && hash.find { |_key, item| item.is_a?(Numeric) }
|
|
150
|
+
numeric ? numeric.first : nil
|
|
151
|
+
end
|
|
152
|
+
member ||= if path.empty? && attribute && value_object?(attribute)
|
|
153
|
+
object = @aggregate.value_object(attribute.type)
|
|
154
|
+
Forms::ValueObjectShape.numeric_member(object)&.name
|
|
155
|
+
end
|
|
156
|
+
nested_expression(name, path, member)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A NUMERIC MEMBER WINS if the value object has one (Price, Money —
|
|
160
|
+
# what every ordered comparison in the corpus until now compared),
|
|
161
|
+
# otherwise the SAME single-field fallback `query_expression`'s own
|
|
162
|
+
# `nested_expression(name, path, member || "value")` already
|
|
163
|
+
# makes for the COLUMN side — a plain `attribute :value, String`
|
|
164
|
+
# value object (IdentityId, RoleName, ...) has no numeric member at
|
|
165
|
+
# all, and returning nil there silently turned an equality
|
|
166
|
+
# comparison into `IS NULL`, matching nothing. Both sides of one
|
|
167
|
+
# comparison have to agree on which field they mean, or they
|
|
168
|
+
# compare two different things and call it a where clause.
|
|
169
|
+
def query_value(value, args)
|
|
170
|
+
value = args[value] if value.is_a?(Symbol)
|
|
171
|
+
hash = value.is_a?(Runtime::Value) ? value.to_h : value
|
|
172
|
+
if hash.is_a?(Hash)
|
|
173
|
+
numeric = hash.find { |_key, item| item.is_a?(Numeric) }
|
|
174
|
+
return numeric.last if numeric
|
|
175
|
+
return hash[:value] if hash.key?(:value)
|
|
176
|
+
return hash.values.first if hash.size == 1
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
Runtime::Value.scalar(value)
|
|
180
|
+
rescue Runtime::TypeMismatch
|
|
181
|
+
value.is_a?(Hash) && value.size == 1 ? value.values.first : value
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def value_object?(attr)
|
|
185
|
+
!attr.list? && !@aggregate.value_object(attr.type).nil?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# `contains` on a `list_of` field means real ELEMENT membership, not
|
|
189
|
+
# a substring search over the column's raw JSON text — the reading
|
|
190
|
+
# QueryInterpreter#holds? and Ports::Query::InMemory already give a
|
|
191
|
+
# real Array, and the SQL side used to disagree with it (a substring
|
|
192
|
+
# match over `[{"value":"not_high_risk"}]` falsely matches
|
|
193
|
+
# "high_risk"). Returns nil for a non-list field (the caller falls
|
|
194
|
+
# back to `contains_clause`, unchanged), "" for a list of bare
|
|
195
|
+
# scalars (no member to walk into), or the one field name a
|
|
196
|
+
# single-field value-object element carries. A list of a
|
|
197
|
+
# MULTI-field value object has no one scalar to compare against and
|
|
198
|
+
# is refused rather than guessed.
|
|
199
|
+
def list_member(field)
|
|
200
|
+
return nil if field.to_s.include?(".")
|
|
201
|
+
|
|
202
|
+
attribute = @aggregate.attribute(field.to_s)
|
|
203
|
+
return nil unless attribute&.list?
|
|
204
|
+
|
|
205
|
+
object = @aggregate.value_object(attribute.type)
|
|
206
|
+
return "" unless object
|
|
207
|
+
return object.attributes.first.name.to_s if object.attributes.size == 1
|
|
208
|
+
|
|
209
|
+
raise ArgumentError,
|
|
210
|
+
"#{dialect_name} query adapter cannot compile contains on #{field} — " \
|
|
211
|
+
"#{attribute.type} carries more than one field, so no single scalar to compare"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The dialect that casts nothing overrides nothing.
|
|
215
|
+
def comparable_expression(expression, _value) = expression
|
|
216
|
+
|
|
217
|
+
# The dialect that accepts a bare OFFSET overrides nothing.
|
|
218
|
+
def unbounded_limit = ""
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative "../../../runtime/value"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class Sqlite
|
|
6
|
+
# How a state crosses the column boundary: which fields persist,
|
|
7
|
+
# and how each one encodes into (and decodes out of) its column.
|
|
8
|
+
module Codec
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def persisted_fields
|
|
12
|
+
fields = @aggregate.attributes.reject { |attribute| attribute.name == :id }.map do |attribute|
|
|
13
|
+
{ name: attribute.name, attribute: attribute, sql_type: sql_type(attribute) }
|
|
14
|
+
end
|
|
15
|
+
lifecycle = @aggregate.lifecycle
|
|
16
|
+
fields << { name: lifecycle.field, attribute: nil, sql_type: "TEXT" } if lifecycle && !fields.any? { |field| field[:name] == lifecycle.field }
|
|
17
|
+
fields
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def encode(attr, value)
|
|
21
|
+
# NEVER SET IS NOT EMPTY. `then_set ... append:` starts a list at `[]`
|
|
22
|
+
# the moment the first element lands, but a list attribute nothing has
|
|
23
|
+
# ever appended to — the shape every OTHER aggregate in the corpus
|
|
24
|
+
# happened not to carry, until one declared a list-typed head
|
|
25
|
+
# attribute that no creating command populates by default — has to
|
|
26
|
+
# stay NULL to answer the same as Memory does. Forcing it to `[]`
|
|
27
|
+
# here is how a persistence topology invented data no dispatch wrote.
|
|
28
|
+
return (value.nil? ? nil : JSON.generate(value)) if attr.list?
|
|
29
|
+
return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def encode_field(field, value)
|
|
35
|
+
return value unless field[:attribute]
|
|
36
|
+
|
|
37
|
+
encode(field[:attribute], value)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def decode(row)
|
|
41
|
+
persisted_fields.each_with_object({}) do |field, state|
|
|
42
|
+
attr = field[:attribute]
|
|
43
|
+
unless attr
|
|
44
|
+
state[field[:name]] = row[field[:name].to_s]
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
raw = row[attr.name.to_s]
|
|
48
|
+
state[attr.name] =
|
|
49
|
+
if attr.list? || value_object?(attr)
|
|
50
|
+
raw ? JSON.parse(raw, symbolize_names: true) : nil
|
|
51
|
+
else
|
|
52
|
+
# A REFERENCE lands here now, with the ordinary scalars. It holds
|
|
53
|
+
# the id of a head, which is text — `JSON.parse("acct-1")` raises,
|
|
54
|
+
# so reading it as JSON was only ever survivable while the column
|
|
55
|
+
# held an object.
|
|
56
|
+
raw
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative "../../../runtime/instance"
|
|
2
|
+
require_relative "../../../runtime/value"
|
|
3
|
+
require_relative "../../../ports/query/in_memory"
|
|
4
|
+
|
|
5
|
+
# The subclass needs its parent — and sqlite.rb requires this file at
|
|
6
|
+
# its BOTTOM, after class Sqlite is defined, so the require cycle this
|
|
7
|
+
# creates resolves correctly from either entry point.
|
|
8
|
+
require_relative "../sqlite"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Adapters
|
|
12
|
+
# Rebuilds a read store from the authoritative journal, entry by entry.
|
|
13
|
+
# A value object is written as its JSON object and a reference as the
|
|
14
|
+
# bare id it holds — the same shapes the command path writes, so there
|
|
15
|
+
# is no second representation to accept here any more.
|
|
16
|
+
class SqliteProjection < Sqlite
|
|
17
|
+
def project(entry)
|
|
18
|
+
return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
|
|
19
|
+
|
|
20
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
|
|
21
|
+
values = [entry.id.to_s] + persisted_fields.map { |field| encode_field(field, entry.state[field[:name]]) }
|
|
22
|
+
@db.execute(
|
|
23
|
+
"INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{Array.new(columns.size,
|
|
24
|
+
'?').join(', ')})", values
|
|
25
|
+
)
|
|
26
|
+
entry
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Execute a declared read model against the projected aggregate-head
|
|
30
|
+
# tables. The report shape is assembled from SQL-selected rows, rather
|
|
31
|
+
# than scanning repositories and matching references in Ruby.
|
|
32
|
+
def query_read_model(_domain, model, args, bluebook = nil)
|
|
33
|
+
raise ArgumentError, "projection query needs its domain bluebook" unless bluebook
|
|
34
|
+
|
|
35
|
+
# The REFERENCE's own shape, read the same way ReadModelInterpreter
|
|
36
|
+
# reads it — not the identity unwrap, which is gone : an identity is
|
|
37
|
+
# declared as a path and followed.
|
|
38
|
+
reference_id = args.fetch(model.reference_name).to_s
|
|
39
|
+
eligible = model.filtered_head_name
|
|
40
|
+
reports = {}
|
|
41
|
+
model.aggregate_heads.each do |head|
|
|
42
|
+
aggregate = bluebook.aggregate(head[:aggregate])
|
|
43
|
+
rows = if head[:aggregate] == model.reference_target
|
|
44
|
+
[select_projected(aggregate, reference_id)].compact
|
|
45
|
+
else
|
|
46
|
+
select_related(aggregate, model.reference_target, reference_id)
|
|
47
|
+
end
|
|
48
|
+
rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible
|
|
49
|
+
reports[head[:as]] = if head[:many]
|
|
50
|
+
rows.map { |row| Runtime::Value.materialize(row.to_h) }
|
|
51
|
+
else
|
|
52
|
+
rows.first && Runtime::Value.materialize(rows.first.to_h)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
[reports]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def select_projected(aggregate, id)
|
|
61
|
+
row = @db.get_first_row("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE id = ?", [id.to_s])
|
|
62
|
+
row && projected_instance(aggregate, row)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def select_related(aggregate, target, id)
|
|
66
|
+
references = aggregate.attributes.select do |attribute|
|
|
67
|
+
attribute.reference? && attribute.type.target_name == target.to_s
|
|
68
|
+
end
|
|
69
|
+
return [] if references.empty?
|
|
70
|
+
|
|
71
|
+
# A REFERENCE COLUMN HOLDS THE ID, so it compares as itself. The
|
|
72
|
+
# `json_extract(col,'$.value') = ? OR col = ?` this replaced was
|
|
73
|
+
# reading both shapes because both existed — one written by the
|
|
74
|
+
# command path, one by older journals. There is one shape now.
|
|
75
|
+
clauses = references.map { |attribute| "#{quote_ident(attribute.name)} = ?" }
|
|
76
|
+
bind = references.map { id.to_s }
|
|
77
|
+
@db.execute("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE #{clauses.join(' OR ')} ORDER BY id", bind)
|
|
78
|
+
.map { |row| projected_instance(aggregate, row) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def projected_instance(aggregate, row)
|
|
82
|
+
Runtime::Instance.new(aggregate: aggregate, id: row["id"], state: decode_for(aggregate, row))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def decode_for(aggregate, row)
|
|
86
|
+
fields = aggregate.attributes.map { |attribute| [attribute.name, attribute] }
|
|
87
|
+
if (lifecycle = aggregate.lifecycle) && !fields.any? { |name, _| name == lifecycle.field }
|
|
88
|
+
fields << [lifecycle.field, nil]
|
|
89
|
+
end
|
|
90
|
+
fields.each_with_object({}) do |(name, attribute), state|
|
|
91
|
+
raw = row[name.to_s]
|
|
92
|
+
state[name] =
|
|
93
|
+
if attribute.nil?
|
|
94
|
+
raw
|
|
95
|
+
elsif attribute.list? || !aggregate.value_object(attribute.type).nil?
|
|
96
|
+
raw ? JSON.parse(raw, symbolize_names: true) : nil
|
|
97
|
+
else
|
|
98
|
+
# A reference is a scalar id — see Codec#decode.
|
|
99
|
+
raw
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Adapters
|
|
3
|
+
class Sqlite
|
|
4
|
+
# The DDL: one table per aggregate head, an append-only entry table
|
|
5
|
+
# beside it, the shared events table, and the two ALTERs that let an
|
|
6
|
+
# older database grow the columns newer code writes.
|
|
7
|
+
module SchemaBuilder
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def create_aggregate_table!
|
|
11
|
+
columns = persisted_fields.map { |field| "#{quote_ident(field[:name])} #{field[:sql_type]}" }
|
|
12
|
+
@db.execute(
|
|
13
|
+
"CREATE TABLE IF NOT EXISTS #{quoted_table} (id TEXT PRIMARY KEY#{columns.empty? ? '' : ', '}#{columns.join(', ')})"
|
|
14
|
+
)
|
|
15
|
+
# RIGHT HERE, NOT AS A SEPARATE STEP IN `Sqlite#initialize` —
|
|
16
|
+
# `create_aggregate_table!` is the one piece of DDL `D1`
|
|
17
|
+
# already calls verbatim through this same shared module
|
|
18
|
+
# (`d1.rb`'s own header: "reuses Sqlite::SchemaBuilder and
|
|
19
|
+
# Sqlite::Codec UNCHANGED"). Folding index creation in here,
|
|
20
|
+
# right after the table it indexes exists, is what makes D1's
|
|
21
|
+
# own automatic indexing a real, running thing rather than an
|
|
22
|
+
# aspiration that only Sqlite ever calls — no D1-specific
|
|
23
|
+
# wiring needed, because D1 never has to know this exists.
|
|
24
|
+
ensure_indexes!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_event_table!
|
|
28
|
+
@db.execute(<<~SQL)
|
|
29
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
name TEXT NOT NULL,
|
|
32
|
+
aggregate TEXT NOT NULL,
|
|
33
|
+
aggregate_id TEXT NOT NULL,
|
|
34
|
+
payload TEXT,
|
|
35
|
+
occurred_at TEXT
|
|
36
|
+
)
|
|
37
|
+
SQL
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_entry_table!
|
|
41
|
+
@db.execute(<<~SQL)
|
|
42
|
+
CREATE TABLE IF NOT EXISTS #{quoted_entry_table} (
|
|
43
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
aggregate_id TEXT NOT NULL,
|
|
45
|
+
operation TEXT NOT NULL DEFAULT 'save',
|
|
46
|
+
state TEXT NOT NULL,
|
|
47
|
+
mirrors TEXT
|
|
48
|
+
)
|
|
49
|
+
SQL
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ensure_entry_operation_column!
|
|
53
|
+
columns = @db.execute("PRAGMA table_info(#{quoted_entry_table})").map { |row| row["name"] }
|
|
54
|
+
return if columns.include?("operation")
|
|
55
|
+
|
|
56
|
+
@db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN operation TEXT NOT NULL DEFAULT 'save'")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ensure_entry_mirrors_column!
|
|
60
|
+
columns = @db.execute("PRAGMA table_info(#{quoted_entry_table})").map { |row| row["name"] }
|
|
61
|
+
return if columns.include?("mirrors")
|
|
62
|
+
|
|
63
|
+
@db.execute("ALTER TABLE #{quoted_entry_table} ADD COLUMN mirrors TEXT")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# THE OPTIONAL saga-persistence capability's own table (§2/§4) —
|
|
67
|
+
# shared here so `D1`, which `include`s this module verbatim for
|
|
68
|
+
# its own `events`-table DDL (`d1.rb`), gets this for free too.
|
|
69
|
+
# `domain` stays an explicit column even though SQLite has no
|
|
70
|
+
# schema/namespace concept the way Postgres does — matches
|
|
71
|
+
# `hecks_saga_instances`' own Postgres shape (§3) and covers the
|
|
72
|
+
# (uncommon but real) case of a domain explicitly sharing one
|
|
73
|
+
# `database` file/D1 database with another.
|
|
74
|
+
def create_saga_table!
|
|
75
|
+
@db.execute(<<~SQL)
|
|
76
|
+
CREATE TABLE IF NOT EXISTS hecks_saga_instances (
|
|
77
|
+
domain TEXT NOT NULL,
|
|
78
|
+
process_manager TEXT NOT NULL,
|
|
79
|
+
correlation TEXT NOT NULL,
|
|
80
|
+
state TEXT NOT NULL,
|
|
81
|
+
memory TEXT NOT NULL,
|
|
82
|
+
PRIMARY KEY (domain, process_manager, correlation)
|
|
83
|
+
)
|
|
84
|
+
SQL
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sql_type(attr)
|
|
88
|
+
return "TEXT" if attr.list?
|
|
89
|
+
|
|
90
|
+
SQL_TYPES.fetch(attr.type, "TEXT")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ── automatic indexing (plan principle 3: derived from existing
|
|
94
|
+
# declared `where`/`order_by` IR, no new DSL keyword — this runs
|
|
95
|
+
# unconditionally on every boot, `CREATE INDEX IF NOT EXISTS`,
|
|
96
|
+
# the same self-healing idiom `postgres_era.rb`'s own
|
|
97
|
+
# `ensure_head_snapshot!`/`ensure_first_head!` already use) ────
|
|
98
|
+
|
|
99
|
+
# Every field a declared query ever filters or sorts on, across
|
|
100
|
+
# the aggregate's OWN queries and every entity's own queries —
|
|
101
|
+
# `query_surfaces` in `dsl/aggregate_builder.rb` walks the
|
|
102
|
+
# identical pair. AN ENTITY'S QUERY STILL RUNS AGAINST THIS
|
|
103
|
+
# TABLE: it compiles through `query_expression`, which is
|
|
104
|
+
# `@aggregate`-scoped, not entity-scoped, so an entity's
|
|
105
|
+
# declared `where`/`order_by` is indexed here too, same as any
|
|
106
|
+
# other declared query — not skipped as "some other table's
|
|
107
|
+
# concern."
|
|
108
|
+
def ensure_indexes!
|
|
109
|
+
declared_query_fields.each { |field| ensure_index_for_field!(field) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def declared_query_fields
|
|
113
|
+
queries = @aggregate.queries + @aggregate.entities.flat_map(&:queries)
|
|
114
|
+
queries.flat_map { |query| query.wheres.map(&:field) + [query.order_by&.field] }
|
|
115
|
+
.compact.map(&:to_s).uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# One field name resolves to exactly one of three outcomes:
|
|
119
|
+
#
|
|
120
|
+
# - a plain scalar attribute (or the lifecycle field itself)
|
|
121
|
+
# — a real btree index on the column, using `plain_column`'s
|
|
122
|
+
# own `quote_ident(name)` phrasing (reached by calling
|
|
123
|
+
# `query_expression` itself, not a second copy of its
|
|
124
|
+
# plain-vs-nested decision);
|
|
125
|
+
# - a non-list value-object attribute, referenced bare or
|
|
126
|
+
# through a member path (`field` or `field.member`) — an
|
|
127
|
+
# EXPRESSION index over `query_expression(field)`'s own
|
|
128
|
+
# `json_extract(...)` text. Reusing the query compiler's own
|
|
129
|
+
# expression, not re-deriving the string a second way, is
|
|
130
|
+
# the whole point: a textual mismatch between the index and
|
|
131
|
+
# what a real query compiles to is invisible to SQLite's
|
|
132
|
+
# planner, and two independent copies of this logic can only
|
|
133
|
+
# drift apart over time;
|
|
134
|
+
# - a list-typed attribute — NO index. SQLite's `contains`
|
|
135
|
+
# compiles to `EXISTS (SELECT 1 FROM json_each(col) WHERE
|
|
136
|
+
# ...)` (`list_contains_clause`, in `sql_query_builder.rb`)
|
|
137
|
+
# — an element-membership scan a plain index on the raw
|
|
138
|
+
# column (or even an expression index on it) does nothing to
|
|
139
|
+
# speed, since neither indexes the elements individually.
|
|
140
|
+
# SQLite has no inverted/array index short of FTS5/R-tree,
|
|
141
|
+
# and neither fits a `list_of` scalar or value-object field
|
|
142
|
+
# — well beyond this plan's scope. Left unindexed
|
|
143
|
+
# deliberately, not a silent gap.
|
|
144
|
+
#
|
|
145
|
+
# A field naming neither a real attribute nor the lifecycle
|
|
146
|
+
# field can't happen through the DSL today — `seal_query_field`
|
|
147
|
+
# (`dsl/aggregate_builder.rb`) already refuses it at PARSE time.
|
|
148
|
+
# This runs at adapter BOOT, not parse time, so it skips rather
|
|
149
|
+
# than crashes ugly if that invariant is ever violated.
|
|
150
|
+
def ensure_index_for_field!(field)
|
|
151
|
+
name, * = field.to_s.split(".")
|
|
152
|
+
attribute = @aggregate.attribute(name)
|
|
153
|
+
lifecycle_field = @aggregate.lifecycle&.field.to_s == name
|
|
154
|
+
|
|
155
|
+
return if !lifecycle_field && attribute.nil?
|
|
156
|
+
return if attribute&.list?
|
|
157
|
+
|
|
158
|
+
expression = query_expression(field)
|
|
159
|
+
@db.execute(
|
|
160
|
+
"CREATE INDEX IF NOT EXISTS #{quote_ident(index_name(field))} ON #{quoted_table}(#{expression})"
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `idx_<table>_<sanitized field>` — TABLE-PREFIXED because
|
|
165
|
+
# SQLite index names are GLOBAL to the database, not scoped per
|
|
166
|
+
# table the way a column name is; two different aggregates each
|
|
167
|
+
# indexing a field called "name" would collide without it. The
|
|
168
|
+
# field itself is sanitized (a dotted path's "." in particular)
|
|
169
|
+
# to stay a valid identifier while remaining visibly tied to
|
|
170
|
+
# what it indexes — collision-free across every field/path this
|
|
171
|
+
# aggregate declares, since `declared_query_fields` already
|
|
172
|
+
# de-duplicates the field strings themselves.
|
|
173
|
+
def index_name(field)
|
|
174
|
+
sanitized = field.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
175
|
+
"idx_#{table}_#{sanitized}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|