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,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,80 @@
|
|
|
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
|
+
# `projects` FIELDS (S12, ADR 0025) ARE A LOCAL COLUMN TOO —
|
|
18
|
+
# `CommandInterpreter#seed_projected_fields`/`RebuildSweep`
|
|
19
|
+
# both write one straight into `Instance#state` the same as
|
|
20
|
+
# any other field, so a column has to exist to hold it or
|
|
21
|
+
# `save`'s own `columns =`/`values =` build (this file, not
|
|
22
|
+
# here) silently drops it on every write. No `attribute` of
|
|
23
|
+
# their own to carry (`ProjectedField` is a bare name/
|
|
24
|
+
# reference/remote_field triple, not a typed attribute) —
|
|
25
|
+
# treated as a raw scalar, same as the lifecycle field just
|
|
26
|
+
# above: every real corpus use projects a status/lifecycle
|
|
27
|
+
# string, and `encode_field`/`decode` both already pass a
|
|
28
|
+
# `attribute: nil` field through untouched, not JSON-encoded.
|
|
29
|
+
@aggregate.projected_fields.each do |field|
|
|
30
|
+
next if fields.any? { |f| f[:name] == field.name }
|
|
31
|
+
|
|
32
|
+
fields << { name: field.name, attribute: nil, sql_type: "TEXT" }
|
|
33
|
+
end
|
|
34
|
+
fields
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def encode(attr, value)
|
|
38
|
+
# NEVER SET IS NOT EMPTY. `then_set ... append:` starts a list at `[]`
|
|
39
|
+
# the moment the first element lands, but a list attribute nothing has
|
|
40
|
+
# ever appended to — the shape every OTHER aggregate in the corpus
|
|
41
|
+
# happened not to carry, until one declared a list-typed head
|
|
42
|
+
# attribute that no creating command populates by default — has to
|
|
43
|
+
# stay NULL to answer the same as Memory does. Forcing it to `[]`
|
|
44
|
+
# here is how a persistence topology invented data no dispatch wrote.
|
|
45
|
+
return (value.nil? ? nil : JSON.generate(value)) if attr.list?
|
|
46
|
+
return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
|
|
47
|
+
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def encode_field(field, value)
|
|
52
|
+
return value unless field[:attribute]
|
|
53
|
+
|
|
54
|
+
encode(field[:attribute], value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decode(row)
|
|
58
|
+
persisted_fields.each_with_object({}) do |field, state|
|
|
59
|
+
attr = field[:attribute]
|
|
60
|
+
unless attr
|
|
61
|
+
state[field[:name]] = row[field[:name].to_s]
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
raw = row[attr.name.to_s]
|
|
65
|
+
state[attr.name] =
|
|
66
|
+
if attr.list? || value_object?(attr)
|
|
67
|
+
raw ? JSON.parse(raw, symbolize_names: true) : nil
|
|
68
|
+
else
|
|
69
|
+
# A REFERENCE lands here now, with the ordinary scalars. It holds
|
|
70
|
+
# the id of a head, which is text — `JSON.parse("acct-1")` raises,
|
|
71
|
+
# so reading it as JSON was only ever survivable while the column
|
|
72
|
+
# held an object.
|
|
73
|
+
raw
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
require_relative "../../../runtime/instance"
|
|
2
|
+
require_relative "../../../runtime/value"
|
|
3
|
+
require_relative "../../../runtime/errors"
|
|
4
|
+
require_relative "../../../runtime/refusal_wording"
|
|
5
|
+
require_relative "../../../rendering"
|
|
6
|
+
require_relative "../../../ports/query/in_memory"
|
|
7
|
+
|
|
8
|
+
# The subclass needs its parent — and sqlite.rb requires this file at
|
|
9
|
+
# its BOTTOM, after class Sqlite is defined, so the require cycle this
|
|
10
|
+
# creates resolves correctly from either entry point.
|
|
11
|
+
require_relative "../sqlite"
|
|
12
|
+
|
|
13
|
+
module Hecks
|
|
14
|
+
module Adapters
|
|
15
|
+
# Rebuilds a read store from the authoritative journal, entry by entry.
|
|
16
|
+
# A value object is written as its JSON object and a reference as the
|
|
17
|
+
# bare id it holds — the same shapes the command path writes, so there
|
|
18
|
+
# is no second representation to accept here any more.
|
|
19
|
+
class SqliteProjection < Sqlite
|
|
20
|
+
def project(entry)
|
|
21
|
+
return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
|
|
22
|
+
|
|
23
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
|
|
24
|
+
values = [entry.id.to_s] + persisted_fields.map { |field| encode_field(field, entry.state[field[:name]]) }
|
|
25
|
+
@db.execute(
|
|
26
|
+
"INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{Array.new(columns.size,
|
|
27
|
+
'?').join(', ')})", values
|
|
28
|
+
)
|
|
29
|
+
entry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Execute a declared read model against the projected aggregate-head
|
|
33
|
+
# tables. The report shape is assembled from SQL-selected rows, rather
|
|
34
|
+
# than scanning repositories and matching references in Ruby.
|
|
35
|
+
#
|
|
36
|
+
# M19 (docs/audits/2026-08-10-main-bug-audit.md,
|
|
37
|
+
# docs/audits/2026-08-11-bug-triage.md) — this used to diverge from
|
|
38
|
+
# `Runtime::ReadModelInterpreter#project` (the in-process path) on
|
|
39
|
+
# two counts, both fixed here to agree with it:
|
|
40
|
+
#
|
|
41
|
+
# MISSING ROOT: the in-process path's own `fetch` refuses with
|
|
42
|
+
# `NotFound` when the reference argument names no record —
|
|
43
|
+
# `query_read_model` used to answer a silent `{root: nil, ...}`
|
|
44
|
+
# instead, the one path a caller could dispatch a read model
|
|
45
|
+
# against a record that never existed and get back something that
|
|
46
|
+
# LOOKS like an empty report rather than the refusal every other
|
|
47
|
+
# path gives.
|
|
48
|
+
#
|
|
49
|
+
# CHAINED-INCLUDE JOIN SCOPE: a non-root head was always matched
|
|
50
|
+
# against the ROOT's own id, regardless of what it actually
|
|
51
|
+
# references — correct for a head that references the root
|
|
52
|
+
# directly, silently EMPTY for one that references another
|
|
53
|
+
# included head instead (`Leaf` -> `Mid` -> `Root`, `Leaf` itself
|
|
54
|
+
# has no attribute referencing `Root` at all, so `references`
|
|
55
|
+
# was always `[]`). The in-process path's own root-first fix
|
|
56
|
+
# (`ReadModelInterpreter#project`'s "ROOT FIRST, ALWAYS" comment)
|
|
57
|
+
# already matches a head against ANY already-projected source, not
|
|
58
|
+
# only the root — `select_related` now does the same: each head is
|
|
59
|
+
# matched against every source resolved so far (root first, then
|
|
60
|
+
# declared order — the same one-level-of-declaration-order
|
|
61
|
+
# dependency the in-process path itself still has, documented
|
|
62
|
+
# there as L2, not a gap introduced here).
|
|
63
|
+
def query_read_model(_domain, model, args, bluebook = nil)
|
|
64
|
+
raise ArgumentError, "projection query needs its domain bluebook" unless bluebook
|
|
65
|
+
|
|
66
|
+
# The REFERENCE's own shape, read the same way ReadModelInterpreter
|
|
67
|
+
# reads it — not the identity unwrap, which is gone : an identity is
|
|
68
|
+
# declared as a path and followed.
|
|
69
|
+
reference_id = args.fetch(model.reference_name).to_s
|
|
70
|
+
eligible = model.filtered_head_name
|
|
71
|
+
|
|
72
|
+
# ROOT FIRST, ALWAYS — see this method's own header. Mirrors
|
|
73
|
+
# `ReadModelInterpreter#project`'s identical partition, for the
|
|
74
|
+
# identical reason: a later head's own join has to be able to
|
|
75
|
+
# match against a root (or another head) already resolved.
|
|
76
|
+
root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target }
|
|
77
|
+
projected = []
|
|
78
|
+
reports = {}
|
|
79
|
+
(root_heads + other_heads).each do |head|
|
|
80
|
+
aggregate = bluebook.aggregate(head[:aggregate])
|
|
81
|
+
rows = if head[:aggregate] == model.reference_target
|
|
82
|
+
[select_projected(aggregate, reference_id) ||
|
|
83
|
+
raise(Runtime::NotFound,
|
|
84
|
+
Runtime::RefusalWording.render("NotFound", "read_model_reference_missing",
|
|
85
|
+
aggregate: head[:aggregate],
|
|
86
|
+
offered: Hecks::Rendering.describe(reference_id)))]
|
|
87
|
+
else
|
|
88
|
+
select_related(aggregate, projected)
|
|
89
|
+
end
|
|
90
|
+
rows = Ports::Query::InMemory.execute(rows, model, args) if head[:as] == eligible
|
|
91
|
+
projected << { aggregate: head[:aggregate], rows: rows }
|
|
92
|
+
reports[head[:as]] = if head[:many]
|
|
93
|
+
rows.map { |row| Runtime::Value.materialize(row.to_h) }
|
|
94
|
+
else
|
|
95
|
+
rows.first && Runtime::Value.materialize(rows.first.to_h)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
[reports]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def select_projected(aggregate, id)
|
|
104
|
+
row = @db.get_first_row("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE id = ?", [id.to_s])
|
|
105
|
+
row && projected_instance(aggregate, row)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Matched against EVERY source already projected (root first, then
|
|
109
|
+
# declared order — see this class's own `query_read_model` header),
|
|
110
|
+
# not only the root — a head whose own reference points at another
|
|
111
|
+
# included head rather than the root directly used to match nothing
|
|
112
|
+
# at all, since its reference attribute was compared against a
|
|
113
|
+
# target (the root) it never names.
|
|
114
|
+
def select_related(aggregate, projected)
|
|
115
|
+
matches = projected.flat_map do |source|
|
|
116
|
+
references = aggregate.attributes.select do |attribute|
|
|
117
|
+
attribute.reference? && attribute.type.target_name == source[:aggregate].to_s
|
|
118
|
+
end
|
|
119
|
+
next [] if references.empty?
|
|
120
|
+
|
|
121
|
+
ids = source[:rows].map { |row| row.id.to_s }
|
|
122
|
+
next [] if ids.empty?
|
|
123
|
+
|
|
124
|
+
references.product(ids)
|
|
125
|
+
end
|
|
126
|
+
return [] if matches.empty?
|
|
127
|
+
|
|
128
|
+
# A REFERENCE COLUMN HOLDS THE ID, so it compares as itself. The
|
|
129
|
+
# `json_extract(col,'$.value') = ? OR col = ?` this replaced was
|
|
130
|
+
# reading both shapes because both existed — one written by the
|
|
131
|
+
# command path, one by older journals. There is one shape now.
|
|
132
|
+
clauses = matches.map { |attribute, _id| "#{quote_ident(attribute.name)} = ?" }
|
|
133
|
+
bind = matches.map { |_attribute, id| id }
|
|
134
|
+
@db.execute("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE #{clauses.join(' OR ')} ORDER BY id", bind)
|
|
135
|
+
.map { |row| projected_instance(aggregate, row) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def projected_instance(aggregate, row)
|
|
139
|
+
Runtime::Instance.new(aggregate: aggregate, id: row["id"], state: decode_for(aggregate, row))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def decode_for(aggregate, row)
|
|
143
|
+
fields = aggregate.attributes.map { |attribute| [attribute.name, attribute] }
|
|
144
|
+
if (lifecycle = aggregate.lifecycle) && !fields.any? { |name, _| name == lifecycle.field }
|
|
145
|
+
fields << [lifecycle.field, nil]
|
|
146
|
+
end
|
|
147
|
+
# `projects` FIELDS (S12, ADR 0025) NEED READING BACK TOO — `project`
|
|
148
|
+
# (above) already writes one into its own column via `persisted_fields`
|
|
149
|
+
# (`Codec#persisted_fields`, this class's own superclass module), but
|
|
150
|
+
# this method built its own independent field list that never
|
|
151
|
+
# consulted it — a column `project` populated correctly, silently
|
|
152
|
+
# dropped on every read back out. Same raw-passthrough treatment as
|
|
153
|
+
# the lifecycle field just above: `attribute: nil` down in the loop.
|
|
154
|
+
aggregate.projected_fields.each do |field|
|
|
155
|
+
fields << [field.name, nil] unless fields.any? { |name, _| name == field.name }
|
|
156
|
+
end
|
|
157
|
+
fields.each_with_object({}) do |(name, attribute), state|
|
|
158
|
+
raw = row[name.to_s]
|
|
159
|
+
state[name] =
|
|
160
|
+
if attribute.nil?
|
|
161
|
+
raw
|
|
162
|
+
elsif attribute.list? || !aggregate.value_object(attribute.type).nil?
|
|
163
|
+
raw ? JSON.parse(raw, symbolize_names: true) : nil
|
|
164
|
+
else
|
|
165
|
+
# A reference is a scalar id — see Codec#decode.
|
|
166
|
+
raw
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
completed_compensations TEXT NOT NULL DEFAULT '[]',
|
|
83
|
+
PRIMARY KEY (domain, process_manager, correlation)
|
|
84
|
+
)
|
|
85
|
+
SQL
|
|
86
|
+
# `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
|
|
87
|
+
# this same domain already created before this column existed
|
|
88
|
+
# — the same reason Postgres's own `create_saga_table!` needs
|
|
89
|
+
# its own `ADD COLUMN IF NOT EXISTS`. SQLite/D1's own `ALTER
|
|
90
|
+
# TABLE ... ADD COLUMN` has no `IF NOT EXISTS` guard on every
|
|
91
|
+
# version this adapter supports, so a duplicate-column error
|
|
92
|
+
# is caught and treated as "already there" rather than relied
|
|
93
|
+
# on to never happen.
|
|
94
|
+
@db.execute("ALTER TABLE hecks_saga_instances ADD COLUMN completed_compensations TEXT NOT NULL DEFAULT '[]'")
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
raise unless e.message.include?("duplicate column name")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sql_type(attr)
|
|
100
|
+
return "TEXT" if attr.list?
|
|
101
|
+
|
|
102
|
+
SQL_TYPES.fetch(attr.type, "TEXT")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── automatic indexing (plan principle 3: derived from existing
|
|
106
|
+
# declared `where`/`order_by` IR, no new DSL keyword — this runs
|
|
107
|
+
# unconditionally on every boot, `CREATE INDEX IF NOT EXISTS`,
|
|
108
|
+
# the same self-healing idiom `postgres_era.rb`'s own
|
|
109
|
+
# `ensure_head_snapshot!`/`ensure_first_head!` already use) ────
|
|
110
|
+
|
|
111
|
+
# Every field a declared query ever filters or sorts on, across
|
|
112
|
+
# the aggregate's OWN queries and every entity's own queries —
|
|
113
|
+
# `query_surfaces` in `dsl/aggregate_builder.rb` walks the
|
|
114
|
+
# identical pair. AN ENTITY'S QUERY STILL RUNS AGAINST THIS
|
|
115
|
+
# TABLE: it compiles through `query_expression`, which is
|
|
116
|
+
# `@aggregate`-scoped, not entity-scoped, so an entity's
|
|
117
|
+
# declared `where`/`order_by` is indexed here too, same as any
|
|
118
|
+
# other declared query — not skipped as "some other table's
|
|
119
|
+
# concern."
|
|
120
|
+
def ensure_indexes!
|
|
121
|
+
declared_query_fields.each { |field| ensure_index_for_field!(field) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def declared_query_fields
|
|
125
|
+
queries = @aggregate.queries + @aggregate.entities.flat_map(&:queries)
|
|
126
|
+
queries.flat_map { |query| query.wheres.map(&:field) + [query.order_by&.field] }
|
|
127
|
+
.compact.map(&:to_s).uniq
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# One field name resolves to exactly one of three outcomes:
|
|
131
|
+
#
|
|
132
|
+
# - a plain scalar attribute (or the lifecycle field itself)
|
|
133
|
+
# — a real btree index on the column, using `plain_column`'s
|
|
134
|
+
# own `quote_ident(name)` phrasing (reached by calling
|
|
135
|
+
# `query_expression` itself, not a second copy of its
|
|
136
|
+
# plain-vs-nested decision);
|
|
137
|
+
# - a non-list value-object attribute, referenced bare or
|
|
138
|
+
# through a member path (`field` or `field.member`) — an
|
|
139
|
+
# EXPRESSION index over `query_expression(field)`'s own
|
|
140
|
+
# `json_extract(...)` text. Reusing the query compiler's own
|
|
141
|
+
# expression, not re-deriving the string a second way, is
|
|
142
|
+
# the whole point: a textual mismatch between the index and
|
|
143
|
+
# what a real query compiles to is invisible to SQLite's
|
|
144
|
+
# planner, and two independent copies of this logic can only
|
|
145
|
+
# drift apart over time;
|
|
146
|
+
# - a list-typed attribute — NO index. SQLite's `contains`
|
|
147
|
+
# compiles to `EXISTS (SELECT 1 FROM json_each(col) WHERE
|
|
148
|
+
# ...)` (`list_contains_clause`, in `sql_query_builder.rb`)
|
|
149
|
+
# — an element-membership scan a plain index on the raw
|
|
150
|
+
# column (or even an expression index on it) does nothing to
|
|
151
|
+
# speed, since neither indexes the elements individually.
|
|
152
|
+
# SQLite has no inverted/array index short of FTS5/R-tree,
|
|
153
|
+
# and neither fits a `list_of` scalar or value-object field
|
|
154
|
+
# — well beyond this plan's scope. Left unindexed
|
|
155
|
+
# deliberately, not a silent gap.
|
|
156
|
+
#
|
|
157
|
+
# A field naming neither a real attribute nor the lifecycle
|
|
158
|
+
# field can't happen through the DSL today — `seal_query_field`
|
|
159
|
+
# (`dsl/aggregate_builder.rb`) already refuses it at PARSE time.
|
|
160
|
+
# This runs at adapter BOOT, not parse time, so it skips rather
|
|
161
|
+
# than crashes ugly if that invariant is ever violated.
|
|
162
|
+
def ensure_index_for_field!(field)
|
|
163
|
+
name, * = field.to_s.split(".")
|
|
164
|
+
attribute = @aggregate.attribute(name)
|
|
165
|
+
lifecycle_field = @aggregate.lifecycle&.field.to_s == name
|
|
166
|
+
|
|
167
|
+
return if !lifecycle_field && attribute.nil?
|
|
168
|
+
return if attribute&.list?
|
|
169
|
+
|
|
170
|
+
expression = query_expression(field)
|
|
171
|
+
@db.execute(
|
|
172
|
+
"CREATE INDEX IF NOT EXISTS #{quote_ident(index_name(field))} ON #{quoted_table}(#{expression})"
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# `idx_<table>_<sanitized field>` — TABLE-PREFIXED because
|
|
177
|
+
# SQLite index names are GLOBAL to the database, not scoped per
|
|
178
|
+
# table the way a column name is; two different aggregates each
|
|
179
|
+
# indexing a field called "name" would collide without it. The
|
|
180
|
+
# field itself is sanitized (a dotted path's "." in particular)
|
|
181
|
+
# to stay a valid identifier while remaining visibly tied to
|
|
182
|
+
# what it indexes — collision-free across every field/path this
|
|
183
|
+
# aggregate declares, since `declared_query_fields` already
|
|
184
|
+
# de-duplicates the field strings themselves.
|
|
185
|
+
def index_name(field)
|
|
186
|
+
sanitized = field.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
187
|
+
"idx_#{table}_#{sanitized}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|