hecks 1.3.0 → 1.5.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 +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +61 -15
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
|
@@ -4,7 +4,7 @@ require_relative "../../runtime/value"
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module QuerySpecification
|
|
6
6
|
module Common
|
|
7
|
-
#
|
|
7
|
+
# **The one comparator table**. There were two, near-identical copies of
|
|
8
8
|
# this — `Ports::Query::InMemory` (the path a Memory- or Heki-backed
|
|
9
9
|
# aggregate query actually runs) and `Runtime::QueryInterpreter`
|
|
10
10
|
# (entity/sub-list queries, and any adapter implementing no `:query`)
|
|
@@ -15,23 +15,25 @@ module Hecks
|
|
|
15
15
|
# real bugs twice already, both recorded in the comments those files
|
|
16
16
|
# carried. `none_in_state` was added to one copy only, so an ordinary
|
|
17
17
|
# Memory query's `none_in_state` clause fell to the `else` branch and
|
|
18
|
-
# silently excluded every row. And `comparable`
|
|
19
|
-
# anyone noticing: one copy took the
|
|
20
|
-
# object, the other only unwrapped when there was
|
|
18
|
+
# silently excluded every row. And `comparable` diverged without
|
|
19
|
+
# anyone noticing: one copy took the first numeric member of a value
|
|
20
|
+
# object, the other only unwrapped when there was exactly one — so a
|
|
21
21
|
# value object with two numeric members compared as a number on one
|
|
22
22
|
# path and as a whole Hash on the other.
|
|
23
23
|
#
|
|
24
|
-
# The callers still differ in how they
|
|
24
|
+
# The callers still differ in how they reach a value — one takes a
|
|
25
25
|
# registry argument, the other closes over `@registry`; one digs the
|
|
26
26
|
# field through `FieldPath`, the other is handed it — so they keep
|
|
27
27
|
# their own resolution and share only the comparison itself.
|
|
28
28
|
module Comparison
|
|
29
29
|
module_function
|
|
30
30
|
|
|
31
|
+
# Unwraps a value object to the one scalar a comparison can mean.
|
|
32
|
+
#
|
|
31
33
|
# A value object compared as a scalar, when which scalar is meant
|
|
32
34
|
# is not in doubt. Exactly one numeric member is unambiguous; a
|
|
33
35
|
# single-member value object is unambiguous whatever its type.
|
|
34
|
-
# Anything else is returned
|
|
36
|
+
# Anything else is returned unchanged rather than guessed at — two
|
|
35
37
|
# numeric members give no reason to prefer either, and picking the
|
|
36
38
|
# first silently compares a field the author never named.
|
|
37
39
|
#
|
|
@@ -40,6 +42,11 @@ module Hecks
|
|
|
40
42
|
# so this branch is a backstop rather than the primary guard: which
|
|
41
43
|
# member is meant is knowable at declaration time, and a refusal
|
|
42
44
|
# naming the candidates is worth more than any runtime reading.
|
|
45
|
+
#
|
|
46
|
+
# @param value [Runtime::Value, Hash, Object, nil] a held or wanted value; a
|
|
47
|
+
# `Runtime::Value` is read through its `to_h`
|
|
48
|
+
# @return [Object, nil] the sole numeric member, or the sole member, of a Hash-shaped
|
|
49
|
+
# value; otherwise `value` unchanged (a `Runtime::Value` comes back as its Hash)
|
|
43
50
|
def comparable(value)
|
|
44
51
|
value = value.to_h if value.is_a?(Runtime::Value)
|
|
45
52
|
return value unless value.is_a?(Hash)
|
|
@@ -51,9 +58,15 @@ module Hecks
|
|
|
51
58
|
value
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
#
|
|
61
|
+
# Lists the members a value object offers a scalar comparison, for a
|
|
55
62
|
# refusal that can name them. Empty when the value object is
|
|
56
63
|
# unambiguous — nothing to report.
|
|
64
|
+
#
|
|
65
|
+
# @param value_object [Class<Bluebook::ValueObject>] the declared shape (a subclass
|
|
66
|
+
# minted by `Bluebook::ValueObject.declare`, closed sets included) a query field names
|
|
67
|
+
# @return [Array<Symbol>] every attribute name when no single member can be meant;
|
|
68
|
+
# `[]` when there is exactly one attribute, or exactly one typed `Integer`,
|
|
69
|
+
# `Float` or `Numeric`
|
|
57
70
|
def ambiguous_members(value_object)
|
|
58
71
|
numerics = value_object.attributes.select { |a| NUMERIC_TYPES.include?(a.type.to_s) }
|
|
59
72
|
return [] if numerics.size == 1 || value_object.attributes.size == 1
|
|
@@ -63,15 +76,32 @@ module Hecks
|
|
|
63
76
|
|
|
64
77
|
NUMERIC_TYPES = %w[Integer Float Numeric].freeze
|
|
65
78
|
|
|
66
|
-
#
|
|
79
|
+
# Decides whether one where-clause comparison holds between the value
|
|
80
|
+
# a record holds and the value the query wants.
|
|
81
|
+
#
|
|
82
|
+
# A `case` over a closed, declared set (`Vocabulary::QueryComparator`,
|
|
67
83
|
# held equal to this list by spec/vocabulary_table_spec — see the
|
|
68
|
-
# `else` branch's own comment) is the whole point of
|
|
69
|
-
#
|
|
84
|
+
# `else` branch's own comment) is the whole point of the one
|
|
85
|
+
# comparator table this file's header describes: one place naming
|
|
70
86
|
# every comparator, not one method per comparator scattered across
|
|
71
87
|
# a module.
|
|
88
|
+
#
|
|
89
|
+
# @param operation [Symbol, String] the comparator name: `eq`, `ne`, `lt`, `lte`,
|
|
90
|
+
# `gt`, `gte`, `in`, `contains` or `none_in_state`
|
|
91
|
+
# @param held [Object, nil] the record's own value for the field; `nil` against a
|
|
92
|
+
# non-nil `want` satisfies no comparator except `none_in_state` (see
|
|
93
|
+
# `NullPolicy.unmatchable?`)
|
|
94
|
+
# @param want [Object, nil] the value compared against; for `in` an Array or a
|
|
95
|
+
# comma-separated String, for `none_in_state` an `"Aggregate:state"` String
|
|
96
|
+
# @param registry [Runtime::Registry, nil] used only by `none_in_state` to look the
|
|
97
|
+
# target aggregate up; `nil` makes that comparator hold
|
|
98
|
+
# @return [Boolean] whether the comparison holds; an ordered comparator over a
|
|
99
|
+
# non-`Numeric` operand is `false`
|
|
100
|
+
# @raise [Runtime::WiringError] if `operation` names no comparator in this table, or
|
|
101
|
+
# `none_in_state`'s target aggregate has no repository that can be wired
|
|
72
102
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
103
|
def holds?(operation, held, want, registry: nil)
|
|
74
|
-
# A NULL
|
|
104
|
+
# A NULL satisfies no comparison — NullPolicy.unmatchable? owns
|
|
75
105
|
# the rule and the reasoning, including why `none_in_state` is
|
|
76
106
|
# exempt from it.
|
|
77
107
|
return false if NullPolicy.unmatchable?(operation, held, want)
|
|
@@ -100,28 +130,51 @@ module Hecks
|
|
|
100
130
|
end
|
|
101
131
|
end
|
|
102
132
|
|
|
133
|
+
# Checks that both operands of an ordered comparison are numbers.
|
|
134
|
+
#
|
|
103
135
|
# gt/gte/lt/lte are numeric-only and silently false otherwise — a
|
|
104
136
|
# where-clause never raises the way a given does, and that contract
|
|
105
137
|
# predates the extraction (lt was already exactly this permissive).
|
|
138
|
+
#
|
|
139
|
+
# @param held [Object, nil] the record's own value for the field
|
|
140
|
+
# @param want [Object, nil] the value compared against
|
|
141
|
+
# @return [Boolean] `true` only when both are `Numeric`
|
|
106
142
|
def ordered?(held, want) = held.is_a?(Numeric) && want.is_a?(Numeric)
|
|
107
143
|
|
|
144
|
+
# Reads a comparator's list operand as the Strings membership is tested against.
|
|
145
|
+
#
|
|
108
146
|
# `in` reads a comma-separated list — a real Array survives
|
|
109
147
|
# untouched (a bluebook's own in-process value, before any wire
|
|
110
148
|
# serialisation), each element unwrapped the same way a scalar
|
|
111
|
-
# field is. This is `in`'s reading of
|
|
149
|
+
# field is. This is `in`'s reading of its argument (a caller may
|
|
112
150
|
# legitimately pass "a,b,c" meaning "any of these") — unrelated to
|
|
113
|
-
# `contains`, which reads the
|
|
151
|
+
# `contains`, which reads the stored field. See `contains?`.
|
|
152
|
+
#
|
|
153
|
+
# @param value [Array, String, Object, nil] an Array of elements, or anything whose
|
|
154
|
+
# `to_s` is a comma-separated list such as `"a, b,c"`
|
|
155
|
+
# @return [Array<String>] one String per member: Array elements unwrapped through
|
|
156
|
+
# `comparable` then `to_s`; split parts stripped of surrounding whitespace; `[]`
|
|
157
|
+
# for `nil` or an empty String
|
|
114
158
|
def members(value)
|
|
115
159
|
return value.map { |element| comparable(element).to_s } if value.is_a?(Array)
|
|
116
160
|
|
|
117
161
|
value.to_s.split(",").map(&:strip)
|
|
118
162
|
end
|
|
119
163
|
|
|
164
|
+
# Answers `in`: whether the held value, or any element of a held Array,
|
|
165
|
+
# occurs in the wanted list.
|
|
166
|
+
#
|
|
120
167
|
# A folded reference hop asks whether the locally-held identity is
|
|
121
168
|
# among the matching target identities. A has_many relationship holds
|
|
122
169
|
# several identities, so the same question becomes an intersection:
|
|
123
|
-
# does
|
|
170
|
+
# does any held identity occur in the wanted set? Scalar `in` retains
|
|
124
171
|
# its existing one-candidate behavior.
|
|
172
|
+
#
|
|
173
|
+
# @param held [Array, Object] the record's own value; an Array contributes each
|
|
174
|
+
# element as a candidate, anything else is the single candidate
|
|
175
|
+
# @param want [Array, String, Object] the wanted set, read through `members`
|
|
176
|
+
# @return [Boolean] whether any candidate, unwrapped through `comparable` and
|
|
177
|
+
# compared as a String, is a wanted member
|
|
125
178
|
def any_member_in?(held, want)
|
|
126
179
|
wanted = members(want)
|
|
127
180
|
candidates = held.is_a?(Array) ? held : [held]
|
|
@@ -129,24 +182,35 @@ module Hecks
|
|
|
129
182
|
candidates.any? { |candidate| wanted.include?(comparable(candidate).to_s) }
|
|
130
183
|
end
|
|
131
184
|
|
|
132
|
-
# `contains
|
|
133
|
-
#
|
|
185
|
+
# Answers `contains`: element membership for a held Array, substring
|
|
186
|
+
# for anything else.
|
|
187
|
+
#
|
|
188
|
+
# `contains` means two different things depending on what is held —
|
|
189
|
+
# real element membership for a `list_of` field (a genuine Array
|
|
134
190
|
# arrives already, one element one member, nothing to split), and
|
|
135
|
-
# plain
|
|
136
|
-
# `members`' comma-split
|
|
137
|
-
#
|
|
138
|
-
# side's `instr`/`position` never
|
|
139
|
-
# moment a scalar's real content
|
|
191
|
+
# plain substring for anything else. The scalar case deliberately
|
|
192
|
+
# stays out of `members`' comma-split: that silently reads a
|
|
193
|
+
# free-text field's own comma as a separator — which the SQL
|
|
194
|
+
# side's `instr`/`position` never does, so the two disagree the
|
|
195
|
+
# moment a scalar's real content holds a comma. Matching SQL's
|
|
140
196
|
# substring reading here keeps every engine answering `contains`
|
|
141
197
|
# identically for the same declared field.
|
|
198
|
+
#
|
|
199
|
+
# @param held [Array, Object] the record's own value; anything but an Array is
|
|
200
|
+
# read through `to_s`
|
|
201
|
+
# @param want [Object] the element or substring looked for, compared as its `to_s`
|
|
202
|
+
# @return [Boolean] whether `held` has `want` as a member (Array) or a substring
|
|
142
203
|
def contains?(held, want)
|
|
143
204
|
return members(held).include?(want.to_s) if held.is_a?(Array)
|
|
144
205
|
|
|
145
206
|
held.to_s.include?(want.to_s)
|
|
146
207
|
end
|
|
147
208
|
|
|
148
|
-
#
|
|
149
|
-
#
|
|
209
|
+
# Answers `none_in_state` by looking the held identity up in another
|
|
210
|
+
# aggregate's repository and reading that record's state.
|
|
211
|
+
#
|
|
212
|
+
# **A cross-aggregate anti-join** — `where ref: { none_in_state:
|
|
213
|
+
# "Claim:held" }` holds when no record in the named aggregate,
|
|
150
214
|
# keyed by this record's own field value, is in the named state.
|
|
151
215
|
# No registry — no way to look the target up — reads as "not
|
|
152
216
|
# excluded", the same graceful default a missing record already
|
|
@@ -154,6 +218,17 @@ module Hecks
|
|
|
154
218
|
# loaded domain; ambiguity (two domains declaring one name) picks
|
|
155
219
|
# the first match rather than refusing, since a where-clause never
|
|
156
220
|
# raises (see `ordered?`).
|
|
221
|
+
#
|
|
222
|
+
# @param held [Object, nil] this record's own field value, used as the target
|
|
223
|
+
# record's identity
|
|
224
|
+
# @param want [String, Symbol] `"Aggregate:state"` — the target aggregate's bare
|
|
225
|
+
# name and the excluded state, split on the first colon
|
|
226
|
+
# @param registry [Runtime::Registry, nil] the booted registry to find the target
|
|
227
|
+
# aggregate and its repository in
|
|
228
|
+
# @return [Boolean] `false` only when the target record exists and its lifecycle
|
|
229
|
+
# field (or `:state`, absent a lifecycle) equals the named state; `true` when
|
|
230
|
+
# there is no registry, no such aggregate or no such record
|
|
231
|
+
# @raise [Runtime::WiringError] if the target aggregate's repository cannot be wired
|
|
157
232
|
def none_in_state?(held, want, registry)
|
|
158
233
|
return true unless registry
|
|
159
234
|
|
|
@@ -165,14 +240,14 @@ module Hecks
|
|
|
165
240
|
record = registry.repository(target_domain, target_ir).find(held)
|
|
166
241
|
return true unless record
|
|
167
242
|
|
|
168
|
-
#
|
|
169
|
-
#
|
|
243
|
+
# The field a state lives on, read from the target's own
|
|
244
|
+
# declaration — not assumed to be literally named `state`. Every
|
|
170
245
|
# `none_in_state` fixture this comparator originally shipped
|
|
171
246
|
# with (spec/query_none_in_state_*_spec.rb) happens to declare a
|
|
172
247
|
# plain `attribute :state, ...` rather than a real `lifecycle`,
|
|
173
248
|
# which is how the previous hardcoded `record.state[:state]`
|
|
174
249
|
# passed every one of them while being wrong for the shape this
|
|
175
|
-
# whole comparator exists to answer about: a real state
|
|
250
|
+
# whole comparator exists to answer about: a real state machine.
|
|
176
251
|
# `lifecycle :field, default: ... do ... end` stores its state
|
|
177
252
|
# under `field` (`Instance#assign_creation_attributes`'s own
|
|
178
253
|
# `state[aggregate.lifecycle.field.to_sym] = ...`), and this
|
|
@@ -181,7 +256,7 @@ module Hecks
|
|
|
181
256
|
# `lifecycle :status` among many others) — so the hardcoded key
|
|
182
257
|
# silently read `nil` from every real lifecycle-backed target,
|
|
183
258
|
# comparable(nil) != state was true unconditionally, and
|
|
184
|
-
# `none_in_state` against
|
|
259
|
+
# `none_in_state` against any lifecycle aggregate answered
|
|
185
260
|
# "not excluded" for every row, always, no matter its actual
|
|
186
261
|
# state. Found chasing `QualityControl::Bug.AwaitingClearance`
|
|
187
262
|
# (qa/bluebook/quality_control.bluebook), which is exactly this
|
|
@@ -194,6 +269,13 @@ module Hecks
|
|
|
194
269
|
comparable(record.state[field]) != state
|
|
195
270
|
end
|
|
196
271
|
|
|
272
|
+
# Searches every loaded domain for an aggregate by its bare name, taking
|
|
273
|
+
# the first match in the registry's load order.
|
|
274
|
+
#
|
|
275
|
+
# @param registry [Runtime::Registry] the booted registry whose bluebooks are searched
|
|
276
|
+
# @param name [String, nil] the aggregate's bare `hecks_name`, such as `"Claim"`
|
|
277
|
+
# @return [Array(String, Bluebook::Aggregate), nil] the owning domain's name and the
|
|
278
|
+
# aggregate; `nil` when no loaded domain declares one by that name
|
|
197
279
|
def find_aggregate_by_name(registry, name)
|
|
198
280
|
registry.bluebooks.each do |domain, bluebook|
|
|
199
281
|
aggregate = bluebook.aggregates.find { |a| a.hecks_name == name }
|
|
@@ -17,15 +17,27 @@ module Hecks
|
|
|
17
17
|
# ReadModelBuilder so the two specification kinds parse the same
|
|
18
18
|
# clauses identically rather than each carrying its own copy.
|
|
19
19
|
module DSL
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# `
|
|
20
|
+
# Records one `WhereClause` per `field => value` pair — what the
|
|
21
|
+
# `where` DSL word forwards to.
|
|
22
|
+
#
|
|
23
|
+
# `where`/`order_by`/`limit`/`offset`/`authorize` (all below) carry
|
|
24
|
+
# an `_impl` name because the words themselves are dispatched by
|
|
25
|
+
# the grammar table rather than defined as methods: both the Query
|
|
26
|
+
# and the ReadModel Keyword rows name the `_impl` method in
|
|
27
|
+
# `calls:`, the same shape `attribute_impl` has, and a shared mixin
|
|
28
|
+
# means one method each. `where`/`order_by` are bootstrap-reachable
|
|
29
|
+
# (every core chapter's own `read_model` filters its roster with
|
|
30
|
+
# them), so they must resolve through
|
|
31
|
+
# `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` while the grammar
|
|
32
|
+
# table is still being built.
|
|
33
|
+
#
|
|
34
|
+
# @param clauses [Hash{Symbol => Object}] field name (a dotted or slashed path is
|
|
35
|
+
# one Symbol) to either a literal, a Symbol naming a query argument, or a
|
|
36
|
+
# one-pair Hash `{ comparator => operand }` such as `{ gte: :minimum }`; a bare
|
|
37
|
+
# value means `eq`
|
|
38
|
+
# @return [void]
|
|
39
|
+
# @raise [ArgumentError] if a Hash value does not have exactly one pair, or names a
|
|
40
|
+
# comparator outside `COMPARATORS`
|
|
29
41
|
def where_impl(clauses)
|
|
30
42
|
@wheres ||= []
|
|
31
43
|
clauses.each do |field, value|
|
|
@@ -34,15 +46,59 @@ module Hecks
|
|
|
34
46
|
end
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Records the query's single ordering, replacing any declared earlier.
|
|
50
|
+
#
|
|
51
|
+
# @param field [Symbol, String] the field to order by; a dotted path such as
|
|
52
|
+
# `:"order.value"` reaches a value object's member
|
|
53
|
+
# @param direction [Symbol, String] `:asc` or `:desc`
|
|
54
|
+
# @return [OrderBy] the ordering just recorded
|
|
37
55
|
def order_by_impl(field, direction = :asc)
|
|
38
56
|
@order_by = OrderBy.new(field: field, direction: direction)
|
|
39
57
|
end
|
|
40
58
|
|
|
59
|
+
# Records the most rows the query returns.
|
|
60
|
+
#
|
|
61
|
+
# @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
|
|
62
|
+
# argument that supplies it
|
|
63
|
+
# @return [LimitSpec] the limit just recorded
|
|
41
64
|
def limit_impl(value) = @limit = LimitSpec.new(value: value)
|
|
65
|
+
|
|
66
|
+
# Records how many matched rows the query skips before the limit applies.
|
|
67
|
+
#
|
|
68
|
+
# @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
|
|
69
|
+
# argument that supplies it
|
|
70
|
+
# @return [OffsetSpec] the offset just recorded
|
|
42
71
|
def offset_impl(value) = @offset = OffsetSpec.new(value: value)
|
|
72
|
+
|
|
73
|
+
# Records a cursor declaration. The word parses and round-trips, but both builders
|
|
74
|
+
# refuse it at `build` (`seal_cursor`) because no interpreter applies it.
|
|
75
|
+
#
|
|
76
|
+
# @param value [Symbol, Object] the query argument carrying the cursor, such as
|
|
77
|
+
# `:after`, or a literal
|
|
78
|
+
# @return [CursorSpec] the cursor just recorded
|
|
43
79
|
def cursor(value) = @cursor = CursorSpec.new(value: value)
|
|
80
|
+
|
|
81
|
+
# Records the authorization the query declares, and the argument that scopes it
|
|
82
|
+
# to one tenant.
|
|
83
|
+
#
|
|
84
|
+
# @param policy [Symbol, String] name of the access policy, such as `:vault_access`
|
|
85
|
+
# @param tenant [Symbol, String, nil] the field every ask must supply a value for
|
|
86
|
+
# and is filtered by (`Runtime::TenantScope`); `nil` declares no tenant scoping
|
|
87
|
+
# @return [AuthorizationSpec] the authorization just recorded
|
|
44
88
|
def authorize_impl(policy, tenant: nil) = @authorization = AuthorizationSpec.new(policy: policy, tenant: tenant)
|
|
89
|
+
|
|
90
|
+
# Records where a null sorts, overriding the `native` default.
|
|
91
|
+
#
|
|
92
|
+
# @param mode [Symbol, String] `:first` or `:last`; anything else orders as
|
|
93
|
+
# `:native` does (see `NullPolicy.order`)
|
|
94
|
+
# @return [NullSemantics] the null policy just recorded
|
|
45
95
|
def nulls(mode) = @null_semantics = NullSemantics.new(mode: mode)
|
|
96
|
+
|
|
97
|
+
# Records that the query asks its adapter to expose the query it generates.
|
|
98
|
+
#
|
|
99
|
+
# @param mode [Symbol, String] what to expose; `:sql` is the only mode
|
|
100
|
+
# `Ports::Query.validate!` lets an adapter without `inspect_query` serve
|
|
101
|
+
# @return [InspectionSpec] the inspection request just recorded
|
|
46
102
|
def inspect_query(mode = :sql) = @inspection = InspectionSpec.new(mode: mode)
|
|
47
103
|
|
|
48
104
|
private
|
|
@@ -10,39 +10,67 @@ module Hecks
|
|
|
10
10
|
module NullPolicy
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Sorts records in memory by one key, placing the null-keyed ones where
|
|
14
|
+
# the policy says and keeping ties in their incoming order.
|
|
15
|
+
#
|
|
16
|
+
# Stable on purpose : rows arrive already in identity order from
|
|
17
|
+
# `Ports::Query::Ordering`, and that base is what makes a tie deterministic
|
|
15
18
|
# rather than store-dependent. A plain sort_by is not stable in Ruby, so
|
|
16
19
|
# equal keys would shuffle and the identity tier would be lost exactly
|
|
17
20
|
# where it is needed. Descending reverses both partitions, so a tie reads
|
|
18
21
|
# identity-descending too — the same total order sql_order renders as
|
|
19
22
|
# `field DESC, id DESC`.
|
|
23
|
+
#
|
|
24
|
+
# @param records [Array<Object>] the rows to order, already in identity order
|
|
25
|
+
# @param direction [Symbol, String] `desc` in any letter case sorts descending;
|
|
26
|
+
# anything else ascending
|
|
27
|
+
# @param policy [NullSemantics, nil] where nulls go: mode `first` or `last`; `nil` or
|
|
28
|
+
# any other mode (`native`) puts nulls first ascending and last descending
|
|
29
|
+
# @yield reads the sort key out of one record
|
|
30
|
+
# @yieldparam record [Object] one element of `records`
|
|
31
|
+
# @yieldreturn [Comparable, nil] the key, mutually comparable across records; `nil`
|
|
32
|
+
# marks the record as null-keyed
|
|
33
|
+
# @return [Array<Object>] a new Array holding every record in the requested order
|
|
34
|
+
# @raise [ArgumentError] if two non-nil keys cannot be compared with each other
|
|
20
35
|
def order(records, direction:, policy: nil, &key)
|
|
36
|
+
descending = direction.to_s.downcase == "desc"
|
|
21
37
|
null_rows, valued_rows = records.partition { |record| key.call(record).nil? }
|
|
22
38
|
sorted = valued_rows.each_with_index.sort_by { |record, index| [key.call(record), index] }.map(&:first)
|
|
23
|
-
if
|
|
39
|
+
if descending
|
|
24
40
|
sorted.reverse!
|
|
25
41
|
null_rows.reverse!
|
|
26
42
|
end
|
|
27
43
|
case policy&.mode.to_s
|
|
28
44
|
when "first" then null_rows + sorted
|
|
29
45
|
when "last" then sorted + null_rows
|
|
30
|
-
else
|
|
46
|
+
else descending ? sorted + null_rows : null_rows + sorted
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
|
|
34
|
-
#
|
|
35
|
-
# `NULLS
|
|
36
|
-
#
|
|
50
|
+
# Renders the `ORDER BY` terms for one expression, with an explicit
|
|
51
|
+
# `NULLS FIRST`/`NULLS LAST` and an `id` tiebreak in the same direction.
|
|
52
|
+
#
|
|
53
|
+
# M3 — an undeclared (`native`) null policy still renders a
|
|
54
|
+
# `NULLS ...` clause. Rendering none leaves each dialect's own
|
|
55
|
+
# default to decide: Postgres puts nulls last on ASC (and first
|
|
37
56
|
# on DESC), while `#order` above — this same "native" default,
|
|
38
|
-
# for Memory — puts nulls
|
|
57
|
+
# for Memory — puts nulls first on ASC (and last on DESC), the
|
|
39
58
|
# SQLite convention. Same query, same data, different row order
|
|
40
59
|
# depending only on which adapter ran it. Rendered explicitly
|
|
41
|
-
# here instead, so an undeclared policy means the
|
|
60
|
+
# here instead, so an undeclared policy means the same total
|
|
42
61
|
# order everywhere rather than "whatever this store already does"
|
|
43
62
|
# — matching `#order`'s own default rather than the other way
|
|
44
63
|
# round, since that default is unconditional (Memory/Heki have no
|
|
45
64
|
# dialect to defer to) and SQLite already agrees with it natively.
|
|
65
|
+
#
|
|
66
|
+
# @param expression [String] the SQL expression to order by, already quoted or
|
|
67
|
+
# built by the adapter; interpolated as is
|
|
68
|
+
# @param direction [Symbol, String] `desc` in any letter case renders `DESC`;
|
|
69
|
+
# anything else `ASC`
|
|
70
|
+
# @param policy [NullSemantics, nil] mode `first` or `last` pins the nulls; `nil` or
|
|
71
|
+
# any other mode renders `NULLS FIRST` ascending and `NULLS LAST` descending
|
|
72
|
+
# @return [String] the terms without the `ORDER BY` keyword, such as
|
|
73
|
+
# `"price ASC NULLS FIRST, id ASC"`
|
|
46
74
|
def sql_order(expression, direction, policy)
|
|
47
75
|
direction = direction.to_s.downcase == "desc" ? "DESC" : "ASC"
|
|
48
76
|
nulls = case policy&.mode.to_s
|
|
@@ -53,36 +81,54 @@ module Hecks
|
|
|
53
81
|
"#{expression} #{direction}#{nulls}, id #{direction}"
|
|
54
82
|
end
|
|
55
83
|
|
|
56
|
-
#
|
|
57
|
-
# `sql_predicate` below. That one answers "the value
|
|
84
|
+
# **The comparators a NULL cannot satisfy** — the other half of
|
|
85
|
+
# `sql_predicate` below. That one answers "the value compared to is
|
|
58
86
|
# null" (`eq: nil` -> IS NULL, `ne: nil` -> IS NOT NULL, a real
|
|
59
87
|
# convention both adapters already shared). This one answers the
|
|
60
|
-
# case nothing covered: the
|
|
88
|
+
# case nothing covered: the row's own value is null and the value
|
|
61
89
|
# compared to is not.
|
|
62
90
|
#
|
|
63
91
|
# SQL says unknown. `NULL <> 'red'` is NULL, not true, so the row
|
|
64
92
|
# is not returned. Ruby says `nil != "red"` is true, so it is. The
|
|
65
|
-
# two adapters therefore answered the
|
|
93
|
+
# two adapters therefore answered the same query on the same data
|
|
66
94
|
# differently — Memory returning a row SQLite omitted — which is
|
|
67
95
|
# not a difference of opinion a caller can plan around.
|
|
68
96
|
#
|
|
69
97
|
# Resolved toward SQL, and not because SQL is the store: an absent
|
|
70
|
-
# or null field is
|
|
98
|
+
# or null field is unknown, not a value, and a comparison against
|
|
71
99
|
# unknown is unknown rather than true. Making SQL match Ruby
|
|
72
100
|
# instead would mean compiling every `ne:` to
|
|
73
101
|
# `(col <> $1 OR col IS NULL)` — more to get right in two
|
|
74
102
|
# dialects, and a reliable way to lose an index — to make a real
|
|
75
103
|
# query engine agree with an in-memory one.
|
|
76
104
|
#
|
|
77
|
-
# `none_in_state` is deliberately
|
|
105
|
+
# `none_in_state` is deliberately not here: it is a 9th, vendored
|
|
78
106
|
# comparator whose `held` is a reference id, and a row holding no
|
|
79
107
|
# reference is genuinely "not in that state" rather than unknown.
|
|
80
108
|
NULL_UNMATCHABLE = %w[eq ne lt lte gt gte in contains].freeze
|
|
81
109
|
|
|
110
|
+
# Decides whether a comparison is lost before it starts because the row's
|
|
111
|
+
# own value is null — the rule `NULL_UNMATCHABLE`'s comment argues for.
|
|
112
|
+
#
|
|
113
|
+
# @param operation [Symbol, String] the comparator name, such as `:ne`
|
|
114
|
+
# @param held [Object, nil] the row's own value for the field
|
|
115
|
+
# @param want [Object, nil] the value compared against
|
|
116
|
+
# @return [Boolean] `true` when `held` is `nil`, `want` is not, and the comparator is
|
|
117
|
+
# one of `NULL_UNMATCHABLE`; `none_in_state` is never unmatchable
|
|
82
118
|
def unmatchable?(operation, held, want)
|
|
83
119
|
held.nil? && !want.nil? && NULL_UNMATCHABLE.include?(operation.to_s)
|
|
84
120
|
end
|
|
85
121
|
|
|
122
|
+
# Renders the SQL for a comparison against a null value — `eq: nil` as
|
|
123
|
+
# `IS NULL`, `ne: nil` as `IS NOT NULL` — so an adapter never binds a
|
|
124
|
+
# `NULL` parameter to `=` or `<>`.
|
|
125
|
+
#
|
|
126
|
+
# @param expression [String] the SQL expression for the field, interpolated as is
|
|
127
|
+
# @param operation [Symbol, String] the comparator name
|
|
128
|
+
# @param value [Object, nil] the resolved value compared against
|
|
129
|
+
# @return [Array(String, Array), nil] the predicate text and its (empty) bind
|
|
130
|
+
# parameters; `nil` when `value` is not `nil` or the comparator is neither `eq`
|
|
131
|
+
# nor `ne`, leaving the adapter to render the clause itself
|
|
86
132
|
def sql_predicate(expression, operation, value)
|
|
87
133
|
if value.nil? && operation.to_s == "eq"
|
|
88
134
|
["#{expression} IS NULL", []]
|
|
@@ -4,6 +4,10 @@ module Hecks
|
|
|
4
4
|
NullSemantics = Struct.new(:mode, keyword_init: true) do
|
|
5
5
|
def to_h = { mode: mode.to_s }
|
|
6
6
|
|
|
7
|
+
# Builds the policy a query has when it never writes `nulls`: each
|
|
8
|
+
# engine orders nulls first ascending and last descending (see `NullPolicy`).
|
|
9
|
+
#
|
|
10
|
+
# @return [NullSemantics] a new instance whose mode is `:native`
|
|
7
11
|
def self.default = new(mode: :native)
|
|
8
12
|
end
|
|
9
13
|
end
|
|
@@ -13,6 +13,18 @@ module Hecks
|
|
|
13
13
|
attr_reader :wheres, :order_by, :limit, :offset, :cursor,
|
|
14
14
|
:authorization, :null_semantics, :inspection
|
|
15
15
|
|
|
16
|
+
# @param wheres [Array<WhereClause>] the filter clauses, all of which must hold
|
|
17
|
+
# @param order_by [OrderBy, nil] the single ordering; `nil` leaves identity order
|
|
18
|
+
# @param limit [LimitSpec, nil] the most rows returned; `nil` is unbounded
|
|
19
|
+
# @param offset [OffsetSpec, nil] rows skipped before the limit; `nil` skips none
|
|
20
|
+
# @param cursor [CursorSpec, nil] the cursor declaration; `nil` when none is declared
|
|
21
|
+
# @param authorization [AuthorizationSpec, nil] the declared policy and tenant field;
|
|
22
|
+
# `nil` when the query declares no `authorize`
|
|
23
|
+
# @param inspection [InspectionSpec, nil] the `inspect_query` request; `nil` when
|
|
24
|
+
# the query asks for none
|
|
25
|
+
# @param null_semantics [NullSemantics, nil] where nulls sort; stored as given, so
|
|
26
|
+
# an explicit `nil` (what `ReadModelBuilder` passes when `nulls` was never
|
|
27
|
+
# written) stays `nil` rather than becoming the `native` default
|
|
16
28
|
def initialize(wheres: [], order_by: nil, limit: nil, offset: nil, cursor: nil,
|
|
17
29
|
authorization: nil,
|
|
18
30
|
inspection: nil, null_semantics: NullSemantics.default)
|
|
@@ -26,12 +38,25 @@ module Hecks
|
|
|
26
38
|
@inspection = inspection
|
|
27
39
|
end
|
|
28
40
|
|
|
41
|
+
# Serializes every shared option, declared or not, so a subclass's `to_h`
|
|
42
|
+
# has one fixed set of keys to build on.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{Symbol => Array<Hash>, Hash, nil}] keys `:wheres` (an Array of clause
|
|
45
|
+
# Hashes, `[]` when none), `:order_by`, `:limit`, `:offset`, `:cursor`,
|
|
46
|
+
# `:authorization`, `:null_semantics` and `:inspection`, each that spec's own
|
|
47
|
+
# `to_h` or `nil` when undeclared
|
|
29
48
|
def options_to_h
|
|
30
49
|
{ wheres: @wheres.map(&:to_h), order_by: @order_by&.to_h, limit: @limit&.to_h,
|
|
31
50
|
offset: @offset&.to_h, cursor: @cursor&.to_h, authorization: @authorization&.to_h,
|
|
32
51
|
null_semantics: @null_semantics&.to_h, inspection: @inspection&.to_h }
|
|
33
52
|
end
|
|
34
53
|
|
|
54
|
+
# Serializes only the declared options beyond the settled three, so a
|
|
55
|
+
# construct that never wrote one keeps its wire shape unchanged.
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash{Symbol => Hash}] the subset of `options_to_h` that is set, without
|
|
58
|
+
# `:wheres`, `:order_by` and `:limit` (which the subclass emits itself) and
|
|
59
|
+
# without a `:null_semantics` of `{ mode: "native" }`; `{}` when nothing is
|
|
35
60
|
def extra_options_to_h
|
|
36
61
|
options_to_h.reject do |key, value|
|
|
37
62
|
value.nil? || value == [] || (key == :null_semantics && value == { mode: "native" })
|