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,478 @@
|
|
|
1
|
+
require_relative "../../../../../../naming"
|
|
2
|
+
require_relative "../../translation/rule_compiler"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class PostgresEra
|
|
7
|
+
class Lineage
|
|
8
|
+
module HeadCompiler
|
|
9
|
+
# ── head compilation ───────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
# The snapshot table backing one aggregate's CURRENT era: one row
|
|
12
|
+
# per live id, upserted transactionally by PostgresEra#append
|
|
13
|
+
# alongside the journal insert it belongs to, never derived by
|
|
14
|
+
# scanning history thereafter. Idempotent and unguarded by
|
|
15
|
+
# `provisioner?` — a plain per-aggregate table, the same
|
|
16
|
+
# privilege class as the per-aggregate views/matviews already
|
|
17
|
+
# created this way, not the shared owner-only journal.
|
|
18
|
+
#
|
|
19
|
+
# BACKFILLED, not just created, the FIRST time this table comes
|
|
20
|
+
# into existence for a domain that already has journal history —
|
|
21
|
+
# a fresh era (compile_head! at mint) never has any, but era 1
|
|
22
|
+
# against an EXISTING deployment (any domain running before this
|
|
23
|
+
# snapshot table existed at all) does, and an empty table would
|
|
24
|
+
# silently erase every already-written record from every read
|
|
25
|
+
# the instant the head view starts pointing at it.
|
|
26
|
+
#
|
|
27
|
+
# CREATION and BACKFILL are deliberately two separate steps now,
|
|
28
|
+
# not one nested transaction — principle 1 (docs/implemented/postgres-era-
|
|
29
|
+
# adapter-split-plan.md): no operation may hold a lock across a
|
|
30
|
+
# scan whose duration scales with table size, and
|
|
31
|
+
# `backfill_head_snapshot!` below is now a CHUNKED, resumable
|
|
32
|
+
# scan (see `ResumableBackfill`), the opposite of something that
|
|
33
|
+
# belongs inside one transaction. Creation alone (an empty
|
|
34
|
+
# table, nothing to scan) still needs the short-held lock
|
|
35
|
+
# unchanged from before. Backfill runs unconditionally after —
|
|
36
|
+
# cheap and correct whether the table was just created, already
|
|
37
|
+
# fully backfilled by a prior boot (one indexed point lookup via
|
|
38
|
+
# `hecks_backfill_progress`, see ResumableBackfill), or left
|
|
39
|
+
# mid-backfill by a crashed or still-racing concurrent boot
|
|
40
|
+
# (picks up exactly where the last COMMITTED chunk left off).
|
|
41
|
+
def ensure_head_snapshot!(storage_name, era)
|
|
42
|
+
name = head_snapshot(storage_name, era)
|
|
43
|
+
unless table_exists?(name)
|
|
44
|
+
# Locked, not a bare CREATE TABLE IF NOT EXISTS — two
|
|
45
|
+
# processes booting this aggregate for the very first time
|
|
46
|
+
# concurrently must not race to CREATE (one wins, one gets a
|
|
47
|
+
# real Postgres error). Re-checked under the lock: the fast
|
|
48
|
+
# path above skips locking entirely once any boot has
|
|
49
|
+
# already finished this once, which is every boot after the
|
|
50
|
+
# first.
|
|
51
|
+
#
|
|
52
|
+
# NESTABLE — this runs both standalone (adapter boot, its
|
|
53
|
+
# own transaction) and from `compile_head!` while
|
|
54
|
+
# `mint_era!` is already mid-transaction (its manual
|
|
55
|
+
# `BEGIN`, held open for the era row, every aggregate's
|
|
56
|
+
# matview, and the advisory lock advance_era! relies on).
|
|
57
|
+
# `@db.transaction` is a bare BEGIN/COMMIT with no savepoint
|
|
58
|
+
# nesting (see H2 in docs/audits/2026-08-10-main-bug-
|
|
59
|
+
# audit.md) — called while already inside a transaction, its
|
|
60
|
+
# COMMIT would end THAT transaction early, releasing mint's
|
|
61
|
+
# advisory lock and letting a later step run uncommitted.
|
|
62
|
+
# `nested_transaction` tells the two cases apart and uses a
|
|
63
|
+
# SAVEPOINT for the second, so the surrounding mint stays
|
|
64
|
+
# one real transaction from BEGIN to its own COMMIT
|
|
65
|
+
# regardless of how deep this is called from.
|
|
66
|
+
nested_transaction("hecks_head_snapshot") do
|
|
67
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
|
|
68
|
+
next if table_exists?(name)
|
|
69
|
+
|
|
70
|
+
# `operation` + a NULLABLE `state` — H3, docs/audits/2026-08-
|
|
71
|
+
# 10-main-bug-audit.md: a delete used to just `DELETE FROM`
|
|
72
|
+
# this table, leaving NO row at all for an id carried in
|
|
73
|
+
# from an ancestor era. `compile_head!`'s union below then
|
|
74
|
+
# had nothing current-era to outrank the ancestor
|
|
75
|
+
# matview's own (still-live-looking) `save` row with, so a
|
|
76
|
+
# deleted ancestor-carried record kept winning `DISTINCT
|
|
77
|
+
# ON` forever. A delete now upserts a TOMBSTONE row here
|
|
78
|
+
# instead (`operation = 'delete'`, `state` NULL) — the
|
|
79
|
+
# exact same ordinal-guarded upsert every write already
|
|
80
|
+
# uses, so it participates in the union/`DISTINCT ON`
|
|
81
|
+
# exactly like a save does, and out-ranks the ancestor row
|
|
82
|
+
# by ordinal the same way a real re-save already did
|
|
83
|
+
# ("re-saves are masked correctly" — the audit's own
|
|
84
|
+
# phrasing for why this half of the read path was never
|
|
85
|
+
# broken).
|
|
86
|
+
@db.exec(<<~SQL)
|
|
87
|
+
CREATE TABLE #{quote(name)} (
|
|
88
|
+
id text PRIMARY KEY,
|
|
89
|
+
ordinal bigint NOT NULL,
|
|
90
|
+
operation text NOT NULL DEFAULT 'save',
|
|
91
|
+
state jsonb
|
|
92
|
+
)
|
|
93
|
+
SQL
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
# SELF-HEALING for a table this ADR predates — same idiom as
|
|
97
|
+
# `postgres/schema_builder.rb`'s own `hecks_version` backfill:
|
|
98
|
+
# unconditional, runs on every boot, a no-op once the column
|
|
99
|
+
# is there. A table created before H3's fix has no
|
|
100
|
+
# `operation` column and a `state NOT NULL` constraint a
|
|
101
|
+
# tombstone's NULL state would violate; both are corrected
|
|
102
|
+
# here regardless of which branch above just ran.
|
|
103
|
+
@db.exec("ALTER TABLE #{quote(name)} ADD COLUMN IF NOT EXISTS operation text NOT NULL DEFAULT 'save'")
|
|
104
|
+
@db.exec("ALTER TABLE #{quote(name)} ALTER COLUMN state DROP NOT NULL")
|
|
105
|
+
backfill_head_snapshot!(name, storage_name, era)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The exact reduction era 1's OLD live view used to run on every
|
|
109
|
+
# single read — DISTINCT ON latest-per-id, saves only — now
|
|
110
|
+
# chunked through `ResumableBackfill#chunked_backfill!` instead
|
|
111
|
+
# of one blocking `INSERT ... SELECT` over the whole journal:
|
|
112
|
+
# each chunk reads the next page of distinct ids (a plain SELECT,
|
|
113
|
+
# no lock held — an ordinary reader or writer is never blocked
|
|
114
|
+
# by this running), then upserts it under the same short-held
|
|
115
|
+
# advisory lock + ordinal guard every other upsert in this
|
|
116
|
+
# adapter already uses. See `ResumableBackfill`'s own header for
|
|
117
|
+
# why this is RESUMABLE, not merely safe-to-restart.
|
|
118
|
+
def backfill_head_snapshot!(name, storage_name, era)
|
|
119
|
+
chunked_backfill!(
|
|
120
|
+
name,
|
|
121
|
+
source_sql: lambda do |cursor|
|
|
122
|
+
<<~SQL
|
|
123
|
+
SELECT id, ordinal, state FROM (
|
|
124
|
+
SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, ordinal, operation, state
|
|
125
|
+
FROM #{quoted_journal}
|
|
126
|
+
WHERE era = #{era.to_i} AND aggregate = #{text_literal(storage_name)}
|
|
127
|
+
#{cursor ? "AND aggregate_id > #{text_literal(cursor)}" : ''}
|
|
128
|
+
ORDER BY aggregate_id, ordinal DESC
|
|
129
|
+
) latest WHERE operation = 'save' ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
|
|
130
|
+
SQL
|
|
131
|
+
end,
|
|
132
|
+
upsert: lambda do |rows|
|
|
133
|
+
rows.each do |row|
|
|
134
|
+
# Always `'save'` — `source_sql` above already filtered
|
|
135
|
+
# to `operation = 'save'`, so nothing this backfill ever
|
|
136
|
+
# sees is a delete tombstone.
|
|
137
|
+
@db.exec_params(
|
|
138
|
+
"INSERT INTO #{quote(name)} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
|
|
139
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
140
|
+
"state = EXCLUDED.state WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
141
|
+
[row["id"], row["ordinal"], row["state"]]
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def table_exists?(name)
|
|
149
|
+
@db.exec_params("SELECT to_regclass($1) IS NOT NULL AS present", [name]).getvalue(0, 0) == "t"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A transaction wrapper safe to call from inside an already-open
|
|
153
|
+
# transaction, unlike `PG::Connection#transaction` (bare
|
|
154
|
+
# BEGIN/COMMIT, no savepoint nesting — see H2). Standalone
|
|
155
|
+
# (`PQTRANS_IDLE`), this is `@db.transaction` itself: a real
|
|
156
|
+
# transaction, committed or rolled back on the way out. Nested
|
|
157
|
+
# (anything else — `PQTRANS_INTRANS` from a manual `BEGIN` like
|
|
158
|
+
# mint_era!'s, or the gem's own `#transaction`), it's a SAVEPOINT
|
|
159
|
+
# instead: released on success, rolled back to (then the error
|
|
160
|
+
# re-raised) on failure, and either way the surrounding
|
|
161
|
+
# transaction is never touched — no early COMMIT, no early
|
|
162
|
+
# release of whatever advisory lock it holds.
|
|
163
|
+
def nested_transaction(name)
|
|
164
|
+
return @db.transaction { yield } if @db.transaction_status == PG::PQTRANS_IDLE
|
|
165
|
+
|
|
166
|
+
@db.exec("SAVEPOINT #{name}")
|
|
167
|
+
begin
|
|
168
|
+
yield
|
|
169
|
+
@db.exec("RELEASE SAVEPOINT #{name}")
|
|
170
|
+
rescue StandardError
|
|
171
|
+
@db.exec("ROLLBACK TO SAVEPOINT #{name}")
|
|
172
|
+
raise
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Era 1: the head is the snapshot table itself, verbatim — no
|
|
177
|
+
# per-read reduction over history left to do, because `append`
|
|
178
|
+
# already keeps the snapshot current as of every write.
|
|
179
|
+
def ensure_first_head!(storage_name)
|
|
180
|
+
ensure_head_snapshot!(storage_name, 1)
|
|
181
|
+
# `WHERE operation = 'save'` — era 1 has no ancestor tail to
|
|
182
|
+
# worry about, but the snapshot table can now hold delete
|
|
183
|
+
# tombstones too (H3, see `ensure_head_snapshot!`'s own
|
|
184
|
+
# comment), and a tombstone's `state` is NULL: without this
|
|
185
|
+
# filter a deleted id would still resolve here, just to a nil
|
|
186
|
+
# state, instead of correctly falling out of the head at all.
|
|
187
|
+
@db.exec(<<~SQL)
|
|
188
|
+
CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
|
|
189
|
+
SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))} WHERE operation = 'save'
|
|
190
|
+
SQL
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The compiled chain over the ancestor tail — the matview's body,
|
|
194
|
+
# also runnable as a LIVE query (the audit previews a pending
|
|
195
|
+
# era through exactly this SQL before anything is minted).
|
|
196
|
+
#
|
|
197
|
+
# NEVER flatten the edges into one merged rule set, however
|
|
198
|
+
# tempting the optimization looks. The two-line counterexample:
|
|
199
|
+
# edge 1 renames A→B, edge 2 renames C→A. Flattened, a single
|
|
200
|
+
# phase order either applies C→A before A→B (aliasing C's value
|
|
201
|
+
# into B) or drops the recycled name entirely — there is NO
|
|
202
|
+
# correct position for both rules in one pass. Chaining the
|
|
203
|
+
# original edges in mint order reproduces the true execution
|
|
204
|
+
# exactly and needs no such reasoning.
|
|
205
|
+
# ONLY THE LATEST ANCESTOR ENTRY PER ID IS OBSERVABLE. The head,
|
|
206
|
+
# the tail-merge, and the audit all reduce by
|
|
207
|
+
# DISTINCT ON (aggregate_id) ORDER BY ordinal DESC before anyone
|
|
208
|
+
# reads a state, so an entry with a newer sibling can never
|
|
209
|
+
# reach a reader. Translating those siblings computes and stores
|
|
210
|
+
# rows nothing can observe — on an append-only journal a record
|
|
211
|
+
# edited a hundred times cost a hundred translations to serve
|
|
212
|
+
# one.
|
|
213
|
+
#
|
|
214
|
+
# So the tail is reduced BEFORE the chain, not after. The
|
|
215
|
+
# translated output is identical (the reducer is idempotent and
|
|
216
|
+
# the survivor is the same row either way); only the work
|
|
217
|
+
# changes, from |journal entries| to |distinct records|.
|
|
218
|
+
#
|
|
219
|
+
# `era` must survive the reduction: each edge's CASE reads it to
|
|
220
|
+
# decide whether a row is old enough to need that edge applied.
|
|
221
|
+
def latest_per_id(tail)
|
|
222
|
+
return tail if tail.to_s.empty?
|
|
223
|
+
|
|
224
|
+
"SELECT DISTINCT ON (aggregate_id) ordinal, era, aggregate, aggregate_id, operation, state " \
|
|
225
|
+
"FROM (#{tail}) tail_entries ORDER BY aggregate_id, ordinal DESC"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# THE LAYERED BUILD — era N from era N-1's matview, not from raw
|
|
229
|
+
# history. Returns nil when it cannot apply, and the caller
|
|
230
|
+
# falls back to the full chain above.
|
|
231
|
+
#
|
|
232
|
+
# The algebra it rests on, both halves load-bearing:
|
|
233
|
+
#
|
|
234
|
+
# 1. THE CUTS DO NOT MOVE. ancestor_tail_sql cuts ancestor k at
|
|
235
|
+
# W(k+1) — the watermark recorded when era k+1 was minted.
|
|
236
|
+
# Those are the SAME literals in the era N-1 build and the
|
|
237
|
+
# era N build, so era N-1's matview already carries exactly
|
|
238
|
+
# the cut era N needs for eras 1..N-2. Only era N-1's own
|
|
239
|
+
# rows are new, and they are cut at W(N). The watermark is
|
|
240
|
+
# still a literal baked into a definition; it is simply
|
|
241
|
+
# baked into the layer beneath. (This is why the tail-merge
|
|
242
|
+
# must pass full: — it moves every watermark at once.)
|
|
243
|
+
#
|
|
244
|
+
# 2. REDUCING IS ASSOCIATIVE. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
|
|
245
|
+
# because the survivor is max-ordinal-per-id either way. So
|
|
246
|
+
# reducing per layer is the same answer as reducing the
|
|
247
|
+
# whole tail once.
|
|
248
|
+
#
|
|
249
|
+
# And every row on both sides of the union is already in era
|
|
250
|
+
# N-1's shape — the matview because it was chained through edge
|
|
251
|
+
# N-2, era N-1's own rows because that is the shape they were
|
|
252
|
+
# written under — so the final edge applies uniformly, with no
|
|
253
|
+
# per-era CASE. That equality is asserted, not argued: the spec
|
|
254
|
+
# builds a third era both ways and diffs them.
|
|
255
|
+
def layered_chain_sql(aggregate, era, edges)
|
|
256
|
+
return nil if era < 3 || edges.size < 2
|
|
257
|
+
|
|
258
|
+
held = eras
|
|
259
|
+
prior = held.find { |candidate| candidate[:ordinal] == era - 1 }
|
|
260
|
+
return nil unless prior && prior[:label]
|
|
261
|
+
|
|
262
|
+
prior_view = matview(aggregate.storage_name, era - 1, prior[:label])
|
|
263
|
+
return nil unless view_exists?(prior_view)
|
|
264
|
+
|
|
265
|
+
names = names_by_era(aggregate, edges)
|
|
266
|
+
cut = held.find { |candidate| candidate[:ordinal] == era }&.dig(:watermark)
|
|
267
|
+
declared = edges.last[:translation].for_aggregate(names[:current][edges.size])
|
|
268
|
+
expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
|
|
269
|
+
id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case("operation = 'save'", declared) : "aggregate_id"
|
|
270
|
+
|
|
271
|
+
<<~SQL
|
|
272
|
+
WITH layered AS (
|
|
273
|
+
SELECT DISTINCT ON (aggregate_id) ordinal, aggregate_id, operation, state FROM (
|
|
274
|
+
SELECT ordinal, aggregate_id, operation, state FROM #{quote(prior_view)}
|
|
275
|
+
UNION ALL
|
|
276
|
+
SELECT ordinal, aggregate_id, operation, state FROM #{quoted_journal}
|
|
277
|
+
WHERE era = #{era - 1} AND aggregate = #{text_literal(names[:storage][era - 2])}#{cut ? " AND ordinal <= #{cut}" : ''}
|
|
278
|
+
) layers ORDER BY aggregate_id, ordinal DESC
|
|
279
|
+
)
|
|
280
|
+
SELECT ordinal, #{id_column}, operation,
|
|
281
|
+
CASE WHEN operation = 'save' THEN #{expression} ELSE state END AS state
|
|
282
|
+
FROM layered
|
|
283
|
+
SQL
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def chain_sql(aggregate, era, edges)
|
|
287
|
+
names = names_by_era(aggregate, edges)
|
|
288
|
+
tail = latest_per_id(ancestor_tail_sql(names, era))
|
|
289
|
+
chain = edges.each_with_index.map do |edge, index|
|
|
290
|
+
declared = edge[:translation].for_aggregate(names[:current][index + 1])
|
|
291
|
+
expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
|
|
292
|
+
guard = "era <= #{index + 1} AND operation = 'save'"
|
|
293
|
+
id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case(guard, declared) : "aggregate_id"
|
|
294
|
+
"edge_#{index + 1} AS (SELECT ordinal, era, #{id_column}, operation, " \
|
|
295
|
+
"CASE WHEN #{guard} THEN #{expression} ELSE state END AS state " \
|
|
296
|
+
"FROM #{index.zero? ? 'tail' : "edge_#{index}"})"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
<<~SQL
|
|
300
|
+
WITH tail AS (#{tail}),
|
|
301
|
+
#{chain.join(",\n")}
|
|
302
|
+
SELECT ordinal, aggregate_id, operation, state FROM edge_#{edges.size}
|
|
303
|
+
SQL
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The translated tail as it would stand in era `era`, latest
|
|
307
|
+
# entry per id, saves only — what the audit holds up against the
|
|
308
|
+
# bluebook and the edge.
|
|
309
|
+
def translated_latest(aggregate, era, edges)
|
|
310
|
+
latest_of(chain_sql(aggregate, era, edges))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The UNtranslated ancestor tail, latest entry per id — the
|
|
314
|
+
# "before" side of every per-rule preservation check.
|
|
315
|
+
def ancestor_latest(aggregate, era, edges)
|
|
316
|
+
names = names_by_era(aggregate, edges)
|
|
317
|
+
tail = ancestor_tail_sql(names, era)
|
|
318
|
+
return {} if tail.empty?
|
|
319
|
+
|
|
320
|
+
latest_of("SELECT ordinal, era, aggregate_id, operation, state FROM (#{tail}) tail_rows")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def latest_of(sql)
|
|
324
|
+
rows = @db.exec(<<~SQL)
|
|
325
|
+
SELECT aggregate_id, state FROM (
|
|
326
|
+
SELECT DISTINCT ON (aggregate_id) aggregate_id, operation, state
|
|
327
|
+
FROM (#{sql}) chained ORDER BY aggregate_id, ordinal DESC
|
|
328
|
+
) latest WHERE operation = 'save'
|
|
329
|
+
SQL
|
|
330
|
+
rows.to_h { |row| [row["aggregate_id"], JSON.parse(row["state"])] }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Era N: materialize the translated ancestor tail (edge chain as
|
|
334
|
+
# CTEs, watermarks baked in), then overlay this era's OWN live
|
|
335
|
+
# rows — read from its snapshot table, not re-derived from raw
|
|
336
|
+
# history — in a plain view.
|
|
337
|
+
#
|
|
338
|
+
# THE FROZEN-TAIL INVARIANT. This materialized view is correct
|
|
339
|
+
# by construction only because BOTH of these hold:
|
|
340
|
+
# 1. journal rows are never updated or deleted (immutability
|
|
341
|
+
# by privilege), and
|
|
342
|
+
# 2. the watermark is baked into this definition as a LITERAL,
|
|
343
|
+
# so post-cut ancestor writes — which DO keep arriving
|
|
344
|
+
# while a fork is live — can never enter the head, even on
|
|
345
|
+
# a full REFRESH.
|
|
346
|
+
# The ancestor partition is append-only but NOT frozen. Any
|
|
347
|
+
# future "optimization" that refreshes incrementally, reads the
|
|
348
|
+
# watermark from hecks_eras at query time, or otherwise
|
|
349
|
+
# re-derives the cut will silently leak the old world's post-cut
|
|
350
|
+
# writes into the new head. Rebuilding the definition (mint,
|
|
351
|
+
# merge) is the only way the cut may move.
|
|
352
|
+
# `full:` forces a rebuild from the raw journal. The tail-merge
|
|
353
|
+
# needs it: it moves EVERY watermark to the new tip, so every
|
|
354
|
+
# ancestor matview's cut goes stale in the same statement, and
|
|
355
|
+
# layering on one would carry a cut that no longer exists.
|
|
356
|
+
#
|
|
357
|
+
# THE LIVE HALF used to be `WHERE era = era AND aggregate = name`
|
|
358
|
+
# over the raw journal — a DISTINCT ON that re-reduced this era's
|
|
359
|
+
# ENTIRE write history on every single read, the exact cost this
|
|
360
|
+
# snapshot table exists to avoid (PostgresEra#append keeps it
|
|
361
|
+
# current, transactionally, as of every write). The union below
|
|
362
|
+
# is bounded by LIVE RECORD COUNT for this era instead of its
|
|
363
|
+
# write count; the ancestor side was already bounded that way
|
|
364
|
+
# (the matview only ever holds the reduced tail, never raw
|
|
365
|
+
# history — see latest_per_id's own comment).
|
|
366
|
+
def compile_head!(aggregate, era, label, edges, full: false)
|
|
367
|
+
storage_name = aggregate.storage_name
|
|
368
|
+
view = matview(storage_name, era, label)
|
|
369
|
+
body =
|
|
370
|
+
if !full && (layered = layered_chain_sql(aggregate, era, edges))
|
|
371
|
+
layered
|
|
372
|
+
else
|
|
373
|
+
chain_sql(aggregate, era, edges)
|
|
374
|
+
end
|
|
375
|
+
@db.exec(<<~SQL)
|
|
376
|
+
CREATE MATERIALIZED VIEW #{quote(view)} AS
|
|
377
|
+
#{body}
|
|
378
|
+
SQL
|
|
379
|
+
# ADDITIVE, changes nothing about what can be pushed through
|
|
380
|
+
# the reduction (that wall is structural, an index doesn't
|
|
381
|
+
# move it — see this file's own module header) — only speeds
|
|
382
|
+
# up the reduction ITSELF, which every read still has to run
|
|
383
|
+
# regardless of a field cache's own two-phase shortcut (a
|
|
384
|
+
# multi-clause query with an uncached clause, an order_by-only
|
|
385
|
+
# query, or simply every read before Track C's cache tables
|
|
386
|
+
# exist for a given field all still hit this). Worth having
|
|
387
|
+
# on any supported server; genuinely skip-scan-usable once
|
|
388
|
+
# running on PG18+.
|
|
389
|
+
@db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{view}_reduce_idx")} ON #{quote(view)} (aggregate_id, ordinal DESC)")
|
|
390
|
+
|
|
391
|
+
# era-qualified, always a FRESH table for a newly-minted era —
|
|
392
|
+
# no separate reset step needed; there is structurally nothing
|
|
393
|
+
# in it yet for anyone to have written, until an append lands
|
|
394
|
+
# under this era specifically.
|
|
395
|
+
ensure_head_snapshot!(storage_name, era)
|
|
396
|
+
@db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
|
|
397
|
+
# THE CURRENT-ERA SIDE READS ITS OWN `operation` COLUMN NOW —
|
|
398
|
+
# H3 (docs/audits/2026-08-10-main-bug-audit.md). This used to
|
|
399
|
+
# hardcode `'save' AS operation` here, on the reasoning that a
|
|
400
|
+
# delete simply removed its row from the snapshot table. But
|
|
401
|
+
# for an id carried in from an ancestor era, that left the
|
|
402
|
+
# ancestor matview's own `save` row as the ONLY row on either
|
|
403
|
+
# side of this union — which then won `DISTINCT ON` and
|
|
404
|
+
# `WHERE operation = 'save'` let it straight through, so a
|
|
405
|
+
# deleted ancestor-carried record kept resurrecting forever.
|
|
406
|
+
# The snapshot side now upserts a real tombstone row on
|
|
407
|
+
# delete (`ensure_head_snapshot!`'s own comment) instead of
|
|
408
|
+
# deleting the row, so reading its actual `operation` here
|
|
409
|
+
# lets that tombstone outrank the stale ancestor `save` row
|
|
410
|
+
# by ordinal, exactly the way a genuine re-save already did.
|
|
411
|
+
@db.exec(<<~SQL)
|
|
412
|
+
CREATE VIEW #{quote(head_view(storage_name))} AS
|
|
413
|
+
SELECT id, state FROM (
|
|
414
|
+
SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, operation, state FROM (
|
|
415
|
+
SELECT ordinal, aggregate_id, operation, state FROM #{quote(view)}
|
|
416
|
+
UNION ALL
|
|
417
|
+
SELECT ordinal, id AS aggregate_id, operation, state
|
|
418
|
+
FROM #{quote(head_snapshot(storage_name, era))}
|
|
419
|
+
) merged ORDER BY aggregate_id, ordinal DESC
|
|
420
|
+
) latest WHERE operation = 'save'
|
|
421
|
+
SQL
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# The aggregate's storage name AS OF each era: `was:` chains
|
|
425
|
+
# walked backward from the current name, one edge at a time.
|
|
426
|
+
# names[:current][i] is the declared (Pascal) name after edge i;
|
|
427
|
+
# names[:storage][e] the snake storage name DURING era e (1-based).
|
|
428
|
+
def names_by_era(aggregate, edges)
|
|
429
|
+
current = Array.new(edges.size + 1)
|
|
430
|
+
current[edges.size] = aggregate.name
|
|
431
|
+
(edges.size - 1).downto(0) do |index|
|
|
432
|
+
declared = edges[index][:translation].for_aggregate(current[index + 1])
|
|
433
|
+
current[index] = declared&.was || current[index + 1]
|
|
434
|
+
end
|
|
435
|
+
storage = current.map { |name| Naming.snake(name) }
|
|
436
|
+
{ current: current, storage: storage }
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Rows this aggregate contributed to every ancestor era, under
|
|
440
|
+
# the name it had THEN, cut at the watermark recorded when the
|
|
441
|
+
# next era was minted.
|
|
442
|
+
def ancestor_tail_sql(names, era)
|
|
443
|
+
watermarks = eras.to_h { |held| [held[:ordinal], held[:watermark]] }
|
|
444
|
+
selects = (1...era).map do |ancestor|
|
|
445
|
+
cut = watermarks[ancestor + 1]
|
|
446
|
+
"SELECT ordinal, era, aggregate, aggregate_id, operation, state FROM #{quoted_journal} " \
|
|
447
|
+
"WHERE era = #{ancestor} AND aggregate = #{text_literal(names[:storage][ancestor - 1])}" \
|
|
448
|
+
"#{cut ? " AND ordinal <= #{cut}" : ''}"
|
|
449
|
+
end
|
|
450
|
+
selects.join(" UNION ALL ")
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# compile_rules/rekeyed?/id_case/compile_id_expression/
|
|
454
|
+
# compile_compute moved to Translation::RuleCompiler — the
|
|
455
|
+
# PURE half of this compiler, with no database connection, no
|
|
456
|
+
# watermark, no era chain. Extracted so Exporter's build-time
|
|
457
|
+
# SQL export (feeding rust/host's own boot-time mint) can call
|
|
458
|
+
# the exact same code this file's own callers do, rather than
|
|
459
|
+
# a hand-ported duplicate. See that module's own header.
|
|
460
|
+
|
|
461
|
+
def view_exists?(name)
|
|
462
|
+
# pg_class + pg_table_is_visible, not information_schema/
|
|
463
|
+
# pg_matviews by bare name — same shared-instance reasoning
|
|
464
|
+
# as provisioning.rb's own catalog lookups: this must resolve
|
|
465
|
+
# the name the same way search_path would, or a sibling
|
|
466
|
+
# domain's same-named view satisfies a check meant for this
|
|
467
|
+
# domain's own.
|
|
468
|
+
@db.exec_params(
|
|
469
|
+
"SELECT 1 FROM pg_class WHERE relname = $1 AND relkind IN ('v', 'm') " \
|
|
470
|
+
"AND pg_table_is_visible(oid)",
|
|
471
|
+
[name]
|
|
472
|
+
).ntuples.positive?
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative "../../../../../../runtime/registry"
|
|
2
|
+
require_relative "../../storage_shape"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Adapters
|
|
6
|
+
class PostgresEra
|
|
7
|
+
class Lineage
|
|
8
|
+
module MintTransaction
|
|
9
|
+
# ── the mint transaction ───────────────────────────────────────
|
|
10
|
+
#
|
|
11
|
+
# One transaction: the new era row (held text + minted name +
|
|
12
|
+
# cut watermark), the new partition, and every aggregate's
|
|
13
|
+
# recompiled matview + head view. The advisory lock is the
|
|
14
|
+
# writer fence — two concurrent minters serialize, and the
|
|
15
|
+
# second finds the era already held. Populating the matview
|
|
16
|
+
# inside the transaction means a convert meeting an unmapped
|
|
17
|
+
# value REFUSES the whole mint, loudly, before anything boots.
|
|
18
|
+
def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
|
|
19
|
+
@db.exec("BEGIN")
|
|
20
|
+
# A concurrent minter blocks briefly, then refuses with a name
|
|
21
|
+
# rather than hanging: mint is non-interactive (the human
|
|
22
|
+
# decision already happened in the audit tool), so the lock is
|
|
23
|
+
# only ever held for the transaction itself.
|
|
24
|
+
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
25
|
+
@db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
|
|
26
|
+
if eras.any? { |era| era[:ordinal] == ordinal }
|
|
27
|
+
@db.exec("ROLLBACK")
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
watermark = last_ordinal
|
|
32
|
+
@db.exec_params(
|
|
33
|
+
"INSERT INTO hecks_eras (domain, ordinal, hash, label, held_text, watermark, held_digest, held_projection, canon_form) " \
|
|
34
|
+
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
|
35
|
+
[@domain, ordinal, hash, label, held_text, watermark, Digest::SHA256.hexdigest(held_text),
|
|
36
|
+
projection && JSON.generate(projection), Runtime::StorageShape::FORM_VERSION]
|
|
37
|
+
)
|
|
38
|
+
archive_text!(ordinal, held_text)
|
|
39
|
+
ensure_partition!(ordinal)
|
|
40
|
+
# AFTER compile_head!, not before — this era's own snapshot
|
|
41
|
+
# table (a possibly-renamed name, per aggregate) is what
|
|
42
|
+
# grant_role! grants on, and compile_head! is what creates it.
|
|
43
|
+
# Granting first would GRANT on a relation that does not
|
|
44
|
+
# exist yet for any aggregate renamed in this very edge.
|
|
45
|
+
aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
|
|
46
|
+
grant_role!(role, aggregates: aggregates, era: ordinal) if role
|
|
47
|
+
# LAST, right before COMMIT — not merely unconditional. Once
|
|
48
|
+
# acquired, a lock is held until the TRANSACTION ends, not
|
|
49
|
+
# just for the statement that took it — so advance_era!'s
|
|
50
|
+
# DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
|
|
51
|
+
# as ALTER TABLE, and unavoidably so: Postgres has no lighter
|
|
52
|
+
# form for changing a policy, unlike the partition attach
|
|
53
|
+
# below) blocks every concurrent writer for as long as it sits
|
|
54
|
+
# BEFORE the expensive step. Ordered here, that block is the
|
|
55
|
+
# width of a few catalog statements plus the commit itself,
|
|
56
|
+
# not the width of compile_head!'s matview build. Measured:
|
|
57
|
+
# moving this above compile_head! (an earlier ordering, caught
|
|
58
|
+
# only once a genuine concurrent-write test was built rather
|
|
59
|
+
# than assumed) reintroduced exactly the mint-stops-the-world
|
|
60
|
+
# cost ensure_partition!'s build-then-ATTACH exists to avoid.
|
|
61
|
+
#
|
|
62
|
+
# UNCONDITIONAL regardless of position — this is the line that
|
|
63
|
+
# drops writing to the old schema. It does not wait for a role
|
|
64
|
+
# to be configured on THIS boot, because the cutoff is a fact
|
|
65
|
+
# about the era, not about who happened to mint it: an old
|
|
66
|
+
# checkout's role, granted by some earlier boot this one knows
|
|
67
|
+
# nothing about, must lose write access the instant this
|
|
68
|
+
# transaction commits.
|
|
69
|
+
advance_era!(ordinal)
|
|
70
|
+
@db.exec("COMMIT")
|
|
71
|
+
true
|
|
72
|
+
rescue PG::LockNotAvailable
|
|
73
|
+
@db.exec("ROLLBACK") rescue nil
|
|
74
|
+
raise Runtime::WiringError,
|
|
75
|
+
"cannot mint era #{ordinal} of #{@domain}: another mint holds the domain lock — " \
|
|
76
|
+
"waited 10s; try again shortly"
|
|
77
|
+
rescue PG::Error => error
|
|
78
|
+
@db.exec("ROLLBACK") rescue nil
|
|
79
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{error.message.strip}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Base privileges for a deployment's app role — a NON-owner,
|
|
83
|
+
# which may append and read once the shared era fence below
|
|
84
|
+
# admits it, and owns nothing. Idempotent, and unconcerned with
|
|
85
|
+
# WHICH era is current: that is advance_era!'s job, not this
|
|
86
|
+
# one's, so a role can be onboarded at any time without
|
|
87
|
+
# disturbing who may write what right now.
|
|
88
|
+
#
|
|
89
|
+
# Owner-only work, so a non-owner app boot skips it: the
|
|
90
|
+
# provisioner has already granted that role, and re-affirming is
|
|
91
|
+
# idempotent anyway.
|
|
92
|
+
#
|
|
93
|
+
# `aggregates:`/`era:` cover the read-cache side of the same
|
|
94
|
+
# story: unlike the journal (immutable, owner-provisioned once),
|
|
95
|
+
# each aggregate's head_snapshot table is a table an app role
|
|
96
|
+
# must itself INSERT/UPDATE/DELETE into — PostgresEra#append writes
|
|
97
|
+
# it directly, not through a view — so it needs real DML grants,
|
|
98
|
+
# not just the SELECT a derived read surface would need. `era:`
|
|
99
|
+
# is the ordinal THIS role is about to write under (the one
|
|
100
|
+
# hold_first!/mint_era! just made current, or the superseded one
|
|
101
|
+
# a stale checkout still speaks) — head_snapshot is era-scoped,
|
|
102
|
+
# so granting on the wrong era's table would grant on a table
|
|
103
|
+
# this role will never touch. Every caller passes its own
|
|
104
|
+
# bluebook's aggregates and resolved era — including a
|
|
105
|
+
# held-but-superseded checkout, since a role connecting to it
|
|
106
|
+
# for the first time (a new instance of an old checkout) has no
|
|
107
|
+
# privileges yet either.
|
|
108
|
+
def grant_role!(role, aggregates: [], era: nil)
|
|
109
|
+
return unless provisioner?
|
|
110
|
+
|
|
111
|
+
quoted_role = quote(role)
|
|
112
|
+
@db.exec("GRANT INSERT, SELECT ON #{quoted_journal} TO #{quoted_role}")
|
|
113
|
+
@db.exec("GRANT USAGE ON SEQUENCE #{quote(sequence)} TO #{quoted_role}")
|
|
114
|
+
return unless era
|
|
115
|
+
|
|
116
|
+
aggregates.each do |aggregate|
|
|
117
|
+
storage_name = aggregate.storage_name
|
|
118
|
+
@db.exec("GRANT SELECT, INSERT, UPDATE, DELETE ON #{quote(head_snapshot(storage_name, era))} TO #{quoted_role}")
|
|
119
|
+
@db.exec("GRANT SELECT ON #{quote(head_view(storage_name))} TO #{quoted_role}") if view_exists?(head_view(storage_name))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# THE current-era fence — ONE policy, shared by every granted
|
|
124
|
+
# role, not one per role. Advancing it is what drops writing to
|
|
125
|
+
# the old schema the instant the new one materializes: the
|
|
126
|
+
# moment this commits, no role — old or new, whether or not it
|
|
127
|
+
# did anything to earn this mint — may insert anything but the
|
|
128
|
+
# era named here. There is no persisted fork: a checkout that
|
|
129
|
+
# keeps a role fenced to a stale era does not exist in this
|
|
130
|
+
# design, because there is no such thing as a role fenced to an
|
|
131
|
+
# era at all — only the ONE era everyone currently shares.
|
|
132
|
+
#
|
|
133
|
+
# A ROW POLICY, not a partition grant. Postgres checks INSERT
|
|
134
|
+
# privilege on the partitioned PARENT for a routed insert and
|
|
135
|
+
# never consults the partition's own grants, so a per-partition
|
|
136
|
+
# GRANT/REVOKE is inert in both directions: grant only on the
|
|
137
|
+
# partition and nobody can write at all; grant on the parent and
|
|
138
|
+
# they may write into EVERY era, ancestors included. Measured,
|
|
139
|
+
# not reasoned about — see the spec, which writes through the
|
|
140
|
+
# fence rather than asserting the catalog.
|
|
141
|
+
#
|
|
142
|
+
# The table owner bypasses RLS by default, and that is load
|
|
143
|
+
# bearing: mint and merge run as the owner and must be able to
|
|
144
|
+
# write any era (the merge re-enters a winner's state into the
|
|
145
|
+
# CURRENT era, and compile_head! reads every ancestor).
|
|
146
|
+
#
|
|
147
|
+
# CALL ONLY WITH THE NEW CURRENT ORDINAL — from hold_first! (era
|
|
148
|
+
# 1) or mint_era! (era N). Calling this with a SUPERSEDED
|
|
149
|
+
# ordinal — from a boot that merely recognizes an old checkout —
|
|
150
|
+
# would roll the fence backward and silently reopen the old
|
|
151
|
+
# schema for everyone. That path grants a role's PRIVILEGES
|
|
152
|
+
# (grant_role!) and stops there on purpose.
|
|
153
|
+
def advance_era!(ordinal)
|
|
154
|
+
@db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
|
|
155
|
+
@db.exec(
|
|
156
|
+
"CREATE POLICY hecks_current_era ON #{quoted_journal} FOR INSERT TO PUBLIC " \
|
|
157
|
+
"WITH CHECK (era = #{ordinal.to_i})"
|
|
158
|
+
)
|
|
159
|
+
@db.exec("DROP POLICY IF EXISTS hecks_read_all ON #{quoted_journal}")
|
|
160
|
+
@db.exec("CREATE POLICY hecks_read_all ON #{quoted_journal} FOR SELECT TO PUBLIC USING (true)")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|