packages feed

keiro-migrations 0.11.0.0 → 0.12.0.0

raw patch · 20 files changed

+2338/−40 lines, 20 filesdep ~kiroku-storedep ~kiroku-store-migrationsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: kiroku-store, kiroku-store-migrations

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -4,6 +4,59 @@ [Keep a Changelog](https://keepachangelog.com/), and the package follows the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## Unreleased++## 0.12.0.0 — 2026-08-14++### Breaking Changes++- Requires `kiroku-store >=0.7 && <0.8` and+  `kiroku-store-migrations ^>=0.3.2.0`. Kiroku migration `0010` supplies the renewable+  history-retention lease required by online replay.++### New Features++- Migration `0030.sql` makes the external-read guard lock the lifecycle row before+  re-reading mutable contract metadata under a matching shared lock. A reader queued+  behind promotion therefore receives retryable `KR001` when its statement snapshot+  crosses the serving epoch; its next statement validates and reads the rebound+  generation from one committed authority.+- Migration `0029.sql` adds bounded, run-scoped async-dedup staging, persisted+  admission/preparation evidence, and private database-clock deadline helpers for+  schema-versioned writer-fence and cumulative promotion-lock attempts.++- Migration `0028.sql` permits an implementation-backed compatibility wrapper+  to retain a zero-argument public signature while a breaking projection shape+  is promoted. Keyed contracts still require a named, versioned private+  implementation; all-row contracts remain Keiro-backed.+- Migration `0027.sql` adds private external-read contract and managed-object+  registries plus the fixed `keiro_read.guard_external_read_v1` security-definer+  guard. The guard takes a shared lifecycle lock, consumes persisted+  `reads_allowed` and serving compatibility, revokes `PUBLIC`, and raises stable+  `KR001`, `KR002`, or `KR003` SQLSTATEs.+- Migration `0026.sql` publishes the frozen, owner-rights+  `keiro_read.projection_group_status_v1` relation and private derived cursor authority.+  The PostgreSQL 18 schema verifier now fingerprints ordered public-view column+  signatures, and migration tests prove narrow-reader privileges and v1 null semantics.+- Migration `0025.sql` persists projection revisions, target generations, serving+  epochs and availability, versioned run/cutover policy, relation/schema evidence,+  promotion-object mappings, replay-source progress, and Kiroku retention handles.++- Migration `0024.sql` renames projection rebuild group+  `catalog_fingerprint` to `slice_fingerprint` and records+  `group_slice_fingerprint` on every rebuild run. This is a clean pre-0.12+  cutover: complete or abandon active catalog rebuilds before upgrading; stale+  group fingerprints are recovered through the library adoption API.++- Migration `0021-keiro-workflows-exact-discovery.sql`. Widens+  `keiro_workflows_active_idx` to `(status, wake_after)` so both arms of keiro's+  exact workflow-discovery predicate are index-served, and returns every+  pre-existing `suspended` instance to `running` once so it is re-examined+  through the new suspend/wake arbitration. Forward-only and idempotent.++### Other Changes++- The source distribution now includes the BSD-3-Clause license file. ## [0.11.0.0] - 2026-08-05  No changes this release. Released with the package set for the `keiro-dsl`
+ LICENSE view
@@ -0,0 +1,28 @@+BSD 3-Clause License++Copyright (c) 2026, Nadeem Bitar++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors+   may be used to endorse or promote products derived from this software without+   specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS+OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT INCLUDING+NEGLIGENCE OR OTHERWISE ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md view
@@ -1,7 +1,7 @@ # Keiro database migrations  `keiro-migrations` exports a native `pg-migrate` component named `keiro`. The-component owns sixteen embedded SQL migrations and declares one dependency,+component owns thirty embedded SQL migrations and declares one dependency, `kiroku`. Applications compose Kiroku first and Keiro second; Keiro never embeds or copies Kiroku's SQL. @@ -53,7 +53,7 @@ ## Importing the shared Codd ledger  `Keiro.Migrations.History.Codd` combines Kiroku's seven mappings with Keiro's-sixteen mappings. `frameworkCoddSourceConfig` selects both histories from the+sixteen legacy mappings. `frameworkCoddSourceConfig` selects both histories from the shared `codd.sql_migrations` or legacy `codd_schema.sql_migrations` ledger, checks the original SHA-256 manifests and payload bytes, and produces one atomic history import:
expected-schema/native/keiro-v18.txt view
@@ -18,6 +18,27 @@ column	keiro_dead_letters.source_event_id	uuid not null column	keiro_dead_letters.source_global_position	bigint not null column	keiro_dead_letters.target_stream_name	text not null+column	keiro_external_read_contracts.argument_names	text[] not null default ARRAY[]::text[]+column	keiro_external_read_contracts.argument_types	text[] not null default ARRAY[]::text[]+column	keiro_external_read_contracts.compatible_revision_ids	text[] not null+column	keiro_external_read_contracts.contract_id	text not null+column	keiro_external_read_contracts.contract_kind	text not null+column	keiro_external_read_contracts.contract_version	integer not null+column	keiro_external_read_contracts.created_at	timestamp with time zone not null default now()+column	keiro_external_read_contracts.definition_hash	text not null+column	keiro_external_read_contracts.group_id	text not null+column	keiro_external_read_contracts.immutable_signature_hash	text not null+column	keiro_external_read_contracts.private_implementation	text+column	keiro_external_read_contracts.private_implementation_version	integer+column	keiro_external_read_contracts.public_function_name	text not null+column	keiro_external_read_contracts.query_model_id	text not null+column	keiro_external_read_contracts.result_shape_hash	text not null+column	keiro_external_read_contracts.result_type	text not null+column	keiro_external_read_contracts.retired_at	timestamp with time zone+column	keiro_external_read_contracts.serving_shape_hash	text+column	keiro_external_read_contracts.state	text not null+column	keiro_external_read_contracts.surface_generation	integer not null+column	keiro_external_read_contracts.updated_at	timestamp with time zone not null default now() column	keiro_inbox.attempt_count	bigint not null default 0 column	keiro_inbox.attributes	jsonb column	keiro_inbox.causation_id	uuid@@ -48,6 +69,19 @@ column	keiro_inbox.status	text not null default 'processing'::text column	keiro_inbox.traceparent	text column	keiro_inbox.tracestate	text+column	keiro_managed_read_objects.contract_id	text+column	keiro_managed_read_objects.contract_version	integer+column	keiro_managed_read_objects.created_at	timestamp with time zone not null default now()+column	keiro_managed_read_objects.definition_hash	text not null+column	keiro_managed_read_objects.managed_by	text not null+column	keiro_managed_read_objects.object_kind	text not null+column	keiro_managed_read_objects.object_name	text not null+column	keiro_managed_read_objects.object_schema	text not null+column	keiro_managed_read_objects.object_signature	text not null default ''::text+column	keiro_managed_read_objects.retired_at	timestamp with time zone+column	keiro_managed_read_objects.state	text not null+column	keiro_managed_read_objects.surface_generation	integer not null+column	keiro_managed_read_objects.updated_at	timestamp with time zone not null default now() column	keiro_outbox.attempt_count	bigint not null default 0 column	keiro_outbox.attributes	jsonb column	keiro_outbox.causation_id	uuid@@ -80,8 +114,123 @@ column	keiro_projection_dedup.applied_at	timestamp with time zone not null default now() column	keiro_projection_dedup.event_id	uuid not null column	keiro_projection_dedup.projection_name	text not null+column	keiro_projection_group_cursors.group_id	text not null+column	keiro_projection_group_cursors.position_basis	text not null+column	keiro_projection_group_cursors.subscription_names	text[] not null default ARRAY[]::text[]+column	keiro_projection_group_cursors.updated_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_adapters.adapter_order	integer not null+column	keiro_projection_rebuild_adapters.apply_count	bigint not null default 0+column	keiro_projection_rebuild_adapters.completed_through	bigint+column	keiro_projection_rebuild_adapters.evaluation_count	bigint not null default 0+column	keiro_projection_rebuild_adapters.projection_id	text not null+column	keiro_projection_rebuild_adapters.run_id	text not null+column	keiro_projection_rebuild_adapters.source_id	text not null+column	keiro_projection_rebuild_adapters.updated_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_dedup_stage.event_id	uuid not null+column	keiro_projection_rebuild_dedup_stage.global_position	bigint not null+column	keiro_projection_rebuild_dedup_stage.projection_name	text not null+column	keiro_projection_rebuild_dedup_stage.run_id	text not null+column	keiro_projection_rebuild_dedup_stage.subscription_name	text not null+column	keiro_projection_rebuild_groups.active_run_id	text+column	keiro_projection_rebuild_groups.completed_at	timestamp with time zone+column	keiro_projection_rebuild_groups.created_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_groups.failed_at	timestamp with time zone+column	keiro_projection_rebuild_groups.failure_code	text+column	keiro_projection_rebuild_groups.failure_detail	text+column	keiro_projection_rebuild_groups.group_id	text not null+column	keiro_projection_rebuild_groups.reads_allowed	boolean not null default true+column	keiro_projection_rebuild_groups.request_reason	text+column	keiro_projection_rebuild_groups.requested_by	text+column	keiro_projection_rebuild_groups.serving_epoch	bigint not null default 0+column	keiro_projection_rebuild_groups.serving_revision_id	text+column	keiro_projection_rebuild_groups.slice_fingerprint	text not null+column	keiro_projection_rebuild_groups.started_at	timestamp with time zone+column	keiro_projection_rebuild_groups.status	text not null+column	keiro_projection_rebuild_groups.updated_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_groups.writes_allowed	boolean not null default true+column	keiro_projection_rebuild_promotion_objects.canonical_name	text not null+column	keiro_projection_rebuild_promotion_objects.generation_name	text not null+column	keiro_projection_rebuild_promotion_objects.object_kind	text not null+column	keiro_projection_rebuild_promotion_objects.object_order	integer not null+column	keiro_projection_rebuild_promotion_objects.run_id	text not null+column	keiro_projection_rebuild_promotion_objects.target_id	text not null+column	keiro_projection_rebuild_run_targets.candidate_generation_id	uuid not null+column	keiro_projection_rebuild_run_targets.run_id	text not null+column	keiro_projection_rebuild_run_targets.target_id	text not null+column	keiro_projection_rebuild_run_targets.target_mode	text not null+column	keiro_projection_rebuild_runs.abandoned_at	timestamp with time zone+column	keiro_projection_rebuild_runs.candidate_revision_id	text+column	keiro_projection_rebuild_runs.captured_head	bigint not null+column	keiro_projection_rebuild_runs.catalog_fingerprint	text not null+column	keiro_projection_rebuild_runs.contract_fingerprint	text not null+column	keiro_projection_rebuild_runs.cutover_lock_timeout_ms	bigint+column	keiro_projection_rebuild_runs.cutover_threshold	bigint+column	keiro_projection_rebuild_runs.dedup_provisional_head	bigint+column	keiro_projection_rebuild_runs.failed_at	timestamp with time zone+column	keiro_projection_rebuild_runs.failure_code	text+column	keiro_projection_rebuild_runs.failure_detail	text+column	keiro_projection_rebuild_runs.failure_position	bigint+column	keiro_projection_rebuild_runs.failure_projection_id	text+column	keiro_projection_rebuild_runs.failure_source_id	text+column	keiro_projection_rebuild_runs.group_id	text not null+column	keiro_projection_rebuild_runs.group_slice_fingerprint	text not null+column	keiro_projection_rebuild_runs.history_retention_expires_at	timestamp with time zone+column	keiro_projection_rebuild_runs.history_retention_lease_id	uuid+column	keiro_projection_rebuild_runs.history_retention_lease_owner	text+column	keiro_projection_rebuild_runs.history_retention_protected_through	bigint+column	keiro_projection_rebuild_runs.history_retention_released_at	timestamp with time zone+column	keiro_projection_rebuild_runs.history_retention_renewed_at	timestamp with time zone+column	keiro_projection_rebuild_runs.page_size	integer not null+column	keiro_projection_rebuild_runs.promoted_at	timestamp with time zone+column	keiro_projection_rebuild_runs.promotion_dedup_limit	bigint+column	keiro_projection_rebuild_runs.promotion_prepared_at	timestamp with time zone+column	keiro_projection_rebuild_runs.rebuild_mode	text not null default 'offline'::text+column	keiro_projection_rebuild_runs.run_id	text not null+column	keiro_projection_rebuild_runs.runner_format	text not null+column	keiro_projection_rebuild_runs.started_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_runs.status	text not null default 'running'::text+column	keiro_projection_rebuild_runs.updated_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_runs.verified_at	timestamp with time zone+column	keiro_projection_rebuild_sources.category	text+column	keiro_projection_rebuild_sources.cursor_position	bigint not null default 0+column	keiro_projection_rebuild_sources.event_count	bigint not null default 0+column	keiro_projection_rebuild_sources.exhausted_through	bigint+column	keiro_projection_rebuild_sources.run_id	text not null+column	keiro_projection_rebuild_sources.source_id	text not null+column	keiro_projection_rebuild_sources.source_scope	text not null+column	keiro_projection_rebuild_sources.target_position	bigint not null+column	keiro_projection_rebuild_sources.updated_at	timestamp with time zone not null default now()+column	keiro_projection_rebuild_verifications.completed_at	timestamp with time zone+column	keiro_projection_rebuild_verifications.detail	text+column	keiro_projection_rebuild_verifications.run_id	text not null+column	keiro_projection_rebuild_verifications.status	text not null default 'pending'::text+column	keiro_projection_rebuild_verifications.verification_id	text not null+column	keiro_projection_rebuild_verifications.verification_version	text not null+column	keiro_projection_revisions.group_id	text not null+column	keiro_projection_revisions.group_slice_fingerprint	text not null+column	keiro_projection_revisions.registered_at	timestamp with time zone not null default now()+column	keiro_projection_revisions.revision_id	text not null+column	keiro_projection_revisions.updated_at	timestamp with time zone not null default now()+column	keiro_projection_target_generations.created_at	timestamp with time zone not null default now()+column	keiro_projection_target_generations.created_by_run_id	text+column	keiro_projection_target_generations.dropped_at	timestamp with time zone+column	keiro_projection_target_generations.expected_shape_id	text not null+column	keiro_projection_target_generations.generation_id	uuid not null+column	keiro_projection_target_generations.group_id	text not null+column	keiro_projection_target_generations.lifecycle	text not null+column	keiro_projection_target_generations.observed_catalog_snapshot	text not null+column	keiro_projection_target_generations.observed_shape_fingerprint	text not null+column	keiro_projection_target_generations.relation_name	text not null+column	keiro_projection_target_generations.relation_oid	bigint not null+column	keiro_projection_target_generations.retired_at	timestamp with time zone+column	keiro_projection_target_generations.revision_id	text not null+column	keiro_projection_target_generations.schema_name	text not null+column	keiro_projection_target_generations.schema_version	text not null+column	keiro_projection_target_generations.served_at	timestamp with time zone+column	keiro_projection_target_generations.target_id	text not null column	keiro_read_models.last_built_at	timestamp with time zone column	keiro_read_models.name	text not null+column	keiro_read_models.rebuild_group_id	text not null column	keiro_read_models.shape_hash	text not null column	keiro_read_models.status	text not null column	keiro_read_models.updated_at	timestamp with time zone not null default now()@@ -166,6 +315,31 @@ constraint	keiro_dead_letters.keiro_dead_letters_source_event_id_not_null	NOT NULL source_event_id constraint	keiro_dead_letters.keiro_dead_letters_source_global_position_not_null	NOT NULL source_global_position constraint	keiro_dead_letters.keiro_dead_letters_target_stream_name_not_null	NOT NULL target_stream_name+constraint	keiro_external_read_contracts.keiro_external_read_contracts_argument_names_not_null	NOT NULL argument_names+constraint	keiro_external_read_contracts.keiro_external_read_contracts_argument_types_not_null	NOT NULL argument_types+constraint	keiro_external_read_contracts.keiro_external_read_contracts_compatible_revision_ids_not_null	NOT NULL compatible_revision_ids+constraint	keiro_external_read_contracts.keiro_external_read_contracts_contract_id_not_null	NOT NULL contract_id+constraint	keiro_external_read_contracts.keiro_external_read_contracts_contract_kind_not_null	NOT NULL contract_kind+constraint	keiro_external_read_contracts.keiro_external_read_contracts_contract_version_not_null	NOT NULL contract_version+constraint	keiro_external_read_contracts.keiro_external_read_contracts_created_at_not_null	NOT NULL created_at+constraint	keiro_external_read_contracts.keiro_external_read_contracts_definition_hash_not_null	NOT NULL definition_hash+constraint	keiro_external_read_contracts.keiro_external_read_contracts_group_id_fkey	FOREIGN KEY (group_id) REFERENCES keiro.keiro_projection_rebuild_groups(group_id)+constraint	keiro_external_read_contracts.keiro_external_read_contracts_group_id_not_null	NOT NULL group_id+constraint	keiro_external_read_contracts.keiro_external_read_contracts_identity_chk	CHECK (((length(contract_id) > 0) AND (contract_version > 0) AND (length(query_model_id) > 0) AND (length(group_id) > 0) AND (length(public_function_name) > 0) AND (length(result_type) > 0) AND (length(result_shape_hash) > 0) AND (length(immutable_signature_hash) > 0) AND (length(definition_hash) > 0) AND (surface_generation > 0) AND (cardinality(compatible_revision_ids) > 0) AND (array_position(compatible_revision_ids, NULL::text) IS NULL) AND (array_position(argument_names, NULL::text) IS NULL) AND (array_position(argument_types, NULL::text) IS NULL) AND (cardinality(argument_names) = cardinality(argument_types))))+constraint	keiro_external_read_contracts.keiro_external_read_contracts_immutable_signature_hash_not_null	NOT NULL immutable_signature_hash+constraint	keiro_external_read_contracts.keiro_external_read_contracts_implementation_chk	CHECK ((((contract_kind = 'all-rows'::text) AND (cardinality(argument_names) = 0) AND (private_implementation IS NULL) AND (private_implementation_version IS NULL)) OR ((contract_kind = 'keyed'::text) AND (private_implementation IS NOT NULL) AND (length(private_implementation) > 0) AND (private_implementation_version > 0))))+constraint	keiro_external_read_contracts.keiro_external_read_contracts_kind_chk	CHECK ((contract_kind = ANY (ARRAY['all-rows'::text, 'keyed'::text])))+constraint	keiro_external_read_contracts.keiro_external_read_contracts_pkey	PRIMARY KEY (contract_id, contract_version)+constraint	keiro_external_read_contracts.keiro_external_read_contracts_public_function_name_argument_key	UNIQUE (public_function_name, argument_types)+constraint	keiro_external_read_contracts.keiro_external_read_contracts_public_function_name_not_null	NOT NULL public_function_name+constraint	keiro_external_read_contracts.keiro_external_read_contracts_query_model_id_not_null	NOT NULL query_model_id+constraint	keiro_external_read_contracts.keiro_external_read_contracts_result_shape_hash_not_null	NOT NULL result_shape_hash+constraint	keiro_external_read_contracts.keiro_external_read_contracts_result_type_not_null	NOT NULL result_type+constraint	keiro_external_read_contracts.keiro_external_read_contracts_retired_chk	CHECK ((((state = 'retired'::text) AND (retired_at IS NOT NULL)) OR ((state <> 'retired'::text) AND (retired_at IS NULL))))+constraint	keiro_external_read_contracts.keiro_external_read_contracts_state_chk	CHECK ((state = ANY (ARRAY['candidate'::text, 'active'::text, 'pending-retirement'::text, 'retired'::text])))+constraint	keiro_external_read_contracts.keiro_external_read_contracts_state_not_null	NOT NULL state+constraint	keiro_external_read_contracts.keiro_external_read_contracts_surface_generation_not_null	NOT NULL surface_generation+constraint	keiro_external_read_contracts.keiro_external_read_contracts_updated_at_not_null	NOT NULL updated_at constraint	keiro_inbox.keiro_inbox_attempt_count_not_null	NOT NULL attempt_count constraint	keiro_inbox.keiro_inbox_content_type_not_null	NOT NULL content_type constraint	keiro_inbox.keiro_inbox_dedupe_key_not_null	NOT NULL dedupe_key@@ -174,6 +348,23 @@ constraint	keiro_inbox.keiro_inbox_received_at_not_null	NOT NULL received_at constraint	keiro_inbox.keiro_inbox_source_not_null	NOT NULL source constraint	keiro_inbox.keiro_inbox_status_not_null	NOT NULL status+constraint	keiro_managed_read_objects.keiro_managed_read_objects_contract_id_contract_version_fkey	FOREIGN KEY (contract_id, contract_version) REFERENCES keiro.keiro_external_read_contracts(contract_id, contract_version)+constraint	keiro_managed_read_objects.keiro_managed_read_objects_created_at_not_null	NOT NULL created_at+constraint	keiro_managed_read_objects.keiro_managed_read_objects_definition_hash_not_null	NOT NULL definition_hash+constraint	keiro_managed_read_objects.keiro_managed_read_objects_identity_chk	CHECK (((length(object_schema) > 0) AND (length(object_name) > 0) AND (length(object_kind) > 0) AND (length(managed_by) > 0) AND (length(definition_hash) > 0) AND (surface_generation > 0) AND (((contract_id IS NULL) AND (contract_version IS NULL)) OR ((contract_id IS NOT NULL) AND (contract_version IS NOT NULL)))))+constraint	keiro_managed_read_objects.keiro_managed_read_objects_kind_chk	CHECK ((object_kind = ANY (ARRAY['guard-function'::text, 'binding-view'::text, 'wrapper-function'::text, 'contract-type'::text])))+constraint	keiro_managed_read_objects.keiro_managed_read_objects_managed_by_not_null	NOT NULL managed_by+constraint	keiro_managed_read_objects.keiro_managed_read_objects_object_kind_not_null	NOT NULL object_kind+constraint	keiro_managed_read_objects.keiro_managed_read_objects_object_name_not_null	NOT NULL object_name+constraint	keiro_managed_read_objects.keiro_managed_read_objects_object_schema_not_null	NOT NULL object_schema+constraint	keiro_managed_read_objects.keiro_managed_read_objects_object_signature_not_null	NOT NULL object_signature+constraint	keiro_managed_read_objects.keiro_managed_read_objects_owner_chk	CHECK ((managed_by = ANY (ARRAY['keiro'::text, 'consumer'::text])))+constraint	keiro_managed_read_objects.keiro_managed_read_objects_pkey	PRIMARY KEY (object_schema, object_name, object_kind, object_signature)+constraint	keiro_managed_read_objects.keiro_managed_read_objects_retired_chk	CHECK ((((state = 'retired'::text) AND (retired_at IS NOT NULL)) OR ((state <> 'retired'::text) AND (retired_at IS NULL))))+constraint	keiro_managed_read_objects.keiro_managed_read_objects_state_chk	CHECK ((state = ANY (ARRAY['active'::text, 'pending-retirement'::text, 'retired'::text])))+constraint	keiro_managed_read_objects.keiro_managed_read_objects_state_not_null	NOT NULL state+constraint	keiro_managed_read_objects.keiro_managed_read_objects_surface_generation_not_null	NOT NULL surface_generation+constraint	keiro_managed_read_objects.keiro_managed_read_objects_updated_at_not_null	NOT NULL updated_at constraint	keiro_outbox.keiro_outbox_attempt_count_not_null	NOT NULL attempt_count constraint	keiro_outbox.keiro_outbox_content_type_not_null	NOT NULL content_type constraint	keiro_outbox.keiro_outbox_created_at_not_null	NOT NULL created_at@@ -194,8 +385,148 @@ constraint	keiro_projection_dedup.keiro_projection_dedup_event_id_not_null	NOT NULL event_id constraint	keiro_projection_dedup.keiro_projection_dedup_pkey	PRIMARY KEY (projection_name, event_id) constraint	keiro_projection_dedup.keiro_projection_dedup_projection_name_not_null	NOT NULL projection_name+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_basis_chk	CHECK ((position_basis = ANY (ARRAY['append'::text, 'checkpoint'::text, 'unmanaged'::text])))+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_group_id_fkey	FOREIGN KEY (group_id) REFERENCES keiro.keiro_projection_rebuild_groups(group_id) ON DELETE CASCADE+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_group_id_not_null	NOT NULL group_id+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_pkey	PRIMARY KEY (group_id)+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_position_basis_not_null	NOT NULL position_basis+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_subscription_names_chk	CHECK (((array_position(subscription_names, NULL::text) IS NULL) AND (((position_basis = 'checkpoint'::text) AND (cardinality(subscription_names) > 0)) OR ((position_basis = ANY (ARRAY['append'::text, 'unmanaged'::text])) AND (cardinality(subscription_names) = 0)))))+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_subscription_names_not_null	NOT NULL subscription_names+constraint	keiro_projection_group_cursors.keiro_projection_group_cursors_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_adapter_order_not_null	NOT NULL adapter_order+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_apply_count_not_null	NOT NULL apply_count+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_completed_chk	CHECK (((completed_through IS NULL) OR (completed_through >= 0)))+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_counts_chk	CHECK (((evaluation_count >= 0) AND (apply_count >= 0) AND (apply_count <= evaluation_count)))+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_evaluation_count_not_null	NOT NULL evaluation_count+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_order_chk	CHECK ((adapter_order >= 0))+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_pkey	PRIMARY KEY (run_id, source_id, projection_id)+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_projection_id_not_null	NOT NULL projection_id+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_run_id_adapter_order_key	UNIQUE (run_id, adapter_order)+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_run_id_source_id_fkey	FOREIGN KEY (run_id, source_id) REFERENCES keiro.keiro_projection_rebuild_sources(run_id, source_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_source_id_not_null	NOT NULL source_id+constraint	keiro_projection_rebuild_adapters.keiro_projection_rebuild_adapters_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_event_id_not_null	NOT NULL event_id+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_global_position_not_null	NOT NULL global_position+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_names_chk	CHECK (((length(subscription_name) > 0) AND (length(projection_name) > 0)))+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_pkey	PRIMARY KEY (run_id, projection_name, event_id)+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_position_chk	CHECK ((global_position > 0))+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_projection_name_not_null	NOT NULL projection_name+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_run_id_fkey	FOREIGN KEY (run_id) REFERENCES keiro.keiro_projection_rebuild_runs(run_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_dedup_stage.keiro_projection_rebuild_dedup_stage_subscription_name_not_null	NOT NULL subscription_name+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_active_run_chk	CHECK ((((status = ANY (ARRAY['live'::text, 'serving-versioned'::text])) AND (active_run_id IS NULL)) OR ((status = ANY (ARRAY['rebuilding'::text, 'failed'::text, 'rebuilding-versioned'::text, 'cutover-versioned'::text, 'failed-versioned'::text])) AND (active_run_id IS NOT NULL))))+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_availability_chk	CHECK ((((status = 'live'::text) AND reads_allowed AND writes_allowed) OR ((status = ANY (ARRAY['rebuilding'::text, 'failed'::text])) AND (NOT reads_allowed) AND (NOT writes_allowed)) OR ((status = ANY (ARRAY['serving-versioned'::text, 'rebuilding-versioned'::text])) AND reads_allowed AND writes_allowed) OR ((status = ANY (ARRAY['cutover-versioned'::text, 'failed-versioned'::text])) AND reads_allowed AND (NOT writes_allowed))))+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_catalog_fingerprint_not_null	NOT NULL slice_fingerprint+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_created_at_not_null	NOT NULL created_at+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_group_id_not_null	NOT NULL group_id+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_pkey	PRIMARY KEY (group_id)+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_reads_allowed_not_null	NOT NULL reads_allowed+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_revision_phase_chk	CHECK ((((status = ANY (ARRAY['live'::text, 'rebuilding'::text, 'failed'::text])) AND (serving_revision_id IS NULL) AND (serving_epoch = 0)) OR ((status = ANY (ARRAY['serving-versioned'::text, 'rebuilding-versioned'::text, 'cutover-versioned'::text, 'failed-versioned'::text])) AND (serving_revision_id IS NOT NULL) AND (serving_epoch >= 0))))+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_serving_epoch_not_null	NOT NULL serving_epoch+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_serving_revision_fk	FOREIGN KEY (group_id, serving_revision_id) REFERENCES keiro.keiro_projection_revisions(group_id, revision_id)+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_status_chk	CHECK ((status = ANY (ARRAY['live'::text, 'rebuilding'::text, 'failed'::text, 'serving-versioned'::text, 'rebuilding-versioned'::text, 'cutover-versioned'::text, 'failed-versioned'::text])))+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_status_not_null	NOT NULL status+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_rebuild_groups.keiro_projection_rebuild_groups_writes_allowed_not_null	NOT NULL writes_allowed+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_prom_run_id_target_id_canonical_na_key	UNIQUE (run_id, target_id, canonical_name)+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_prom_run_id_target_id_generation_n_key	UNIQUE (run_id, target_id, generation_name)+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_obj_generation_name_not_null	NOT NULL generation_name+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_obje_canonical_name_not_null	NOT NULL canonical_name+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_object_object_order_not_null	NOT NULL object_order+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_object_run_id_target_id_fkey	FOREIGN KEY (run_id, target_id) REFERENCES keiro.keiro_projection_rebuild_run_targets(run_id, target_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_kind_chk	CHECK ((object_kind = ANY (ARRAY['index'::text, 'constraint'::text, 'owned-sequence'::text])))+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_names_chk	CHECK (((length(generation_name) > 0) AND (length(canonical_name) > 0)))+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_object_kind_not_null	NOT NULL object_kind+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_order_chk	CHECK ((object_order >= 0))+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_pkey	PRIMARY KEY (run_id, target_id, object_order)+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_promotion_objects.keiro_projection_rebuild_promotion_objects_target_id_not_null	NOT NULL target_id+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run__candidate_generation_id_run__fkey	FOREIGN KEY (candidate_generation_id, run_id, target_id) REFERENCES keiro.keiro_projection_target_generations(generation_id, created_by_run_id, target_id)+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_t_candidate_generation_id_not_null	NOT NULL candidate_generation_id+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_target_candidate_generation_id_key	UNIQUE (candidate_generation_id)+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_mode_chk	CHECK ((target_mode = ANY (ARRAY['application'::text, 'clone'::text])))+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_pkey	PRIMARY KEY (run_id, target_id)+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_run_id_fkey	FOREIGN KEY (run_id) REFERENCES keiro.keiro_projection_rebuild_runs(run_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_target_id_not_null	NOT NULL target_id+constraint	keiro_projection_rebuild_run_targets.keiro_projection_rebuild_run_targets_target_mode_not_null	NOT NULL target_mode+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_abandoned_chk	CHECK ((((status = 'abandoned'::text) AND (abandoned_at IS NOT NULL)) OR ((status <> 'abandoned'::text) AND (abandoned_at IS NULL))))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_candidate_revision_fk	FOREIGN KEY (group_id, candidate_revision_id) REFERENCES keiro.keiro_projection_revisions(group_id, revision_id)+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_captured_head_not_null	NOT NULL captured_head+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_catalog_fingerprint_not_null	NOT NULL catalog_fingerprint+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_contract_fingerprint_not_null	NOT NULL contract_fingerprint+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_failure_chk	CHECK ((((status = 'failed'::text) AND (failure_code IS NOT NULL) AND (failed_at IS NOT NULL)) OR ((status <> 'failed'::text) AND (failure_code IS NULL) AND (failed_at IS NULL))))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_group_id_fkey	FOREIGN KEY (group_id) REFERENCES keiro.keiro_projection_rebuild_groups(group_id)+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_group_id_not_null	NOT NULL group_id+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_group_slice_fingerprint_not_null	NOT NULL group_slice_fingerprint+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_head_chk	CHECK ((captured_head >= 0))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_mode_chk	CHECK ((((rebuild_mode = 'offline'::text) AND (candidate_revision_id IS NULL) AND (cutover_threshold IS NULL) AND (cutover_lock_timeout_ms IS NULL) AND (promotion_dedup_limit IS NULL) AND (dedup_provisional_head IS NULL) AND (promotion_prepared_at IS NULL) AND (history_retention_lease_id IS NULL) AND (history_retention_lease_owner IS NULL) AND (history_retention_protected_through IS NULL) AND (history_retention_expires_at IS NULL) AND (history_retention_renewed_at IS NULL) AND (history_retention_released_at IS NULL)) OR ((rebuild_mode = 'versioned'::text) AND (candidate_revision_id IS NOT NULL) AND (cutover_threshold IS NOT NULL) AND (cutover_threshold >= 0) AND (cutover_lock_timeout_ms IS NOT NULL) AND (cutover_lock_timeout_ms > 0) AND (promotion_dedup_limit IS NOT NULL) AND (promotion_dedup_limit > 0) AND ((dedup_provisional_head IS NULL) OR (dedup_provisional_head >= 0)) AND (history_retention_lease_id IS NOT NULL) AND (history_retention_lease_owner IS NOT NULL) AND (length(history_retention_lease_owner) > 0) AND (history_retention_protected_through IS NOT NULL) AND (history_retention_protected_through >= 0) AND (history_retention_expires_at IS NOT NULL) AND (history_retention_renewed_at IS NOT NULL))))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_page_size_chk	CHECK ((page_size > 0))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_page_size_not_null	NOT NULL page_size+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_pkey	PRIMARY KEY (run_id)+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_rebuild_mode_not_null	NOT NULL rebuild_mode+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_runner_format_not_null	NOT NULL runner_format+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_started_at_not_null	NOT NULL started_at+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_status_chk	CHECK ((status = ANY (ARRAY['running'::text, 'failed'::text, 'verified'::text, 'cutover'::text, 'promoted'::text, 'abandoned'::text])))+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_status_not_null	NOT NULL status+constraint	keiro_projection_rebuild_runs.keiro_projection_rebuild_runs_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_count_chk	CHECK ((event_count >= 0))+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_cursor_chk	CHECK (((cursor_position >= 0) AND (cursor_position <= target_position)))+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_cursor_position_not_null	NOT NULL cursor_position+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_event_count_not_null	NOT NULL event_count+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_exhausted_chk	CHECK (((exhausted_through IS NULL) OR (exhausted_through = target_position)))+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_pkey	PRIMARY KEY (run_id, source_id)+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_run_id_fkey	FOREIGN KEY (run_id) REFERENCES keiro.keiro_projection_rebuild_runs(run_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_scope_chk	CHECK ((((source_scope = 'all'::text) AND (category IS NULL)) OR ((source_scope = 'category'::text) AND (category IS NOT NULL))))+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_source_id_not_null	NOT NULL source_id+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_source_scope_not_null	NOT NULL source_scope+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_target_chk	CHECK ((target_position >= 0))+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_target_position_not_null	NOT NULL target_position+constraint	keiro_projection_rebuild_sources.keiro_projection_rebuild_sources_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifica_verification_version_not_null	NOT NULL verification_version+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_completion_chk	CHECK ((((status = 'pending'::text) AND (completed_at IS NULL)) OR ((status = ANY (ARRAY['passed'::text, 'failed'::text])) AND (completed_at IS NOT NULL))))+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_pkey	PRIMARY KEY (run_id, verification_id)+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_run_id_fkey	FOREIGN KEY (run_id) REFERENCES keiro.keiro_projection_rebuild_runs(run_id) ON DELETE CASCADE+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_run_id_not_null	NOT NULL run_id+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_status_chk	CHECK ((status = ANY (ARRAY['pending'::text, 'passed'::text, 'failed'::text])))+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_status_not_null	NOT NULL status+constraint	keiro_projection_rebuild_verifications.keiro_projection_rebuild_verifications_verification_id_not_null	NOT NULL verification_id+constraint	keiro_projection_revisions.keiro_projection_revisions_group_id_fkey	FOREIGN KEY (group_id) REFERENCES keiro.keiro_projection_rebuild_groups(group_id)+constraint	keiro_projection_revisions.keiro_projection_revisions_group_id_not_null	NOT NULL group_id+constraint	keiro_projection_revisions.keiro_projection_revisions_group_slice_fingerprint_not_null	NOT NULL group_slice_fingerprint+constraint	keiro_projection_revisions.keiro_projection_revisions_identity_chk	CHECK (((length(group_id) > 0) AND (length(revision_id) > 0) AND (length(group_slice_fingerprint) > 0)))+constraint	keiro_projection_revisions.keiro_projection_revisions_pkey	PRIMARY KEY (group_id, revision_id)+constraint	keiro_projection_revisions.keiro_projection_revisions_registered_at_not_null	NOT NULL registered_at+constraint	keiro_projection_revisions.keiro_projection_revisions_revision_id_key	UNIQUE (revision_id)+constraint	keiro_projection_revisions.keiro_projection_revisions_revision_id_not_null	NOT NULL revision_id+constraint	keiro_projection_revisions.keiro_projection_revisions_updated_at_not_null	NOT NULL updated_at+constraint	keiro_projection_target_generations.keiro_projection_target_gen_observed_shape_fingerprint_not_null	NOT NULL observed_shape_fingerprint+constraint	keiro_projection_target_generations.keiro_projection_target_gene_observed_catalog_snapshot_not_null	NOT NULL observed_catalog_snapshot+constraint	keiro_projection_target_generations.keiro_projection_target_gener_generation_id_created_by_run__key	UNIQUE (generation_id, created_by_run_id, target_id)+constraint	keiro_projection_target_generations.keiro_projection_target_generations_created_at_not_null	NOT NULL created_at+constraint	keiro_projection_target_generations.keiro_projection_target_generations_created_by_run_id_fkey	FOREIGN KEY (created_by_run_id) REFERENCES keiro.keiro_projection_rebuild_runs(run_id)+constraint	keiro_projection_target_generations.keiro_projection_target_generations_expected_shape_id_not_null	NOT NULL expected_shape_id+constraint	keiro_projection_target_generations.keiro_projection_target_generations_generation_id_not_null	NOT NULL generation_id+constraint	keiro_projection_target_generations.keiro_projection_target_generations_group_id_not_null	NOT NULL group_id+constraint	keiro_projection_target_generations.keiro_projection_target_generations_group_id_revision_id_fkey	FOREIGN KEY (group_id, revision_id) REFERENCES keiro.keiro_projection_revisions(group_id, revision_id)+constraint	keiro_projection_target_generations.keiro_projection_target_generations_identity_chk	CHECK (((length(target_id) > 0) AND (length(schema_name) > 0) AND (length(relation_name) > 0) AND (relation_oid > 0) AND (length(schema_version) > 0) AND (length(expected_shape_id) > 0) AND (length(observed_shape_fingerprint) > 0)))+constraint	keiro_projection_target_generations.keiro_projection_target_generations_lifecycle_chk	CHECK ((lifecycle = ANY (ARRAY['staging'::text, 'serving'::text, 'retired'::text, 'dropped'::text])))+constraint	keiro_projection_target_generations.keiro_projection_target_generations_lifecycle_not_null	NOT NULL lifecycle+constraint	keiro_projection_target_generations.keiro_projection_target_generations_pkey	PRIMARY KEY (generation_id)+constraint	keiro_projection_target_generations.keiro_projection_target_generations_relation_name_not_null	NOT NULL relation_name+constraint	keiro_projection_target_generations.keiro_projection_target_generations_relation_oid_not_null	NOT NULL relation_oid+constraint	keiro_projection_target_generations.keiro_projection_target_generations_revision_id_not_null	NOT NULL revision_id+constraint	keiro_projection_target_generations.keiro_projection_target_generations_schema_name_not_null	NOT NULL schema_name+constraint	keiro_projection_target_generations.keiro_projection_target_generations_schema_version_not_null	NOT NULL schema_version+constraint	keiro_projection_target_generations.keiro_projection_target_generations_target_id_not_null	NOT NULL target_id+constraint	keiro_projection_target_generations.keiro_projection_target_generations_transition_evidence_chk	CHECK ((((lifecycle = 'staging'::text) AND (created_by_run_id IS NOT NULL) AND (served_at IS NULL) AND (retired_at IS NULL) AND (dropped_at IS NULL)) OR ((lifecycle = 'serving'::text) AND (served_at IS NOT NULL) AND (retired_at IS NULL) AND (dropped_at IS NULL)) OR ((lifecycle = 'retired'::text) AND (served_at IS NOT NULL) AND (retired_at IS NOT NULL) AND (dropped_at IS NULL)) OR ((lifecycle = 'dropped'::text) AND (dropped_at IS NOT NULL) AND (((served_at IS NULL) AND (retired_at IS NULL)) OR ((served_at IS NOT NULL) AND (retired_at IS NOT NULL)))))) constraint	keiro_read_models.keiro_read_models_name_not_null	NOT NULL name constraint	keiro_read_models.keiro_read_models_pkey	PRIMARY KEY (name)+constraint	keiro_read_models.keiro_read_models_rebuild_group_fk	FOREIGN KEY (rebuild_group_id) REFERENCES keiro.keiro_projection_rebuild_groups(group_id)+constraint	keiro_read_models.keiro_read_models_rebuild_group_id_not_null	NOT NULL rebuild_group_id constraint	keiro_read_models.keiro_read_models_shape_hash_not_null	NOT NULL shape_hash constraint	keiro_read_models.keiro_read_models_status_not_null	NOT NULL status constraint	keiro_read_models.keiro_read_models_updated_at_not_null	NOT NULL updated_at@@ -258,9 +589,14 @@ index	keiro_dead_letters_dispatcher_created_at_idx	CREATE INDEX keiro_dead_letters_dispatcher_created_at_idx ON keiro.keiro_dead_letters USING btree (dispatcher_name, created_at DESC) index	keiro_dead_letters_dispatcher_name_source_event_id_emit_ind_key	CREATE UNIQUE INDEX keiro_dead_letters_dispatcher_name_source_event_id_emit_ind_key ON keiro.keiro_dead_letters USING btree (dispatcher_name, source_event_id, emit_index) index	keiro_dead_letters_pkey	CREATE UNIQUE INDEX keiro_dead_letters_pkey ON keiro.keiro_dead_letters USING btree (dead_letter_id)+index	keiro_external_read_contracts_group_idx	CREATE INDEX keiro_external_read_contracts_group_idx ON keiro.keiro_external_read_contracts USING btree (group_id, state)+index	keiro_external_read_contracts_pkey	CREATE UNIQUE INDEX keiro_external_read_contracts_pkey ON keiro.keiro_external_read_contracts USING btree (contract_id, contract_version)+index	keiro_external_read_contracts_public_function_name_argument_key	CREATE UNIQUE INDEX keiro_external_read_contracts_public_function_name_argument_key ON keiro.keiro_external_read_contracts USING btree (public_function_name, argument_types) index	keiro_inbox_backlog_idx	CREATE INDEX keiro_inbox_backlog_idx ON keiro.keiro_inbox USING btree (status) WHERE (status = ANY (ARRAY['processing'::text, 'failed'::text])) index	keiro_inbox_completed_idx	CREATE INDEX keiro_inbox_completed_idx ON keiro.keiro_inbox USING btree (completed_at) WHERE (status = 'completed'::text) index	keiro_inbox_pkey	CREATE UNIQUE INDEX keiro_inbox_pkey ON keiro.keiro_inbox USING btree (source, dedupe_key)+index	keiro_managed_read_objects_contract_idx	CREATE INDEX keiro_managed_read_objects_contract_idx ON keiro.keiro_managed_read_objects USING btree (contract_id, contract_version)+index	keiro_managed_read_objects_pkey	CREATE UNIQUE INDEX keiro_managed_read_objects_pkey ON keiro.keiro_managed_read_objects USING btree (object_schema, object_name, object_kind, object_signature) index	keiro_outbox_claim_order_idx	CREATE INDEX keiro_outbox_claim_order_idx ON keiro.keiro_outbox USING btree (created_at, outbox_id) WHERE (status = ANY (ARRAY['pending'::text, 'failed'::text])) index	keiro_outbox_head_of_line_idx	CREATE INDEX keiro_outbox_head_of_line_idx ON keiro.keiro_outbox USING btree (source, message_key, created_at) WHERE ((status <> ALL (ARRAY['sent'::text, 'dead'::text])) AND (message_key IS NOT NULL)) index	keiro_outbox_pending_idx	CREATE INDEX keiro_outbox_pending_idx ON keiro.keiro_outbox USING btree (status, next_attempt_at, created_at)@@ -270,7 +606,29 @@ index	keiro_outbox_source_order_idx	CREATE INDEX keiro_outbox_source_order_idx ON keiro.keiro_outbox USING btree (source, created_at, outbox_id) WHERE (status <> ALL (ARRAY['sent'::text, 'dead'::text])) index	keiro_projection_dedup_applied_at_idx	CREATE INDEX keiro_projection_dedup_applied_at_idx ON keiro.keiro_projection_dedup USING btree (applied_at) index	keiro_projection_dedup_pkey	CREATE UNIQUE INDEX keiro_projection_dedup_pkey ON keiro.keiro_projection_dedup USING btree (projection_name, event_id)+index	keiro_projection_group_cursors_pkey	CREATE UNIQUE INDEX keiro_projection_group_cursors_pkey ON keiro.keiro_projection_group_cursors USING btree (group_id)+index	keiro_projection_rebuild_adapters_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_adapters_pkey ON keiro.keiro_projection_rebuild_adapters USING btree (run_id, source_id, projection_id)+index	keiro_projection_rebuild_adapters_run_id_adapter_order_key	CREATE UNIQUE INDEX keiro_projection_rebuild_adapters_run_id_adapter_order_key ON keiro.keiro_projection_rebuild_adapters USING btree (run_id, adapter_order)+index	keiro_projection_rebuild_dedup_stage_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_dedup_stage_pkey ON keiro.keiro_projection_rebuild_dedup_stage USING btree (run_id, projection_name, event_id)+index	keiro_projection_rebuild_dedup_stage_run_position_idx	CREATE INDEX keiro_projection_rebuild_dedup_stage_run_position_idx ON keiro.keiro_projection_rebuild_dedup_stage USING btree (run_id, subscription_name, global_position)+index	keiro_projection_rebuild_groups_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_groups_pkey ON keiro.keiro_projection_rebuild_groups USING btree (group_id)+index	keiro_projection_rebuild_prom_run_id_target_id_canonical_na_key	CREATE UNIQUE INDEX keiro_projection_rebuild_prom_run_id_target_id_canonical_na_key ON keiro.keiro_projection_rebuild_promotion_objects USING btree (run_id, target_id, canonical_name)+index	keiro_projection_rebuild_prom_run_id_target_id_generation_n_key	CREATE UNIQUE INDEX keiro_projection_rebuild_prom_run_id_target_id_generation_n_key ON keiro.keiro_projection_rebuild_promotion_objects USING btree (run_id, target_id, generation_name)+index	keiro_projection_rebuild_promotion_objects_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_promotion_objects_pkey ON keiro.keiro_projection_rebuild_promotion_objects USING btree (run_id, target_id, object_order)+index	keiro_projection_rebuild_run_target_candidate_generation_id_key	CREATE UNIQUE INDEX keiro_projection_rebuild_run_target_candidate_generation_id_key ON keiro.keiro_projection_rebuild_run_targets USING btree (candidate_generation_id)+index	keiro_projection_rebuild_run_targets_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_run_targets_pkey ON keiro.keiro_projection_rebuild_run_targets USING btree (run_id, target_id)+index	keiro_projection_rebuild_runs_group_idx	CREATE INDEX keiro_projection_rebuild_runs_group_idx ON keiro.keiro_projection_rebuild_runs USING btree (group_id, started_at DESC)+index	keiro_projection_rebuild_runs_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_runs_pkey ON keiro.keiro_projection_rebuild_runs USING btree (run_id)+index	keiro_projection_rebuild_sources_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_sources_pkey ON keiro.keiro_projection_rebuild_sources USING btree (run_id, source_id)+index	keiro_projection_rebuild_verifications_pkey	CREATE UNIQUE INDEX keiro_projection_rebuild_verifications_pkey ON keiro.keiro_projection_rebuild_verifications USING btree (run_id, verification_id)+index	keiro_projection_revisions_pkey	CREATE UNIQUE INDEX keiro_projection_revisions_pkey ON keiro.keiro_projection_revisions USING btree (group_id, revision_id)+index	keiro_projection_revisions_revision_id_key	CREATE UNIQUE INDEX keiro_projection_revisions_revision_id_key ON keiro.keiro_projection_revisions USING btree (revision_id)+index	keiro_projection_target_gener_generation_id_created_by_run__key	CREATE UNIQUE INDEX keiro_projection_target_gener_generation_id_created_by_run__key ON keiro.keiro_projection_target_generations USING btree (generation_id, created_by_run_id, target_id)+index	keiro_projection_target_generations_pkey	CREATE UNIQUE INDEX keiro_projection_target_generations_pkey ON keiro.keiro_projection_target_generations USING btree (generation_id)+index	keiro_projection_target_generations_relation_idx	CREATE UNIQUE INDEX keiro_projection_target_generations_relation_idx ON keiro.keiro_projection_target_generations USING btree (schema_name, relation_name) WHERE (lifecycle <> 'dropped'::text)+index	keiro_projection_target_generations_serving_idx	CREATE UNIQUE INDEX keiro_projection_target_generations_serving_idx ON keiro.keiro_projection_target_generations USING btree (group_id, target_id) WHERE (lifecycle = 'serving'::text) index	keiro_read_models_pkey	CREATE UNIQUE INDEX keiro_read_models_pkey ON keiro.keiro_read_models USING btree (name)+index	keiro_read_models_rebuild_group_idx	CREATE INDEX keiro_read_models_rebuild_group_idx ON keiro.keiro_read_models USING btree (rebuild_group_id) index	keiro_snapshots_compat_idx	CREATE INDEX keiro_snapshots_compat_idx ON keiro.keiro_snapshots USING btree (stream_id, state_codec_version, regfile_shape_hash, stream_version DESC) index	keiro_snapshots_pkey	CREATE UNIQUE INDEX keiro_snapshots_pkey ON keiro.keiro_snapshots USING btree (stream_id) index	keiro_subscription_shards_lease_idx	CREATE INDEX keiro_subscription_shards_lease_idx ON keiro.keiro_subscription_shards USING btree (subscription_name, lease_expires_at)@@ -283,14 +641,27 @@ index	keiro_workflow_children_running_idx	CREATE INDEX keiro_workflow_children_running_idx ON keiro.keiro_workflow_children USING btree (status) WHERE (status = 'running'::text) index	keiro_workflow_steps_pkey	CREATE UNIQUE INDEX keiro_workflow_steps_pkey ON keiro.keiro_workflow_steps USING btree (workflow_id, workflow_name, generation, step_name) index	keiro_workflow_steps_workflow_idx	CREATE INDEX keiro_workflow_steps_workflow_idx ON keiro.keiro_workflow_steps USING btree (workflow_id, workflow_name, generation)-index	keiro_workflows_active_idx	CREATE INDEX keiro_workflows_active_idx ON keiro.keiro_workflows USING btree (status) WHERE (status = ANY (ARRAY['running'::text, 'suspended'::text]))+index	keiro_workflows_active_idx	CREATE INDEX keiro_workflows_active_idx ON keiro.keiro_workflows USING btree (status, wake_after) WHERE (status = ANY (ARRAY['running'::text, 'suspended'::text])) index	keiro_workflows_gc_idx	CREATE INDEX keiro_workflows_gc_idx ON keiro.keiro_workflows USING btree (status, completed_at) index	keiro_workflows_pkey	CREATE UNIQUE INDEX keiro_workflows_pkey ON keiro.keiro_workflows USING btree (workflow_id, workflow_name) table	keiro_awakeables	kind=r table	keiro_dead_letters	kind=r+table	keiro_external_read_contracts	kind=r table	keiro_inbox	kind=r+table	keiro_managed_read_objects	kind=r table	keiro_outbox	kind=r table	keiro_projection_dedup	kind=r+table	keiro_projection_group_cursors	kind=r+table	keiro_projection_rebuild_adapters	kind=r+table	keiro_projection_rebuild_dedup_stage	kind=r+table	keiro_projection_rebuild_groups	kind=r+table	keiro_projection_rebuild_promotion_objects	kind=r+table	keiro_projection_rebuild_run_targets	kind=r+table	keiro_projection_rebuild_runs	kind=r+table	keiro_projection_rebuild_sources	kind=r+table	keiro_projection_rebuild_verifications	kind=r+table	keiro_projection_revisions	kind=r+table	keiro_projection_target_generations	kind=r table	keiro_read_models	kind=r table	keiro_snapshots	kind=r table	keiro_subscription_shards	kind=r@@ -298,3 +669,4 @@ table	keiro_workflow_children	kind=r table	keiro_workflow_steps	kind=r table	keiro_workflows	kind=r+view	keiro_read.projection_group_status_v1	columns=1:group_id text,2:lifecycle_phase text,3:reads_allowed boolean,4:writes_allowed boolean,5:serving_revision_id text,6:serving_epoch bigint,7:serving_position_basis text,8:serving_applied_position bigint,9:active_run_id text,10:candidate_revision_id text,11:candidate_rebuild_position bigint,12:candidate_rebuild_head bigint,13:query_models text[],14:rebuild_started_at timestamp with time zone,15:last_promoted_at timestamp with time zone,16:failed_at timestamp with time zone,17:failure_code text,18:failure_detail text
keiro-migrations.cabal view
@@ -1,12 +1,13 @@ cabal-version:      3.0 name:               keiro-migrations-version:            0.11.0.0+version:            0.12.0.0 synopsis:           Schema migrations for keiro description:   Embedded PostgreSQL schema migrations and a migration runner for the Keiro   event-sourcing framework.  license:            BSD-3-Clause+license-file:       LICENSE author:             Nadeem Bitar maintainer:         nadeem@gmail.com copyright:          2026 Nadeem Bitar@@ -332,7 +333,7 @@     , bytestring               >=0.11     && <0.13     , containers               >=0.6      && <0.8     , hasql                    >=1.10     && <1.11-    , kiroku-store-migrations  ^>=0.3.0.0+    , kiroku-store-migrations  ^>=0.3.2.0     , pg-migrate               ^>=1.1.0.0     , pg-migrate-embed         ^>=1.1.0.0     , pg-migrate-import-codd   ^>=1.1.0.0@@ -364,7 +365,7 @@     , bytestring               >=0.11     && <0.13     , hasql                    >=1.10     && <1.11     , keiro-migrations-    , kiroku-store-migrations  ^>=0.3.0.0+    , kiroku-store-migrations  ^>=0.3.2.0     , optparse-applicative     >=0.17     && <0.20     , pg-migrate               ^>=1.1.0.0     , pg-migrate-cli           ^>=1.1.0.0@@ -404,7 +405,7 @@     , hasql                    >=1.10     && <1.11     , hspec                    >=2.10     && <2.12     , keiro-migrations-    , kiroku-store-migrations  ^>=0.3.0.0+    , kiroku-store-migrations  ^>=0.3.2.0     , pg-migrate               ^>=1.1.0.0     , pg-migrate-import-codd   ^>=1.1.0.0     , pg-migrate-test-support  ^>=1.1.0.0@@ -437,8 +438,8 @@     , hasql-pool               >=1.2      && <1.5     , hspec                    >=2.10     , keiro-migrations-    , kiroku-store             >=0.3      && <0.4-    , kiroku-store-migrations  ^>=0.3.0.0+    , kiroku-store             >=0.7      && <0.8+    , kiroku-store-migrations  ^>=0.3.2.0     , lens                     >=5.2      && <5.4     , temporary     , text                     >=2.0      && <2.2
migrations.native.lock view
@@ -18,3 +18,13 @@ be7e510421f983ac2e446ae655d000b810b19cf2667f328dd7caf41a2101d0b6  0018.sql 7a0d0280d8d54f811c33b4bec525d78fe8f172de92b62e4745dcdd217c656209  0019-keiro-snapshots-state-shape-hash.sql c125e8c32a0848854d94bce1245f349cf7f479326a1bf540e9dba9e222b75a4f  0020-keiro-workflow-children-failure-reason.sql+fa59bb3930e541b55b92f22b4662b800f560a48c65af9edbb43089b33ba43cb2  0021-keiro-workflows-exact-discovery.sql+f06ed6b5a3de431f3f9de55f7776eb86549432317bc728ef848fd246db67c0ce  0022.sql+04cbd376eb9509cb202ec0caa43e6646957d10c241511829b225d072cfd92641  0023.sql+faad6bd740cda45378645be1d3fe0b1754c4f1e9ac6a02e9e935bb5f6e304f95  0024.sql+343f0852de6cddf85ab7a69bbba7d07396427877a78b0f7f97f9835d86c4deaf  0025.sql+cc167230c3765bd2de986cc1dc0db447d4f186a3e6b2dbbdac7ea2cabe6cb2b3  0026.sql+bb725945d567c9446e67db65535daf2017dd1b6facb3277c2b85e7429b4e95f7  0027.sql+524e90271704112722bbfe0fc54f1c8dcc06b1f9e2ce1821e54dc232d434104f  0028.sql+40f62a18adf0457fc6d35e880eca8be1922f22a2fd8c893c13d59537fe941719  0029.sql+0e0b847088d178290e77913224df82435b86c00062feb47c79351ed014a605a1  0030.sql
+ migrations/0021-keiro-workflows-exact-discovery.sql view
@@ -0,0 +1,21 @@+-- Exact discovery: the resume worker now returns a workflow only when its+-- instance row says it has progress to make — status 'running', or status+-- 'suspended' with a due wake_after hint. The status-only active index cannot+-- serve the second arm, so replace it with one that also carries wake_after.+-- The index predicate is unchanged, so the swap is transparent to every other+-- reader of the active set.+DROP INDEX IF EXISTS keiro.keiro_workflows_active_idx;+CREATE INDEX IF NOT EXISTS keiro_workflows_active_idx+  ON keiro.keiro_workflows (status, wake_after)+  WHERE status IN ('running', 'suspended');++-- Legacy suspensions predate exact discovery: before this change a 'suspended'+-- row was re-examined on every pass, so a workflow could be parked on a promise+-- that was already cancelled, or on a wake whose row-lifecycle transition was+-- never reflected in the instance row, and nothing would ever flip it again.+-- Return every suspended instance to the runnable pool once. The next pass+-- re-examines each and re-suspends it through the new suspend/wake arbitration,+-- which is what makes the row's status trustworthy from then on. The cost is one+-- extra replay per legacy suspended instance, once.+UPDATE keiro.keiro_workflows SET status = 'running', updated_at = now()+WHERE status = 'suspended';
+ migrations/0022.sql view
@@ -0,0 +1,80 @@+-- projection rebuild groups++CREATE TABLE IF NOT EXISTS keiro.keiro_projection_rebuild_groups (+  group_id TEXT PRIMARY KEY,+  catalog_fingerprint TEXT NOT NULL,+  status TEXT NOT NULL,+  active_run_id TEXT,+  requested_by TEXT,+  request_reason TEXT,+  started_at TIMESTAMPTZ,+  completed_at TIMESTAMPTZ,+  failed_at TIMESTAMPTZ,+  failure_code TEXT,+  failure_detail TEXT,+  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  CONSTRAINT keiro_projection_rebuild_groups_status_chk+    CHECK (status IN ('live', 'rebuilding', 'failed')),+  CONSTRAINT keiro_projection_rebuild_groups_active_run_chk+    CHECK (+      (status = 'live' AND active_run_id IS NULL)+      OR (status IN ('rebuilding', 'failed') AND active_run_id IS NOT NULL)+    )+);++ALTER TABLE keiro.keiro_read_models+  ADD COLUMN IF NOT EXISTS rebuild_group_id TEXT;++-- Every pre-catalog read model becomes a deterministic singleton group. A+-- non-live legacy state stays fenced: rebuilding remains rebuilding, while+-- paused, abandoned, and unknown values become failed with the raw state in+-- failure_detail. The catalog registration path may later adopt a matching live+-- singleton into an explicitly named catalog group.+INSERT INTO keiro.keiro_projection_rebuild_groups (+  group_id,+  catalog_fingerprint,+  status,+  active_run_id,+  started_at,+  failed_at,+  failure_code,+  failure_detail,+  created_at,+  updated_at+)+SELECT+  '$legacy-read-model:' || name,+  '$legacy-unmanaged',+  CASE status+    WHEN 'live' THEN 'live'+    WHEN 'rebuilding' THEN 'rebuilding'+    ELSE 'failed'+  END,+  CASE+    WHEN status = 'live' THEN NULL+    ELSE '$legacy-read-model:' || name+  END,+  CASE WHEN status = 'rebuilding' THEN updated_at ELSE NULL END,+  CASE WHEN status NOT IN ('live', 'rebuilding') THEN updated_at ELSE NULL END,+  CASE WHEN status NOT IN ('live', 'rebuilding') THEN 'legacy-read-model-state' ELSE NULL END,+  CASE WHEN status NOT IN ('live', 'rebuilding') THEN status ELSE NULL END,+  updated_at,+  updated_at+FROM keiro.keiro_read_models+ON CONFLICT (group_id) DO NOTHING;++UPDATE keiro.keiro_read_models+SET rebuild_group_id = '$legacy-read-model:' || name+WHERE rebuild_group_id IS NULL;++ALTER TABLE keiro.keiro_read_models+  ALTER COLUMN rebuild_group_id SET NOT NULL;++ALTER TABLE keiro.keiro_read_models+  ADD CONSTRAINT keiro_read_models_rebuild_group_fk+  FOREIGN KEY (rebuild_group_id)+  REFERENCES keiro.keiro_projection_rebuild_groups (group_id);++CREATE INDEX IF NOT EXISTS keiro_read_models_rebuild_group_idx+  ON keiro.keiro_read_models (rebuild_group_id);
+ migrations/0023.sql view
@@ -0,0 +1,104 @@+-- projection replay progress++CREATE TABLE keiro.keiro_projection_rebuild_runs (+  run_id TEXT PRIMARY KEY,+  group_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_groups (group_id),+  catalog_fingerprint TEXT NOT NULL,+  contract_fingerprint TEXT NOT NULL,+  runner_format TEXT NOT NULL,+  captured_head BIGINT NOT NULL,+  page_size INTEGER NOT NULL,+  status TEXT NOT NULL DEFAULT 'running',+  started_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  failed_at TIMESTAMPTZ,+  verified_at TIMESTAMPTZ,+  promoted_at TIMESTAMPTZ,+  failure_code TEXT,+  failure_detail TEXT,+  failure_source_id TEXT,+  failure_projection_id TEXT,+  failure_position BIGINT,+  CONSTRAINT keiro_projection_rebuild_runs_status_chk+    CHECK (status IN ('running', 'failed', 'verified', 'promoted')),+  CONSTRAINT keiro_projection_rebuild_runs_head_chk+    CHECK (captured_head >= 0),+  CONSTRAINT keiro_projection_rebuild_runs_page_size_chk+    CHECK (page_size > 0),+  CONSTRAINT keiro_projection_rebuild_runs_failure_chk+    CHECK (+      (status = 'failed' AND failure_code IS NOT NULL AND failed_at IS NOT NULL)+      OR (status <> 'failed' AND failure_code IS NULL AND failed_at IS NULL)+    )+);++CREATE TABLE keiro.keiro_projection_rebuild_sources (+  run_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_runs (run_id) ON DELETE CASCADE,+  source_id TEXT NOT NULL,+  source_scope TEXT NOT NULL,+  category TEXT,+  cursor_position BIGINT NOT NULL DEFAULT 0,+  target_position BIGINT NOT NULL,+  exhausted_through BIGINT,+  event_count BIGINT NOT NULL DEFAULT 0,+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  PRIMARY KEY (run_id, source_id),+  CONSTRAINT keiro_projection_rebuild_sources_scope_chk+    CHECK (+      (source_scope = 'all' AND category IS NULL)+      OR (source_scope = 'category' AND category IS NOT NULL)+    ),+  CONSTRAINT keiro_projection_rebuild_sources_cursor_chk+    CHECK (cursor_position >= 0 AND cursor_position <= target_position),+  CONSTRAINT keiro_projection_rebuild_sources_target_chk+    CHECK (target_position >= 0),+  CONSTRAINT keiro_projection_rebuild_sources_exhausted_chk+    CHECK (exhausted_through IS NULL OR exhausted_through = target_position),+  CONSTRAINT keiro_projection_rebuild_sources_count_chk+    CHECK (event_count >= 0)+);++CREATE TABLE keiro.keiro_projection_rebuild_adapters (+  run_id TEXT NOT NULL,+  source_id TEXT NOT NULL,+  projection_id TEXT NOT NULL,+  adapter_order INTEGER NOT NULL,+  evaluation_count BIGINT NOT NULL DEFAULT 0,+  apply_count BIGINT NOT NULL DEFAULT 0,+  completed_through BIGINT,+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  PRIMARY KEY (run_id, source_id, projection_id),+  UNIQUE (run_id, adapter_order),+  FOREIGN KEY (run_id, source_id)+    REFERENCES keiro.keiro_projection_rebuild_sources (run_id, source_id)+    ON DELETE CASCADE,+  CONSTRAINT keiro_projection_rebuild_adapters_order_chk+    CHECK (adapter_order >= 0),+  CONSTRAINT keiro_projection_rebuild_adapters_counts_chk+    CHECK (evaluation_count >= 0 AND apply_count >= 0 AND apply_count <= evaluation_count),+  CONSTRAINT keiro_projection_rebuild_adapters_completed_chk+    CHECK (completed_through IS NULL OR completed_through >= 0)+);++CREATE TABLE keiro.keiro_projection_rebuild_verifications (+  run_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_runs (run_id) ON DELETE CASCADE,+  verification_id TEXT NOT NULL,+  verification_version TEXT NOT NULL,+  status TEXT NOT NULL DEFAULT 'pending',+  detail TEXT,+  completed_at TIMESTAMPTZ,+  PRIMARY KEY (run_id, verification_id),+  CONSTRAINT keiro_projection_rebuild_verifications_status_chk+    CHECK (status IN ('pending', 'passed', 'failed')),+  CONSTRAINT keiro_projection_rebuild_verifications_completion_chk+    CHECK (+      (status = 'pending' AND completed_at IS NULL)+      OR (status IN ('passed', 'failed') AND completed_at IS NOT NULL)+    )+);++CREATE INDEX keiro_projection_rebuild_runs_group_idx+  ON keiro.keiro_projection_rebuild_runs (group_id, started_at DESC);
+ migrations/0024.sql view
@@ -0,0 +1,13 @@+-- projection rebuild slice fingerprints++-- Group identity becomes the group's own catalog slice; runs carry the slice+-- they were begun under for epoch-consistency joins. The run catalog+-- fingerprint remains begin-stamped provenance.+ALTER TABLE keiro.keiro_projection_rebuild_groups+  RENAME COLUMN catalog_fingerprint TO slice_fingerprint;++ALTER TABLE keiro.keiro_projection_rebuild_runs+  ADD COLUMN group_slice_fingerprint TEXT NOT NULL DEFAULT '$pre-canonical';++ALTER TABLE keiro.keiro_projection_rebuild_runs+  ALTER COLUMN group_slice_fingerprint DROP DEFAULT;
+ migrations/0025.sql view
@@ -0,0 +1,266 @@+-- schema-versioned projection rebuild lifecycle++-- The existing status column remains the lifecycle phase so a pre-feature+-- runtime sees every version-managed value as unknown and fails closed.+ALTER TABLE keiro.keiro_projection_rebuild_groups+  DROP CONSTRAINT keiro_projection_rebuild_groups_status_chk,+  DROP CONSTRAINT keiro_projection_rebuild_groups_active_run_chk,+  ADD COLUMN serving_revision_id TEXT,+  ADD COLUMN serving_epoch BIGINT NOT NULL DEFAULT 0,+  ADD COLUMN reads_allowed BOOLEAN NOT NULL DEFAULT TRUE,+  ADD COLUMN writes_allowed BOOLEAN NOT NULL DEFAULT TRUE;++UPDATE keiro.keiro_projection_rebuild_groups+SET reads_allowed = (status = 'live'),+    writes_allowed = (status = 'live');++ALTER TABLE keiro.keiro_projection_rebuild_groups+  ADD CONSTRAINT keiro_projection_rebuild_groups_status_chk+    CHECK (+      status IN (+        'live',+        'rebuilding',+        'failed',+        'serving-versioned',+        'rebuilding-versioned',+        'cutover-versioned',+        'failed-versioned'+      )+    ),+  ADD CONSTRAINT keiro_projection_rebuild_groups_active_run_chk+    CHECK (+      (status IN ('live', 'serving-versioned') AND active_run_id IS NULL)+      OR (+        status IN (+          'rebuilding',+          'failed',+          'rebuilding-versioned',+          'cutover-versioned',+          'failed-versioned'+        )+        AND active_run_id IS NOT NULL+      )+    ),+  ADD CONSTRAINT keiro_projection_rebuild_groups_revision_phase_chk+    CHECK (+      (+        status IN ('live', 'rebuilding', 'failed')+        AND serving_revision_id IS NULL+        AND serving_epoch = 0+      )+      OR (+        status IN (+          'serving-versioned',+          'rebuilding-versioned',+          'cutover-versioned',+          'failed-versioned'+        )+        AND serving_revision_id IS NOT NULL+        AND serving_epoch >= 0+      )+    ),+  ADD CONSTRAINT keiro_projection_rebuild_groups_availability_chk+    CHECK (+      (status = 'live' AND reads_allowed AND writes_allowed)+      OR (status IN ('rebuilding', 'failed') AND NOT reads_allowed AND NOT writes_allowed)+      OR (status IN ('serving-versioned', 'rebuilding-versioned') AND reads_allowed AND writes_allowed)+      OR (status IN ('cutover-versioned', 'failed-versioned') AND reads_allowed AND NOT writes_allowed)+    );++CREATE TABLE keiro.keiro_projection_revisions (+  group_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_groups (group_id),+  revision_id TEXT NOT NULL,+  group_slice_fingerprint TEXT NOT NULL,+  registered_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  PRIMARY KEY (group_id, revision_id),+  UNIQUE (revision_id),+  CONSTRAINT keiro_projection_revisions_identity_chk+    CHECK (+      length(group_id) > 0+      AND length(revision_id) > 0+      AND length(group_slice_fingerprint) > 0+    )+);++ALTER TABLE keiro.keiro_projection_rebuild_groups+  ADD CONSTRAINT keiro_projection_rebuild_groups_serving_revision_fk+  FOREIGN KEY (group_id, serving_revision_id)+  REFERENCES keiro.keiro_projection_revisions (group_id, revision_id);++ALTER TABLE keiro.keiro_projection_rebuild_runs+  DROP CONSTRAINT keiro_projection_rebuild_runs_status_chk,+  DROP CONSTRAINT keiro_projection_rebuild_runs_failure_chk,+  ADD COLUMN rebuild_mode TEXT NOT NULL DEFAULT 'offline',+  ADD COLUMN candidate_revision_id TEXT,+  ADD COLUMN cutover_threshold BIGINT,+  ADD COLUMN cutover_lock_timeout_ms BIGINT,+  ADD COLUMN history_retention_lease_id UUID,+  ADD COLUMN history_retention_lease_owner TEXT,+  ADD COLUMN history_retention_protected_through BIGINT,+  ADD COLUMN history_retention_expires_at TIMESTAMPTZ,+  ADD COLUMN history_retention_renewed_at TIMESTAMPTZ,+  ADD COLUMN history_retention_released_at TIMESTAMPTZ,+  ADD COLUMN abandoned_at TIMESTAMPTZ,+  ADD CONSTRAINT keiro_projection_rebuild_runs_status_chk+    CHECK (status IN ('running', 'failed', 'verified', 'cutover', 'promoted', 'abandoned')),+  ADD CONSTRAINT keiro_projection_rebuild_runs_failure_chk+    CHECK (+      (status = 'failed' AND failure_code IS NOT NULL AND failed_at IS NOT NULL)+      OR (status <> 'failed' AND failure_code IS NULL AND failed_at IS NULL)+    ),+  ADD CONSTRAINT keiro_projection_rebuild_runs_abandoned_chk+    CHECK (+      (status = 'abandoned' AND abandoned_at IS NOT NULL)+      OR (status <> 'abandoned' AND abandoned_at IS NULL)+    ),+  ADD CONSTRAINT keiro_projection_rebuild_runs_mode_chk+    CHECK (+      (+        rebuild_mode = 'offline'+        AND candidate_revision_id IS NULL+        AND cutover_threshold IS NULL+        AND cutover_lock_timeout_ms IS NULL+        AND history_retention_lease_id IS NULL+        AND history_retention_lease_owner IS NULL+        AND history_retention_protected_through IS NULL+        AND history_retention_expires_at IS NULL+        AND history_retention_renewed_at IS NULL+        AND history_retention_released_at IS NULL+      )+      OR (+        rebuild_mode = 'versioned'+        AND candidate_revision_id IS NOT NULL+        AND cutover_threshold IS NOT NULL+        AND cutover_threshold >= 0+        AND cutover_lock_timeout_ms IS NOT NULL+        AND cutover_lock_timeout_ms > 0+        AND history_retention_lease_id IS NOT NULL+        AND history_retention_lease_owner IS NOT NULL+        AND length(history_retention_lease_owner) > 0+        AND history_retention_protected_through IS NOT NULL+        AND history_retention_protected_through >= 0+        AND history_retention_expires_at IS NOT NULL+        AND history_retention_renewed_at IS NOT NULL+      )+    ),+  ADD CONSTRAINT keiro_projection_rebuild_runs_candidate_revision_fk+    FOREIGN KEY (group_id, candidate_revision_id)+    REFERENCES keiro.keiro_projection_revisions (group_id, revision_id);++CREATE TABLE keiro.keiro_projection_target_generations (+  generation_id UUID PRIMARY KEY,+  group_id TEXT NOT NULL,+  target_id TEXT NOT NULL,+  revision_id TEXT NOT NULL,+  schema_name TEXT NOT NULL,+  relation_name TEXT NOT NULL,+  relation_oid BIGINT NOT NULL,+  schema_version TEXT NOT NULL,+  expected_shape_id TEXT NOT NULL,+  observed_shape_fingerprint TEXT NOT NULL,+  observed_catalog_snapshot TEXT NOT NULL,+  lifecycle TEXT NOT NULL,+  created_by_run_id TEXT+    REFERENCES keiro.keiro_projection_rebuild_runs (run_id),+  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  served_at TIMESTAMPTZ,+  retired_at TIMESTAMPTZ,+  dropped_at TIMESTAMPTZ,+  UNIQUE (generation_id, created_by_run_id, target_id),+  FOREIGN KEY (group_id, revision_id)+    REFERENCES keiro.keiro_projection_revisions (group_id, revision_id),+  CONSTRAINT keiro_projection_target_generations_identity_chk+    CHECK (+      length(target_id) > 0+      AND length(schema_name) > 0+      AND length(relation_name) > 0+      AND relation_oid > 0+      AND length(schema_version) > 0+      AND length(expected_shape_id) > 0+      AND length(observed_shape_fingerprint) > 0+    ),+  CONSTRAINT keiro_projection_target_generations_lifecycle_chk+    CHECK (lifecycle IN ('staging', 'serving', 'retired', 'dropped')),+  CONSTRAINT keiro_projection_target_generations_transition_evidence_chk+    CHECK (+      (+        lifecycle = 'staging'+        AND created_by_run_id IS NOT NULL+        AND served_at IS NULL+        AND retired_at IS NULL+        AND dropped_at IS NULL+      )+      OR (+        lifecycle = 'serving'+        AND served_at IS NOT NULL+        AND retired_at IS NULL+        AND dropped_at IS NULL+      )+      OR (+        lifecycle = 'retired'+        AND served_at IS NOT NULL+        AND retired_at IS NOT NULL+        AND dropped_at IS NULL+      )+      OR (+        lifecycle = 'dropped'+        AND dropped_at IS NOT NULL+        AND (+          (served_at IS NULL AND retired_at IS NULL)+          OR (served_at IS NOT NULL AND retired_at IS NOT NULL)+        )+      )+    )+);++CREATE UNIQUE INDEX keiro_projection_target_generations_serving_idx+  ON keiro.keiro_projection_target_generations (group_id, target_id)+  WHERE lifecycle = 'serving';++CREATE UNIQUE INDEX keiro_projection_target_generations_relation_idx+  ON keiro.keiro_projection_target_generations (schema_name, relation_name)+  WHERE lifecycle <> 'dropped';++CREATE TABLE keiro.keiro_projection_rebuild_run_targets (+  run_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_runs (run_id) ON DELETE CASCADE,+  target_id TEXT NOT NULL,+  target_mode TEXT NOT NULL,+  candidate_generation_id UUID NOT NULL,+  PRIMARY KEY (run_id, target_id),+  UNIQUE (candidate_generation_id),+  FOREIGN KEY (candidate_generation_id, run_id, target_id)+    REFERENCES keiro.keiro_projection_target_generations+      (generation_id, created_by_run_id, target_id),+  CONSTRAINT keiro_projection_rebuild_run_targets_mode_chk+    CHECK (target_mode IN ('application', 'clone'))+);++CREATE TABLE keiro.keiro_projection_rebuild_promotion_objects (+  run_id TEXT NOT NULL,+  target_id TEXT NOT NULL,+  object_order INTEGER NOT NULL,+  object_kind TEXT NOT NULL,+  generation_name TEXT NOT NULL,+  canonical_name TEXT NOT NULL,+  PRIMARY KEY (run_id, target_id, object_order),+  UNIQUE (run_id, target_id, generation_name),+  UNIQUE (run_id, target_id, canonical_name),+  FOREIGN KEY (run_id, target_id)+    REFERENCES keiro.keiro_projection_rebuild_run_targets (run_id, target_id)+    ON DELETE CASCADE,+  CONSTRAINT keiro_projection_rebuild_promotion_objects_order_chk+    CHECK (object_order >= 0),+  CONSTRAINT keiro_projection_rebuild_promotion_objects_kind_chk+    CHECK (object_kind IN ('index', 'constraint', 'owned-sequence')),+  CONSTRAINT keiro_projection_rebuild_promotion_objects_names_chk+    CHECK (length(generation_name) > 0 AND length(canonical_name) > 0)+);++COMMENT ON TABLE keiro.keiro_projection_revisions IS+  'Registered executable/schema revision identities for projection rebuild groups.';++COMMENT ON TABLE keiro.keiro_projection_target_generations IS+  'Durable physical projection target identity and schema evidence across staging, serving, retirement, and drop.';
+ migrations/0026.sql view
@@ -0,0 +1,149 @@+-- public projection-group status contract++-- Persist the cursor authority selected by catalog registration.  A row exists+-- for every group so an inline projection (append) is distinguishable from a+-- legacy or not-yet-reconciled group (unmanaged).  Checkpoint subscription+-- names are private registration metadata; only their conservative floor is+-- published below.+CREATE TABLE keiro.keiro_projection_group_cursors (+  group_id TEXT PRIMARY KEY+    REFERENCES keiro.keiro_projection_rebuild_groups (group_id) ON DELETE CASCADE,+  position_basis TEXT NOT NULL,+  subscription_names TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  CONSTRAINT keiro_projection_group_cursors_basis_chk+    CHECK (position_basis IN ('append', 'checkpoint', 'unmanaged')),+  CONSTRAINT keiro_projection_group_cursors_subscription_names_chk+    CHECK (+      array_position(subscription_names, NULL) IS NULL+      AND (+        (position_basis = 'checkpoint' AND cardinality(subscription_names) > 0)+        OR (+          position_basis IN ('append', 'unmanaged')+          AND cardinality(subscription_names) = 0+        )+      )+    )+);++INSERT INTO keiro.keiro_projection_group_cursors+  (group_id, position_basis, subscription_names)+SELECT group_id, 'unmanaged', ARRAY[]::TEXT[]+FROM keiro.keiro_projection_rebuild_groups+ON CONFLICT (group_id) DO NOTHING;++CREATE SCHEMA IF NOT EXISTS keiro_read;+REVOKE ALL ON SCHEMA keiro_read FROM PUBLIC;++-- This owner-rights view is the only supported SQL surface for an external+-- projection reader.  It intentionally depends only on Keiro-owned relations+-- and Kiroku's versioned owner relation, never Kiroku's private tables.+CREATE VIEW keiro_read.projection_group_status_v1+WITH (security_barrier = true, security_invoker = false) AS+WITH checkpoint_floors AS (+  SELECT+    cursors.group_id,+    CASE+      WHEN cursors.position_basis = 'checkpoint'+        AND count(DISTINCT checkpoints.subscription_name)+              = cardinality(cursors.subscription_names)::BIGINT+      THEN min(checkpoints.checkpoint_position)+      ELSE NULL+    END AS serving_applied_position+  FROM keiro.keiro_projection_group_cursors AS cursors+  LEFT JOIN LATERAL unnest(cursors.subscription_names)+    AS required_subscription(subscription_name)+    ON true+  LEFT JOIN kiroku.subscription_checkpoints_v1 AS checkpoints+    ON checkpoints.subscription_name = required_subscription.subscription_name+  GROUP BY cursors.group_id, cursors.position_basis, cursors.subscription_names+), candidate_progress AS (+  SELECT+    sources.run_id,+    min(sources.cursor_position) AS candidate_rebuild_position+  FROM keiro.keiro_projection_rebuild_sources AS sources+  GROUP BY sources.run_id+), query_models AS (+  SELECT+    models.rebuild_group_id AS group_id,+    array_agg(models.name ORDER BY models.name) AS query_models+  FROM keiro.keiro_read_models AS models+  GROUP BY models.rebuild_group_id+)+SELECT+  groups.group_id,+  groups.status AS lifecycle_phase,+  groups.reads_allowed,+  groups.writes_allowed,+  groups.serving_revision_id,+  groups.serving_epoch,+  coalesce(cursors.position_basis, 'unmanaged') AS serving_position_basis,+  CASE+    WHEN groups.reads_allowed AND cursors.position_basis = 'checkpoint'+    THEN checkpoint_floors.serving_applied_position+    ELSE NULL+  END AS serving_applied_position,+  groups.active_run_id,+  runs.candidate_revision_id,+  candidate_progress.candidate_rebuild_position,+  runs.captured_head AS candidate_rebuild_head,+  coalesce(query_models.query_models, ARRAY[]::TEXT[]) AS query_models,+  runs.started_at AS rebuild_started_at,+  groups.completed_at AS last_promoted_at,+  groups.failed_at,+  groups.failure_code,+  groups.failure_detail+FROM keiro.keiro_projection_rebuild_groups AS groups+LEFT JOIN keiro.keiro_projection_group_cursors AS cursors+  ON cursors.group_id = groups.group_id+LEFT JOIN checkpoint_floors+  ON checkpoint_floors.group_id = groups.group_id+LEFT JOIN keiro.keiro_projection_rebuild_runs AS runs+  ON runs.run_id = groups.active_run_id+LEFT JOIN candidate_progress+  ON candidate_progress.run_id = runs.run_id+LEFT JOIN query_models+  ON query_models.group_id = groups.group_id;++REVOKE ALL ON keiro_read.projection_group_status_v1 FROM PUBLIC;++COMMENT ON SCHEMA keiro_read IS+  'Versioned, owner-rights read contracts for out-of-process Keiro consumers.';+COMMENT ON VIEW keiro_read.projection_group_status_v1 IS+  'Frozen v1 projection-group status contract. Grant schema USAGE and view SELECT only.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.group_id IS+  'Stable rebuild-group identifier and row key.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.lifecycle_phase IS+  'Diagnostic lifecycle phase; readers must use reads_allowed for availability.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.reads_allowed IS+  'Authoritative permission to serve reads from the current generation.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.writes_allowed IS+  'Whether live projection writes may currently target the serving generation.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.serving_revision_id IS+  'Served schema revision, or NULL for legacy/offline groups without versioned generations.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.serving_epoch IS+  'Monotonic cache-invalidation epoch for changes to the served generation.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.serving_position_basis IS+  'append, checkpoint, or unmanaged; determines how serving_applied_position is interpreted.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.serving_applied_position IS+  'Conservative floor across every registered checkpoint subscription/member; NULL until all subscriptions are present, and always NULL for append or unmanaged groups.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.active_run_id IS+  'Active rebuild run, including a retained failed run, or NULL when no rebuild is active.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.candidate_revision_id IS+  'Versioned candidate revision for the active run, or NULL for offline/no active run.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.candidate_rebuild_position IS+  'Conservative floor of persisted source cursors for the active run, or NULL before sources exist/no active run.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.candidate_rebuild_head IS+  'Head captured by the active rebuild run, or NULL when no run is active.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.query_models IS+  'Sorted query-model names owned by the group; empty when none are registered.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.rebuild_started_at IS+  'Start time of the active rebuild run, or NULL when no run is active.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.last_promoted_at IS+  'Most recent group promotion completion time, or NULL before the first promotion.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.failed_at IS+  'Group failure time, or NULL outside a failed lifecycle.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.failure_code IS+  'Stable machine-readable group failure code, or NULL outside a failed lifecycle.';+COMMENT ON COLUMN keiro_read.projection_group_status_v1.failure_detail IS+  'Operator-facing group failure detail, or NULL outside a failed lifecycle.';
+ migrations/0027.sql view
@@ -0,0 +1,230 @@+-- managed external read contracts++CREATE TABLE keiro.keiro_external_read_contracts (+  contract_id TEXT NOT NULL,+  contract_version INTEGER NOT NULL,+  query_model_id TEXT NOT NULL,+  group_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_groups (group_id),+  public_function_name TEXT NOT NULL,+  contract_kind TEXT NOT NULL,+  argument_names TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],+  argument_types TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],+  result_type TEXT NOT NULL,+  result_shape_hash TEXT NOT NULL,+  serving_shape_hash TEXT,+  compatible_revision_ids TEXT[] NOT NULL,+  private_implementation TEXT,+  private_implementation_version INTEGER,+  immutable_signature_hash TEXT NOT NULL,+  definition_hash TEXT NOT NULL,+  surface_generation INTEGER NOT NULL,+  state TEXT NOT NULL,+  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  retired_at TIMESTAMPTZ,+  PRIMARY KEY (contract_id, contract_version),+  UNIQUE (public_function_name, argument_types),+  CONSTRAINT keiro_external_read_contracts_identity_chk+    CHECK (+      length(contract_id) > 0+      AND contract_version > 0+      AND length(query_model_id) > 0+      AND length(group_id) > 0+      AND length(public_function_name) > 0+      AND length(result_type) > 0+      AND length(result_shape_hash) > 0+      AND length(immutable_signature_hash) > 0+      AND length(definition_hash) > 0+      AND surface_generation > 0+      AND cardinality(compatible_revision_ids) > 0+      AND array_position(compatible_revision_ids, NULL) IS NULL+      AND array_position(argument_names, NULL) IS NULL+      AND array_position(argument_types, NULL) IS NULL+      AND cardinality(argument_names) = cardinality(argument_types)+    ),+  CONSTRAINT keiro_external_read_contracts_kind_chk+    CHECK (contract_kind IN ('all-rows', 'keyed')),+  CONSTRAINT keiro_external_read_contracts_implementation_chk+    CHECK (+      (+        contract_kind = 'all-rows'+        AND cardinality(argument_names) = 0+        AND private_implementation IS NULL+        AND private_implementation_version IS NULL+      )+      OR (+        contract_kind = 'keyed'+        AND cardinality(argument_names) > 0+        AND private_implementation IS NOT NULL+        AND length(private_implementation) > 0+        AND private_implementation_version > 0+      )+    ),+  CONSTRAINT keiro_external_read_contracts_state_chk+    CHECK (state IN ('candidate', 'active', 'pending-retirement', 'retired')),+  CONSTRAINT keiro_external_read_contracts_retired_chk+    CHECK (+      (state = 'retired' AND retired_at IS NOT NULL)+      OR (state <> 'retired' AND retired_at IS NULL)+    )+);++CREATE INDEX keiro_external_read_contracts_group_idx+  ON keiro.keiro_external_read_contracts (group_id, state);++CREATE TABLE keiro.keiro_managed_read_objects (+  object_schema TEXT NOT NULL,+  object_name TEXT NOT NULL,+  object_kind TEXT NOT NULL,+  object_signature TEXT NOT NULL DEFAULT '',+  contract_id TEXT,+  contract_version INTEGER,+  managed_by TEXT NOT NULL,+  definition_hash TEXT NOT NULL,+  surface_generation INTEGER NOT NULL,+  state TEXT NOT NULL,+  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),+  retired_at TIMESTAMPTZ,+  PRIMARY KEY (object_schema, object_name, object_kind, object_signature),+  FOREIGN KEY (contract_id, contract_version)+    REFERENCES keiro.keiro_external_read_contracts (contract_id, contract_version),+  CONSTRAINT keiro_managed_read_objects_identity_chk+    CHECK (+      length(object_schema) > 0+      AND length(object_name) > 0+      AND length(object_kind) > 0+      AND length(managed_by) > 0+      AND length(definition_hash) > 0+      AND surface_generation > 0+      AND (+        (contract_id IS NULL AND contract_version IS NULL)+        OR (contract_id IS NOT NULL AND contract_version IS NOT NULL)+      )+    ),+  CONSTRAINT keiro_managed_read_objects_kind_chk+    CHECK (object_kind IN ('guard-function', 'binding-view', 'wrapper-function', 'contract-type')),+  CONSTRAINT keiro_managed_read_objects_owner_chk+    CHECK (managed_by IN ('keiro', 'consumer')),+  CONSTRAINT keiro_managed_read_objects_state_chk+    CHECK (state IN ('active', 'pending-retirement', 'retired')),+  CONSTRAINT keiro_managed_read_objects_retired_chk+    CHECK (+      (state = 'retired' AND retired_at IS NOT NULL)+      OR (state <> 'retired' AND retired_at IS NULL)+    )+);++CREATE INDEX keiro_managed_read_objects_contract_idx+  ON keiro.keiro_managed_read_objects (contract_id, contract_version);++CREATE FUNCTION keiro_read.guard_external_read_v1(+  requested_contract_id TEXT,+  requested_contract_version INTEGER+)+RETURNS VOID+LANGUAGE plpgsql+SECURITY DEFINER+SET search_path = pg_catalog+AS $guard$+DECLARE+  contract_group_id TEXT;+  contract_state TEXT;+  contract_shape_hash TEXT;+  contract_serving_shape_hash TEXT;+  contract_compatible_revisions TEXT[];+  group_reads_allowed BOOLEAN;+  group_serving_revision TEXT;+BEGIN+  SELECT+    contracts.group_id,+    contracts.state,+    contracts.result_shape_hash,+    contracts.serving_shape_hash,+    contracts.compatible_revision_ids+  INTO+    contract_group_id,+    contract_state,+    contract_shape_hash,+    contract_serving_shape_hash,+    contract_compatible_revisions+  FROM keiro.keiro_external_read_contracts AS contracts+  WHERE contracts.contract_id = requested_contract_id+    AND contracts.contract_version = requested_contract_version;++  IF NOT FOUND OR contract_state IN ('pending-retirement', 'retired') THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR002',+      MESSAGE = 'external read contract is unknown or retired',+      DETAIL = pg_catalog.format(+        'contract=%s version=%s',+        requested_contract_id,+        requested_contract_version+      );+  END IF;++  SELECT groups.reads_allowed, groups.serving_revision_id+  INTO group_reads_allowed, group_serving_revision+  FROM keiro.keiro_projection_rebuild_groups AS groups+  WHERE groups.group_id = contract_group_id+  FOR SHARE;++  IF NOT FOUND THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR002',+      MESSAGE = 'external read contract group is unavailable',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version+      );+  END IF;++  IF NOT group_reads_allowed THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR001',+      MESSAGE = 'projection is temporarily unavailable',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s serving_revision=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version,+        coalesce(group_serving_revision, '<none>')+      );+  END IF;++  IF contract_state <> 'active'+     OR group_serving_revision IS NULL+     OR NOT (group_serving_revision = ANY(contract_compatible_revisions))+     OR contract_serving_shape_hash IS DISTINCT FROM contract_shape_hash THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR003',+      MESSAGE = 'external read contract is incompatible with the serving revision',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s serving_revision=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version,+        coalesce(group_serving_revision, '<none>')+      );+  END IF;+END+$guard$;++REVOKE ALL ON FUNCTION keiro_read.guard_external_read_v1(TEXT, INTEGER) FROM PUBLIC;++INSERT INTO keiro.keiro_managed_read_objects+  (object_schema, object_name, object_kind, object_signature,+   managed_by, definition_hash, surface_generation, state)+VALUES+  ('keiro_read', 'guard_external_read_v1', 'guard-function', 'text,integer',+   'keiro', 'migration-0027', 1, 'active');++COMMENT ON TABLE keiro.keiro_external_read_contracts IS+  'Private lifecycle and compatibility metadata for versioned external read functions.';+COMMENT ON TABLE keiro.keiro_managed_read_objects IS+  'Object-level ownership, generation, and definition hashes for reconciled external read surfaces.';+COMMENT ON FUNCTION keiro_read.guard_external_read_v1(TEXT, INTEGER) IS+  'Shared-lock availability and serving-revision compatibility guard for managed external reads.';
+ migrations/0028.sql view
@@ -0,0 +1,23 @@+-- permit implementation-backed compatibility wrappers with stable zero-argument signatures++ALTER TABLE keiro.keiro_external_read_contracts+  DROP CONSTRAINT keiro_external_read_contracts_implementation_chk,+  ADD CONSTRAINT keiro_external_read_contracts_implementation_chk+    CHECK (+      (+        contract_kind = 'all-rows'+        AND cardinality(argument_names) = 0+        AND private_implementation IS NULL+        AND private_implementation_version IS NULL+      )+      OR (+        contract_kind = 'keyed'+        AND private_implementation IS NOT NULL+        AND length(private_implementation) > 0+        AND private_implementation_version > 0+      )+    );++COMMENT ON CONSTRAINT keiro_external_read_contracts_implementation_chk+  ON keiro.keiro_external_read_contracts IS+  'All-row contracts are Keiro-backed; keyed contracts delegate to a versioned consumer implementation and may retain a zero-argument public compatibility signature.';
+ migrations/0029.sql view
@@ -0,0 +1,218 @@+-- Bound schema-versioned promotion attempts and stage async redelivery evidence.++ALTER TABLE keiro.keiro_projection_rebuild_runs+  ADD COLUMN promotion_dedup_limit BIGINT,+  ADD COLUMN dedup_provisional_head BIGINT,+  ADD COLUMN promotion_prepared_at TIMESTAMPTZ;++UPDATE keiro.keiro_projection_rebuild_runs+SET promotion_dedup_limit = 1000000+WHERE rebuild_mode = 'versioned';++ALTER TABLE keiro.keiro_projection_rebuild_runs+  DROP CONSTRAINT keiro_projection_rebuild_runs_mode_chk,+  ADD CONSTRAINT keiro_projection_rebuild_runs_mode_chk+    CHECK (+      (+        rebuild_mode = 'offline'+        AND candidate_revision_id IS NULL+        AND cutover_threshold IS NULL+        AND cutover_lock_timeout_ms IS NULL+        AND promotion_dedup_limit IS NULL+        AND dedup_provisional_head IS NULL+        AND promotion_prepared_at IS NULL+        AND history_retention_lease_id IS NULL+        AND history_retention_lease_owner IS NULL+        AND history_retention_protected_through IS NULL+        AND history_retention_expires_at IS NULL+        AND history_retention_renewed_at IS NULL+        AND history_retention_released_at IS NULL+      )+      OR (+        rebuild_mode = 'versioned'+        AND candidate_revision_id IS NOT NULL+        AND cutover_threshold IS NOT NULL+        AND cutover_threshold >= 0+        AND cutover_lock_timeout_ms IS NOT NULL+        AND cutover_lock_timeout_ms > 0+        AND promotion_dedup_limit IS NOT NULL+        AND promotion_dedup_limit > 0+        AND (dedup_provisional_head IS NULL OR dedup_provisional_head >= 0)+        AND history_retention_lease_id IS NOT NULL+        AND history_retention_lease_owner IS NOT NULL+        AND length(history_retention_lease_owner) > 0+        AND history_retention_protected_through IS NOT NULL+        AND history_retention_protected_through >= 0+        AND history_retention_expires_at IS NOT NULL+        AND history_retention_renewed_at IS NOT NULL+      )+    );++CREATE TABLE keiro.keiro_projection_rebuild_dedup_stage (+  run_id TEXT NOT NULL+    REFERENCES keiro.keiro_projection_rebuild_runs (run_id) ON DELETE CASCADE,+  subscription_name TEXT NOT NULL,+  projection_name TEXT NOT NULL,+  event_id UUID NOT NULL,+  global_position BIGINT NOT NULL,+  PRIMARY KEY (run_id, projection_name, event_id),+  CONSTRAINT keiro_projection_rebuild_dedup_stage_names_chk+    CHECK (length(subscription_name) > 0 AND length(projection_name) > 0),+  CONSTRAINT keiro_projection_rebuild_dedup_stage_position_chk+    CHECK (global_position > 0)+);++CREATE INDEX keiro_projection_rebuild_dedup_stage_run_position_idx+  ON keiro.keiro_projection_rebuild_dedup_stage+    (run_id, subscription_name, global_position);++-- Each helper catches only lock/deadline failures. That keeps the transaction+-- usable long enough for Keiro to return a typed retryable lifecycle outcome;+-- every other SQL error remains an ordinary database failure.+CREATE FUNCTION keiro.keiro_try_projection_cutover_fence_v1(+  requested_run_id TEXT,+  requested_contract TEXT,+  attempt_deadline TIMESTAMPTZ+)+RETURNS TEXT+LANGUAGE plpgsql+SECURITY INVOKER+SET search_path = pg_catalog, pg_temp+AS $$+DECLARE+  remaining_ms BIGINT;+  fenced BOOLEAN;+BEGIN+  remaining_ms := ceil(extract(epoch FROM (attempt_deadline - clock_timestamp())) * 1000);+  IF remaining_ms <= 0 THEN+    RETURN 'deadline-exceeded';+  END IF;+  PERFORM set_config('lock_timeout', remaining_ms::text || 'ms', true);+  PERFORM set_config('statement_timeout', remaining_ms::text || 'ms', true);+  BEGIN+    UPDATE keiro.keiro_projection_rebuild_groups AS groups+    SET status = 'cutover-versioned', writes_allowed = FALSE, updated_at = now()+    FROM keiro.keiro_projection_rebuild_runs AS runs+    WHERE runs.run_id = requested_run_id+      AND runs.contract_fingerprint = requested_contract+      AND runs.status = 'running'+      AND groups.group_id = runs.group_id+      AND groups.status = 'rebuilding-versioned'+      AND groups.active_run_id = runs.run_id+      AND NOT EXISTS (+        SELECT 1 FROM keiro.keiro_projection_rebuild_sources AS sources+        WHERE sources.run_id = runs.run_id+          AND sources.exhausted_through IS DISTINCT FROM sources.target_position+      )+    RETURNING TRUE INTO fenced;+  EXCEPTION+    WHEN lock_not_available OR query_canceled THEN+      RETURN 'deadline-exceeded';+  END;+  IF coalesce(fenced, FALSE) THEN+    RETURN 'fenced';+  END IF;+  RETURN 'not-ready';+END;+$$;++CREATE FUNCTION keiro.keiro_try_projection_promotion_lock_v1(+  requested_run_id TEXT,+  requested_contract TEXT,+  attempt_deadline TIMESTAMPTZ+)+RETURNS TEXT+LANGUAGE plpgsql+SECURITY INVOKER+SET search_path = pg_catalog, pg_temp+AS $$+DECLARE+  remaining_ms BIGINT;+  locked_run_id TEXT;+BEGIN+  remaining_ms := ceil(extract(epoch FROM (attempt_deadline - clock_timestamp())) * 1000);+  IF remaining_ms <= 0 THEN+    RETURN 'deadline-exceeded';+  END IF;+  PERFORM set_config('lock_timeout', remaining_ms::text || 'ms', true);+  PERFORM set_config('statement_timeout', remaining_ms::text || 'ms', true);+  BEGIN+    SELECT runs.run_id INTO locked_run_id+    FROM keiro.keiro_projection_rebuild_runs AS runs+    JOIN keiro.keiro_projection_rebuild_groups AS groups+      ON groups.group_id = runs.group_id+    WHERE runs.run_id = requested_run_id+      AND runs.contract_fingerprint = requested_contract+      AND runs.status = 'cutover'+      AND runs.promotion_prepared_at IS NOT NULL+      AND groups.status = 'cutover-versioned'+      AND groups.active_run_id = runs.run_id+      AND NOT EXISTS (+        SELECT 1 FROM keiro.keiro_projection_rebuild_sources AS sources+        WHERE sources.run_id = runs.run_id+          AND sources.exhausted_through IS DISTINCT FROM sources.target_position+      )+    FOR UPDATE OF runs, groups;+  EXCEPTION+    WHEN lock_not_available OR query_canceled THEN+      RETURN 'deadline-exceeded';+  END;+  IF locked_run_id IS NOT NULL THEN+    RETURN 'locked';+  END IF;+  RETURN 'not-ready';+END;+$$;++CREATE FUNCTION keiro.keiro_try_projection_relation_locks_v1(+  relation_oids BIGINT[],+  attempt_deadline TIMESTAMPTZ+)+RETURNS BOOLEAN+LANGUAGE plpgsql+SECURITY INVOKER+SET search_path = pg_catalog, pg_temp+AS $$+DECLARE+  remaining_ms BIGINT;+  relation_list TEXT;+  resolved_count BIGINT;+BEGIN+  remaining_ms := ceil(extract(epoch FROM (attempt_deadline - clock_timestamp())) * 1000);+  IF remaining_ms <= 0 THEN+    RETURN FALSE;+  END IF;++  SELECT+    string_agg(format('%I.%I', namespaces.nspname, relations.relname), ', ' ORDER BY requested.ordinality),+    count(*)+  INTO relation_list, resolved_count+  FROM unnest(relation_oids) WITH ORDINALITY AS requested(relation_oid, ordinality)+  JOIN pg_class AS relations ON relations.oid = requested.relation_oid+  JOIN pg_namespace AS namespaces ON namespaces.oid = relations.relnamespace;++  IF resolved_count IS DISTINCT FROM cardinality(relation_oids) OR relation_list IS NULL THEN+    RAISE EXCEPTION 'one or more promotion relations no longer exist';+  END IF;++  -- Object resolution is part of the attempt. Recompute immediately before the+  -- cumulative lock so time already spent under the group lock is not restored.+  remaining_ms := ceil(extract(epoch FROM (attempt_deadline - clock_timestamp())) * 1000);+  IF remaining_ms <= 0 THEN+    RETURN FALSE;+  END IF;+  PERFORM set_config('lock_timeout', remaining_ms::text || 'ms', true);+  PERFORM set_config('statement_timeout', remaining_ms::text || 'ms', true);+  BEGIN+    EXECUTE 'LOCK TABLE ' || relation_list || ' IN ACCESS EXCLUSIVE MODE';+  EXCEPTION+    WHEN lock_not_available OR query_canceled THEN+      RETURN FALSE;+  END;+  RETURN TRUE;+END;+$$;++REVOKE ALL ON FUNCTION keiro.keiro_try_projection_cutover_fence_v1(TEXT, TEXT, TIMESTAMPTZ) FROM PUBLIC;+REVOKE ALL ON FUNCTION keiro.keiro_try_projection_promotion_lock_v1(TEXT, TEXT, TIMESTAMPTZ) FROM PUBLIC;+REVOKE ALL ON FUNCTION keiro.keiro_try_projection_relation_locks_v1(BIGINT[], TIMESTAMPTZ) FROM PUBLIC;
+ migrations/0030.sql view
@@ -0,0 +1,145 @@+-- make guarded external reads revalidate contract state after the lifecycle lock++CREATE OR REPLACE FUNCTION keiro_read.guard_external_read_v1(+  requested_contract_id TEXT,+  requested_contract_version INTEGER+)+RETURNS VOID+LANGUAGE plpgsql+SECURITY DEFINER+SET search_path = pg_catalog+AS $guard$+DECLARE+  contract_group_id TEXT;+  contract_state TEXT;+  contract_shape_hash TEXT;+  contract_serving_shape_hash TEXT;+  contract_compatible_revisions TEXT[];+  group_reads_allowed BOOLEAN;+  group_serving_revision TEXT;+  group_serving_epoch_before BIGINT;+  group_serving_epoch BIGINT;+BEGIN+  -- Contract-to-group ownership is immutable. Read only that routing fact before+  -- locking, then re-read every mutable contract fact after the group lock.+  SELECT contracts.group_id, groups.serving_epoch+  INTO contract_group_id, group_serving_epoch_before+  FROM keiro.keiro_external_read_contracts AS contracts+  JOIN keiro.keiro_projection_rebuild_groups AS groups+    ON groups.group_id = contracts.group_id+  WHERE contracts.contract_id = requested_contract_id+    AND contracts.contract_version = requested_contract_version;++  IF NOT FOUND THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR002',+      MESSAGE = 'external read contract is unknown or retired',+      DETAIL = pg_catalog.format(+        'contract=%s version=%s',+        requested_contract_id,+        requested_contract_version+      );+  END IF;++  SELECT groups.reads_allowed, groups.serving_revision_id, groups.serving_epoch+  INTO group_reads_allowed, group_serving_revision, group_serving_epoch+  FROM keiro.keiro_projection_rebuild_groups AS groups+  WHERE groups.group_id = contract_group_id+  FOR SHARE;++  IF NOT FOUND THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR002',+      MESSAGE = 'external read contract group is unavailable',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version+      );+  END IF;++  -- A promotion updates the group and reconciles this row in one transaction.+  -- The locking re-read observes that committed row after waiting for promotion,+  -- rather than retaining the statement's pre-promotion metadata snapshot.+  SELECT+    contracts.state,+    contracts.result_shape_hash,+    contracts.serving_shape_hash,+    contracts.compatible_revision_ids+  INTO+    contract_state,+    contract_shape_hash,+    contract_serving_shape_hash,+    contract_compatible_revisions+  FROM keiro.keiro_external_read_contracts AS contracts+  WHERE contracts.contract_id = requested_contract_id+    AND contracts.contract_version = requested_contract_version+    AND contracts.group_id = contract_group_id+  FOR SHARE;++  IF NOT FOUND OR contract_state IN ('pending-retirement', 'retired') THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR002',+      MESSAGE = 'external read contract is unknown or retired',+      DETAIL = pg_catalog.format(+        'contract=%s version=%s',+        requested_contract_id,+        requested_contract_version+      );+  END IF;++  -- PostgreSQL fixes the caller statement snapshot before entering this+  -- function. A locking read can observe the newly committed group row after+  -- waiting, but the later projection query cannot safely adopt every catalog+  -- and relation change in that old snapshot. Refuse the crossed epoch so the+  -- caller retries as a new statement instead of returning stale or empty data.+  IF group_serving_epoch IS DISTINCT FROM group_serving_epoch_before THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR001',+      MESSAGE = 'projection generation changed while the external read waited',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s serving_epoch_before=%s serving_epoch=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version,+        group_serving_epoch_before,+        group_serving_epoch+      );+  END IF;++  IF NOT group_reads_allowed THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR001',+      MESSAGE = 'projection is temporarily unavailable',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s serving_revision=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version,+        coalesce(group_serving_revision, '<none>')+      );+  END IF;++  IF contract_state <> 'active'+     OR group_serving_revision IS NULL+     OR NOT (group_serving_revision = ANY(contract_compatible_revisions))+     OR contract_serving_shape_hash IS DISTINCT FROM contract_shape_hash THEN+    RAISE EXCEPTION USING+      ERRCODE = 'KR003',+      MESSAGE = 'external read contract is incompatible with the serving revision',+      DETAIL = pg_catalog.format(+        'group=%s contract=%s version=%s serving_revision=%s',+        contract_group_id,+        requested_contract_id,+        requested_contract_version,+        coalesce(group_serving_revision, '<none>')+      );+  END IF;+END+$guard$;++REVOKE ALL ON FUNCTION keiro_read.guard_external_read_v1(TEXT, INTEGER) FROM PUBLIC;++COMMENT ON FUNCTION keiro_read.guard_external_read_v1(TEXT, INTEGER) IS+  'Shared-lock availability, post-lock contract revalidation, and crossed-epoch retry fencing for managed external reads.';
migrations/manifest view
@@ -18,3 +18,13 @@ 0018.sql 0019-keiro-snapshots-state-shape-hash.sql 0020-keiro-workflow-children-failure-reason.sql+0021-keiro-workflows-exact-discovery.sql+0022.sql+0023.sql+0024.sql+0025.sql+0026.sql+0027.sql+0028.sql+0029.sql+0030.sql
src/Keiro/Migrations/SchemaCheck.hs view
@@ -101,17 +101,19 @@             <> actualDefinition             <> ")" --- | Read a sorted canonical snapshot of tables, columns, constraints, and indexes.+-- | Read a sorted canonical snapshot of tables, columns, constraints, indexes,+-- and ordered view column signatures in one schema. snapshotSchema :: Text -> Session Text snapshotSchema schema =   Text.unlines <$> Session.statement schema schemaSnapshotStatement --- | PostgreSQL 18 snapshot generated from the complete embedded migration plan.+-- | PostgreSQL 18 snapshot regenerated through migration 0030. expectedSchemaSnapshot :: Text expectedSchemaSnapshot =   $(embedTextFile "expected-schema/native/keiro-v18.txt") --- | Compare the live @keiro@ schema with the embedded PostgreSQL 18 snapshot.+-- | Compare the live @keiro@ and @keiro_read@ schemas with the embedded+-- PostgreSQL 18 snapshot. verifyExpectedSchema ::   Settings.Settings ->   IO (Either MigrationError [SchemaDrift])@@ -135,7 +137,10 @@       serverVersionNumber <- Session.statement () serverVersionStatement       let majorVersion = fromIntegral serverVersionNumber `div` 10000       if majorVersion == (18 :: Int)-        then Right <$> snapshotSchema "keiro"+        then do+          privateSchema <- snapshotSchema "keiro"+          publicSchema <- snapshotSchema "keiro_read"+          pure (Right (privateSchema <> publicSchema))         else pure (Left (UnsupportedPostgresVersion majorVersion))  snapshotObjects :: Text -> Map Text (Text, Text)@@ -212,6 +217,23 @@         JOIN pg_namespace n ON n.oid = ct.relnamespace         WHERE n.nspname = $1           AND configured.search_path = 'pg_catalog'+      UNION ALL+      SELECT 'view' || E'\t' || n.nspname || '.' || c.relname || E'\t'+             || 'columns='+             || string_agg(+                  a.attnum::text || ':' || a.attname || ' '+                    || format_type(a.atttypid, a.atttypmod),+                  ',' ORDER BY a.attnum+                ) AS line+        FROM pg_class c+        JOIN pg_namespace n ON n.oid = c.relnamespace+        JOIN pg_attribute a ON a.attrelid = c.oid+        WHERE n.nspname = $1+          AND c.relkind = 'v'+          AND a.attnum > 0+          AND NOT a.attisdropped+          AND configured.search_path = 'pg_catalog'+        GROUP BY n.nspname, c.relname     ) snapshot     ORDER BY line COLLATE "C"     """
test/Lint.hs view
@@ -20,6 +20,7 @@ -- "-- pg-migrate: no-transaction" leading comment, which review gates. data LintConfig = LintConfig   { requiredQualifier :: Text,+    additionalQualifiers :: [Text],     exemptFiles :: [FilePath]   }   deriving stock (Eq, Show)@@ -38,21 +39,34 @@         body = Text.Encoding.decodeUtf8With lenientDecode bytes         statements = map Text.strip . Text.splitOn ";" $ stripCommentLines body -    requiredLower = Text.toCaseFold (requiredQualifier config)+    allowedQualifiers =+      Text.toCaseFold <$> (requiredQualifier config : additionalQualifiers config)      searchPathViolation file body =       [ "migration body mentions search_path: " <> Text.pack file-      | "search_path" `Text.isInfixOf` Text.toCaseFold (stripCommentLines body)+      | let normalized = Text.toCaseFold (stripCommentLines body),+        "search_path" `Text.isInfixOf` normalized,+        not (onlyHardenedSearchPathClauses normalized)       ] +    onlyHardenedSearchPathClauses body =+      "search_path" `Text.isInfixOf` body+        && not+          ( "search_path"+              `Text.isInfixOf` Text.replace "set search_path = pg_catalog" "" body+          )+     statementViolations file statement =       case statementTarget statement of         Nothing -> []         Just target-          | requiredLower `Text.isPrefixOf` Text.toCaseFold (cleanTarget target) -> []+          | any+              (`Text.isPrefixOf` Text.toCaseFold (cleanTarget target))+              allowedQualifiers ->+              []           | otherwise ->               [ "migration DDL target is not qualified with "-                  <> requiredQualifier config+                  <> Text.intercalate " or " (requiredQualifier config : additionalQualifiers config)                   <> " in "                   <> Text.pack file                   <> ": "@@ -66,6 +80,10 @@       targetAfter ["create", "table"] wordsOriginal   | lower `startsWithWords` ["alter", "table"] =       targetAfter ["alter", "table"] wordsOriginal+  | lower `startsWithWords` ["create", "view"] =+      targetAfter ["create", "view"] wordsOriginal+  | lower `startsWithWords` ["create", "or", "replace", "view"] =+      targetAfter ["create", "or", "replace", "view"] wordsOriginal   | lower `startsWithWords` ["drop", "index"] =       targetAfter ["drop", "index"] wordsOriginal   | lower `startsWithWords` ["create", "index"] =
test/Main.hs view
@@ -9,9 +9,10 @@ import Data.ByteString qualified as ByteString import Data.Either (isLeft) import Data.Foldable (toList)-import Data.Int (Int64)+import Data.Int (Int32, Int64) import Data.List (findIndex, sort, (\\)) import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NonEmpty import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text (Text)@@ -53,7 +54,7 @@ main :: IO () main = hspec $ do   describe "native Keiro migration definition" $ do-    it "tracks twenty native files in manifest order" $ do+    it "tracks thirty native files in manifest order" $ do       directory <- findMigrationsDirectory       manifest <- Text.lines <$> Text.IO.readFile (directory </> "manifest")       manifest `shouldBe` Text.pack <$> nativeMigrationFiles@@ -66,7 +67,7 @@         bytes <- ByteString.readFile (directory </> nativeName)         lookup legacyName lockEntries `shouldBe` Just (checksumText bytes) -    it "builds component keiro with dependency kiroku and twenty migrations" $ do+    it "builds component keiro with dependency kiroku and thirty migrations" $ do       plan <- requirePlan       let PlanDescription components = planDescription plan       case toList components of@@ -75,11 +76,11 @@           ] -> do             componentNameText kirokuName `shouldBe` "kiroku"             kirokuDependencies `shouldBe` mempty-            length kirokuEntries `shouldBe` 8+            length kirokuEntries `shouldBe` 10             componentNameText keiroName `shouldBe` "keiro"             dependencyName <- requireRight (componentName "kiroku")             keiroDependencies `shouldBe` Set.singleton dependencyName-            length keiroEntries `shouldBe` 20+            length keiroEntries `shouldBe` 30         actual -> expectationFailure ("unexpected plan description: " <> show actual)       validateHistoryMappingTargets plan frameworkCoddHistoryMappings `shouldBe` Right () @@ -125,7 +126,12 @@             )    describe "migration body lint" $ do-    let config = LintConfig {requiredQualifier = "keiro.", exemptFiles = []}+    let config =+          LintConfig+            { requiredQualifier = "keiro.",+              additionalQualifiers = ["keiro_read."],+              exemptFiles = []+            }      it "flags an unqualified DDL target" $ do       let violations =@@ -142,13 +148,28 @@         [("9999-fixture.sql", "SET search_path TO keiro;")]         `shouldSatisfy` (not . null) +    it "accepts a fixed pg_catalog search path on a security-definer function" $ do+      lintViolations+        config+        [ ( "9999-fixture.sql",+            "CREATE FUNCTION keiro_read.safe_v1() RETURNS void LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog AS 'SELECT';"+          )+        ]+        `shouldBe` []++    it "accepts a versioned view in the public read schema" $ do+      lintViolations+        config+        [("9999-fixture.sql", "CREATE VIEW keiro_read.status_v1 AS SELECT 1;")]+        `shouldBe` []+     it "ignores comment-only mentions" $ do       lintViolations         config         [("9999-fixture.sql", "-- Never set search_path in a migration.\nSELECT 1;")]         `shouldBe` [] -    it "passes all 20 embedded native bodies" $ do+    it "passes all 30 embedded native bodies" $ do       lintViolations config (toList embeddedMigrationEntries) `shouldBe` []    describe "startup handshake" $ do@@ -162,7 +183,7 @@             plan             >>= requireRight         Keiro.pendingMigrations handshake `shouldBe` planMigrationIds plan-        length (Keiro.pendingMigrations handshake) `shouldBe` 28+        length (Keiro.pendingMigrations handshake) `shouldBe` 40         Keiro.ledgerIssues handshake `shouldBe` []         handshakePassed handshake `shouldBe` False @@ -187,8 +208,8 @@         _ <- runMigrationPlan defaultRunOptions settings kirokuOnly >>= requireRight         handshake <-           missingMigrations defaultRunOptions provider plan >>= requireRight-        Keiro.pendingMigrations handshake `shouldBe` drop 8 (planMigrationIds plan)-        length (Keiro.pendingMigrations handshake) `shouldBe` 20+        Keiro.pendingMigrations handshake `shouldBe` drop 10 (planMigrationIds plan)+        length (Keiro.pendingMigrations handshake) `shouldBe` 30         Keiro.ledgerIssues handshake `shouldBe` []         handshakePassed handshake `shouldBe` False @@ -220,7 +241,9 @@       snapshotPath <- findNativeExpectedSchema       regenerate <- maybe False (const True) <$> lookupEnv "KEIRO_REGENERATE_EXPECTED_SCHEMA"       result <- withMigratedDatabase plan $ \connection -> do-        actual <- useSession connection (snapshotSchema "keiro")+        privateSchema <- useSession connection (snapshotSchema "keiro")+        publicSchema <- useSession connection (snapshotSchema "keiro_read")+        let actual = privateSchema <> publicSchema         if regenerate           then do             Text.IO.writeFile snapshotPath actual@@ -244,6 +267,7 @@             ( Session.script                 """                 DROP INDEX keiro.keiro_outbox_pending_idx;+                DROP VIEW keiro_read.projection_group_status_v1;                 ALTER TABLE keiro.keiro_outbox                   ALTER COLUMN correlation_id TYPE character varying(64)                   USING correlation_id::text;@@ -257,6 +281,9 @@         rendered           `shouldSatisfy` any             (Text.isInfixOf "keiro_outbox.correlation_id")+        rendered+          `shouldSatisfy` any+            (Text.isInfixOf "keiro_read.projection_group_status_v1")    describe "fresh native databases" $ do     it "applies Kiroku then Keiro, verifies strictly, and is repeatable" $ do@@ -265,16 +292,51 @@         assertSchema connection         let provider = providerFor connection         rerun <- runMigrationPlanWith defaultRunOptions provider plan >>= requireRight-        reportOutcomes rerun `shouldBe` replicate 28 AlreadyApplied+        reportOutcomes rerun `shouldBe` replicate 40 AlreadyApplied         verified <- verifyMigrationPlanWith defaultRunOptions provider plan >>= requireRight         case verified of           VerificationReport verificationIssues applied pending unknown -> do             verificationIssues `shouldBe` []-            length applied `shouldBe` 28+            length applied `shouldBe` 40             pending `shouldBe` []             unknown `shouldBe` []       either (expectationFailure . show) pure result +    it "publishes the frozen status shape through an isolated reader grant" $ do+      plan <- requirePlan+      result <- withMigratedDatabase plan $ \connection -> do+        useSession connection (Session.script projectionStatusFixtureSql)+        columns <-+          useSession connection (Session.statement () projectionStatusColumnsStatement)+        columns+          `shouldBe` [ ("group_id", "text"),+                       ("lifecycle_phase", "text"),+                       ("reads_allowed", "boolean"),+                       ("writes_allowed", "boolean"),+                       ("serving_revision_id", "text"),+                       ("serving_epoch", "bigint"),+                       ("serving_position_basis", "text"),+                       ("serving_applied_position", "bigint"),+                       ("active_run_id", "text"),+                       ("candidate_revision_id", "text"),+                       ("candidate_rebuild_position", "bigint"),+                       ("candidate_rebuild_head", "bigint"),+                       ("query_models", "text[]"),+                       ("rebuild_started_at", "timestamp with time zone"),+                       ("last_promoted_at", "timestamp with time zone"),+                       ("failed_at", "timestamp with time zone"),+                       ("failure_code", "text"),+                       ("failure_detail", "text")+                     ]+        contractHealthy <-+          useSession connection (Session.statement () projectionStatusContractStatement)+        contractHealthy `shouldBe` True+        withProjectionReaderRole connection $ do+          readerHealthy <-+            useSession connection (Session.statement () projectionReaderFactsStatement)+          readerHealthy `shouldBe` True+      either (expectationFailure . show) pure result+     it "serializes concurrent composed applies" $ do       plan <- requirePlan       withKeiroPg $ \database -> do@@ -284,8 +346,134 @@             (runMigrationPlan defaultRunOptions settings plan >>= requireRight)             (runMigrationPlan defaultRunOptions settings plan >>= requireRight)         sort [reportOutcomes first, reportOutcomes second]-          `shouldBe` sort [replicate 28 AppliedNow, replicate 28 AlreadyApplied]+          `shouldBe` sort [replicate 40 AppliedNow, replicate 40 AlreadyApplied] +    it "upgrades singleton read-model rows into deterministic rebuild groups" $ do+      fullPlan <- requirePlan+      kiroku <- requireRight Kiroku.kirokuMigrations+      priorKeiro <-+        requireRight+          ( migrationComponentFromEmbeddedSql+              "keiro"+              (Set.singleton "kiroku")+              (NonEmpty.fromList (take 21 (toList embeddedMigrationEntries)))+          )+      priorPlan <- requireRight (frameworkMigrationPlan kiroku priorKeiro)+      withKeiroPg $ \database -> do+        let settings = Pg.connectionSettings database+        _ <- runMigrationPlan defaultRunOptions settings priorPlan >>= requireRight+        withConnection settings $ \connection ->+          useSession connection (Session.script legacyReadModelFixtureSql)+        report <- runMigrationPlan defaultRunOptions settings fullPlan >>= requireRight+        Prelude.drop 31 (reportOutcomes report) `shouldBe` replicate 9 AppliedNow+        withConnection settings $ \connection -> do+          rows <- useSession connection (Session.statement () legacyGroupUpgradeStatement)+          rows+            `shouldBe` [ ("legacy-abandoned", 9, "shape-abandoned", "abandoned", "$legacy-read-model:legacy-abandoned", "failed", Just "$legacy-read-model:legacy-abandoned", Just "abandoned"),+                         ("legacy-live", 7, "shape-live", "live", "$legacy-read-model:legacy-live", "live", Nothing, Nothing),+                         ("legacy-rebuilding", 8, "shape-rebuilding", "rebuilding", "$legacy-read-model:legacy-rebuilding", "rebuilding", Just "$legacy-read-model:legacy-rebuilding", Nothing)+                       ]++    it "0024 stamps in-flight rebuild runs with the pre-canonical sentinel" $ do+      fullPlan <- requirePlan+      kiroku <- requireRight Kiroku.kirokuMigrations+      priorKeiro <-+        requireRight+          ( migrationComponentFromEmbeddedSql+              "keiro"+              (Set.singleton "kiroku")+              (NonEmpty.fromList (take 23 (toList embeddedMigrationEntries)))+          )+      priorPlan <- requireRight (frameworkMigrationPlan kiroku priorKeiro)+      withKeiroPg $ \database -> do+        let settings = Pg.connectionSettings database+        _ <- runMigrationPlan defaultRunOptions settings priorPlan >>= requireRight+        withConnection settings $ \connection ->+          useSession connection (Session.script preCanonicalRebuildFixtureSql)+        report <- runMigrationPlan defaultRunOptions settings fullPlan >>= requireRight+        Prelude.drop 33 (reportOutcomes report) `shouldBe` replicate 7 AppliedNow+        withConnection settings $ \connection -> do+          rows <- useSession connection (Session.statement () preCanonicalRebuildShapeStatement)+          rows+            `shouldBe` [ ("upgrade-failed", Text.replicate 64 "b", "upgrade-run-failed", "$pre-canonical"),+                         ("upgrade-rebuilding", Text.replicate 64 "a", "upgrade-run-live", "$pre-canonical")+                       ]++    it "0026 gives pre-existing groups explicit fail-safe cursor authority" $ do+      fullPlan <- requirePlan+      kiroku <- requireRight Kiroku.kirokuMigrations+      priorKeiro <-+        requireRight+          ( migrationComponentFromEmbeddedSql+              "keiro"+              (Set.singleton "kiroku")+              (NonEmpty.fromList (take 25 (toList embeddedMigrationEntries)))+          )+      priorPlan <- requireRight (frameworkMigrationPlan kiroku priorKeiro)+      withKeiroPg $ \database -> do+        let settings = Pg.connectionSettings database+        _ <- runMigrationPlan defaultRunOptions settings priorPlan >>= requireRight+        withConnection settings $ \connection ->+          useSession connection (Session.script preStatusContractFixtureSql)+        report <- runMigrationPlan defaultRunOptions settings fullPlan >>= requireRight+        Prelude.drop 35 (reportOutcomes report) `shouldBe` replicate 5 AppliedNow+        withConnection settings $ \connection -> do+          facts <- useSession connection (Session.statement () preStatusContractFactsStatement)+          facts `shouldBe` ("unmanaged", 0, "unmanaged", True, True)++    it "0030 upgrades the guard in place with post-lock epoch fencing" $ do+      fullPlan <- requirePlan+      kiroku <- requireRight Kiroku.kirokuMigrations+      priorKeiro <-+        requireRight+          ( migrationComponentFromEmbeddedSql+              "keiro"+              (Set.singleton "kiroku")+              (NonEmpty.fromList (take 29 (toList embeddedMigrationEntries)))+          )+      priorPlan <- requireRight (frameworkMigrationPlan kiroku priorKeiro)+      withKeiroPg $ \database -> do+        let settings = Pg.connectionSettings database+        _ <- runMigrationPlan defaultRunOptions settings priorPlan >>= requireRight+        beforeOid <-+          withConnection settings $ \connection ->+            useSession connection (Session.statement () guardFunctionOidStatement)+        report <- runMigrationPlan defaultRunOptions settings fullPlan >>= requireRight+        Prelude.drop 39 (reportOutcomes report) `shouldBe` [AppliedNow]+        withConnection settings $ \connection -> do+          (afterOid, epochFenced, publicRevoked) <-+            useSession connection (Session.statement () upgradedGuardFactsStatement)+          afterOid `shouldBe` beforeOid+          epochFenced `shouldBe` True+          publicRevoked `shouldBe` True++    it "enforces replay source, adapter, and verification membership constraints" $ do+      plan <- requirePlan+      result <- withMigratedDatabase plan $ \connection -> do+        useSession connection (Session.script replayProgressFixtureSql)+        invalidScope <-+          Connection.use+            connection+            ( Session.script+                "INSERT INTO keiro.keiro_projection_rebuild_sources (run_id, source_id, source_scope, target_position) VALUES ('constraint-run', 'bad-scope', 'category', 0)"+            )+        invalidScope `shouldSatisfy` isLeft+        missingSource <-+          Connection.use+            connection+            ( Session.script+                "INSERT INTO keiro.keiro_projection_rebuild_adapters (run_id, source_id, projection_id, adapter_order) VALUES ('constraint-run', 'missing', 'projection', 0)"+            )+        missingSource `shouldSatisfy` isLeft+        duplicateVerification <-+          Connection.use+            connection+            ( Session.script+                "INSERT INTO keiro.keiro_projection_rebuild_verifications (run_id, verification_id, verification_version) VALUES ('constraint-run', 'verify', 'v2')"+            )+        duplicateVerification `shouldSatisfy` isLeft+      either (expectationFailure . show) pure result+   describe "codd-ledger preflight" $ do     it "blocks a current codd ledger before native history exists" $       assertBlockedCoddPreflight "codd"@@ -418,9 +606,9 @@         up <- runMigrationPlan defaultRunOptions settings plan >>= requireRight         reportOutcomes up           `shouldBe` replicate 7 AlreadyApplied-            <> [AppliedNow]+            <> replicate 3 AppliedNow             <> replicate 16 AlreadyApplied-            <> replicate 4 AppliedNow+            <> replicate 14 AppliedNow          verifiedAfterUp <-           verifyMigrationPlan defaultRunOptions settings plan >>= requireRight@@ -429,6 +617,326 @@             verificationIssues `shouldBe` []         withConnection settings assertSchema +legacyReadModelFixtureSql :: Text+legacyReadModelFixtureSql =+  """+  INSERT INTO keiro.keiro_read_models+    (name, version, shape_hash, last_built_at, status, updated_at)+  VALUES+    ('legacy-live', 7, 'shape-live', '2026-01-01 00:00:00+00', 'live', '2026-01-01 00:00:00+00'),+    ('legacy-rebuilding', 8, 'shape-rebuilding', '2026-01-01 00:00:00+00', 'rebuilding', '2026-01-02 00:00:00+00'),+    ('legacy-abandoned', 9, 'shape-abandoned', '2026-01-01 00:00:00+00', 'abandoned', '2026-01-03 00:00:00+00');+  """++projectionStatusFixtureSql :: Text+projectionStatusFixtureSql =+  """+  INSERT INTO keiro.keiro_projection_rebuild_groups+    (group_id, slice_fingerprint, status, active_run_id,+     reads_allowed, writes_allowed, started_at)+  VALUES+    ('status-contract-group', 'slice-v1:status-contract', 'rebuilding',+     'status-contract-run', false, false, '2026-08-13 12:00:00+00');++  INSERT INTO keiro.keiro_projection_group_cursors+    (group_id, position_basis, subscription_names)+  VALUES ('status-contract-group', 'append', ARRAY[]::TEXT[]);++  INSERT INTO keiro.keiro_projection_rebuild_runs+    (run_id, group_id, catalog_fingerprint, group_slice_fingerprint,+     contract_fingerprint, runner_format, captured_head, page_size)+  VALUES+    ('status-contract-run', 'status-contract-group', 'catalog-status-contract',+     'slice-v1:status-contract', 'contract-v2:status-contract',+     'keiro/projection-replay/v2', 10, 100);++  INSERT INTO keiro.keiro_projection_rebuild_sources+    (run_id, source_id, source_scope, category, cursor_position, target_position)+  VALUES+    ('status-contract-run', 'orders', 'category', 'orders', 7, 10),+    ('status-contract-run', 'customers', 'category', 'customers', 4, 10);++  INSERT INTO keiro.keiro_read_models+    (name, version, shape_hash, status, rebuild_group_id)+  VALUES+    ('orders.summary', 1, 'shape-summary', 'rebuilding', 'status-contract-group'),+    ('orders.by-customer', 1, 'shape-by-customer', 'rebuilding', 'status-contract-group');+  """++projectionStatusColumnsStatement :: Statement () [(Text, Text)]+projectionStatusColumnsStatement =+  Statement.preparable+    """+    SELECT attribute.attname::TEXT, format_type(attribute.atttypid, attribute.atttypmod)+    FROM pg_catalog.pg_attribute AS attribute+    JOIN pg_catalog.pg_class AS relation+      ON relation.oid = attribute.attrelid+    JOIN pg_catalog.pg_namespace AS namespace+      ON namespace.oid = relation.relnamespace+    WHERE namespace.nspname = 'keiro_read'+      AND relation.relname = 'projection_group_status_v1'+      AND attribute.attnum > 0+      AND NOT attribute.attisdropped+    ORDER BY attribute.attnum+    """+    Encoders.noParams+    ( Decoders.rowList+        ( (,)+            <$> column Decoders.text+            <*> column Decoders.text+        )+    )+  where+    column = Decoders.column . Decoders.nonNullable++projectionStatusContractStatement :: Statement () Bool+projectionStatusContractStatement =+  Statement.preparable+    """+    SELECT+      lifecycle_phase = 'rebuilding'+      AND NOT reads_allowed+      AND NOT writes_allowed+      AND serving_revision_id IS NULL+      AND serving_epoch = 0+      AND serving_position_basis = 'append'+      AND serving_applied_position IS NULL+      AND active_run_id = 'status-contract-run'+      AND candidate_revision_id IS NULL+      AND candidate_rebuild_position = 4+      AND candidate_rebuild_head = 10+      AND query_models = ARRAY['orders.by-customer', 'orders.summary']::TEXT[]+      AND rebuild_started_at IS NOT NULL+      AND last_promoted_at IS NULL+      AND failed_at IS NULL+      AND failure_code IS NULL+      AND failure_detail IS NULL+    FROM keiro_read.projection_group_status_v1+    WHERE group_id = 'status-contract-group'+    """+    Encoders.noParams+    (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))++projectionReaderFactsStatement :: Statement () Bool+projectionReaderFactsStatement =+  Statement.preparable+    """+    SELECT+      has_schema_privilege(current_user, 'keiro_read', 'USAGE')+      AND has_table_privilege(+        current_user,+        'keiro_read.projection_group_status_v1',+        'SELECT'+      )+      AND NOT has_schema_privilege(current_user, 'keiro', 'USAGE')+      AND NOT has_schema_privilege(current_user, 'kiroku', 'USAGE')+      AND (+        SELECT count(*) = 1+        FROM keiro_read.projection_group_status_v1+        WHERE group_id = 'status-contract-group'+      )+    """+    Encoders.noParams+    (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))++backendPidStatement :: Statement () Int32+backendPidStatement =+  Statement.preparable+    "SELECT pg_backend_pid()"+    Encoders.noParams+    (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))++withProjectionReaderRole :: Connection.Connection -> IO value -> IO value+withProjectionReaderRole connection action = do+  backendPid <- useSession connection (Session.statement () backendPidStatement)+  let roleName = "keiro_projection_reader_test_" <> Text.pack (show backendPid)+      install =+        Text.unlines+          [ "CREATE ROLE " <> roleName <> " NOLOGIN;",+            "GRANT USAGE ON SCHEMA keiro_read TO " <> roleName <> ";",+            "GRANT SELECT ON keiro_read.projection_group_status_v1 TO " <> roleName <> ";",+            "SET ROLE " <> roleName <> ";"+          ]+      cleanup =+        Text.unlines+          [ "RESET ROLE;",+            "REVOKE ALL ON keiro_read.projection_group_status_v1 FROM " <> roleName <> ";",+            "REVOKE ALL ON SCHEMA keiro_read FROM " <> roleName <> ";",+            "DROP ROLE " <> roleName <> ";"+          ]+  useSession connection (Session.script install)+  action `finally` useSession connection (Session.script cleanup)++replayProgressFixtureSql :: Text+replayProgressFixtureSql =+  """+  INSERT INTO keiro.keiro_projection_rebuild_groups+    (group_id, slice_fingerprint, status, active_run_id, reads_allowed, writes_allowed)+  VALUES ('constraint-group', 'slice-v1:fixture', 'rebuilding', 'constraint-run', false, false);+  INSERT INTO keiro.keiro_projection_rebuild_runs+    (run_id, group_id, catalog_fingerprint, group_slice_fingerprint, contract_fingerprint,+     runner_format, captured_head, page_size)+  VALUES+    ('constraint-run', 'constraint-group', 'catalog-fingerprint',+     'slice-v1:fixture', 'contract-v2:fixture', 'keiro/projection-replay/v2', 0, 10);+  INSERT INTO keiro.keiro_projection_rebuild_sources+    (run_id, source_id, source_scope, category, target_position)+  VALUES ('constraint-run', 'source', 'category', 'orders', 0);+  INSERT INTO keiro.keiro_projection_rebuild_verifications+    (run_id, verification_id, verification_version)+  VALUES ('constraint-run', 'verify', 'v1');+  """++preCanonicalRebuildFixtureSql :: Text+preCanonicalRebuildFixtureSql =+  """+  INSERT INTO keiro.keiro_projection_rebuild_groups+    (group_id, catalog_fingerprint, status, active_run_id, requested_by, request_reason, started_at)+  VALUES+    ('upgrade-rebuilding', repeat('a', 64), 'rebuilding', 'upgrade-run-live', 'ops', 'mid-rebuild upgrade fixture', now()),+    ('upgrade-failed', repeat('b', 64), 'failed', 'upgrade-run-failed', 'ops', 'abandoned before upgrade', now());+  UPDATE keiro.keiro_projection_rebuild_groups+    SET failed_at = now(), failure_code = 'operator.abandoned', failure_detail = 'abandoned with the old binary'+    WHERE group_id = 'upgrade-failed';+  INSERT INTO keiro.keiro_projection_rebuild_runs+    (run_id, group_id, catalog_fingerprint, contract_fingerprint, runner_format, captured_head, page_size, status)+  VALUES+    ('upgrade-run-live', 'upgrade-rebuilding', repeat('a', 64), 'contract-v2:' || repeat('c', 64), 'keiro/projection-replay/v2', 6, 100, 'running');+  INSERT INTO keiro.keiro_projection_rebuild_runs+    (run_id, group_id, catalog_fingerprint, contract_fingerprint, runner_format, captured_head, page_size, status, failed_at, failure_code, failure_detail)+  VALUES+    ('upgrade-run-failed', 'upgrade-failed', repeat('b', 64), 'contract-v2:' || repeat('d', 64), 'keiro/projection-replay/v2', 6, 100, 'failed', now(), 'operator.abandoned', 'abandoned with the old binary');+  """++preStatusContractFixtureSql :: Text+preStatusContractFixtureSql =+  """+  INSERT INTO keiro.keiro_projection_rebuild_groups+    (group_id, slice_fingerprint, status, reads_allowed, writes_allowed)+  VALUES+    ('upgrade-status-group', 'slice-v6:upgrade-status', 'live', true, true);+  """++preStatusContractFactsStatement :: Statement () (Text, Int32, Text, Bool, Bool)+preStatusContractFactsStatement =+  Statement.preparable+    """+    SELECT cursors.position_basis,+           cardinality(cursors.subscription_names)::integer,+           status.serving_position_basis,+           status.serving_applied_position IS NULL,+           status.reads_allowed+    FROM keiro.keiro_projection_group_cursors AS cursors+    JOIN keiro_read.projection_group_status_v1 AS status+      ON status.group_id = cursors.group_id+    WHERE cursors.group_id = 'upgrade-status-group'+    """+    Encoders.noParams+    ( Decoders.singleRow+        ( (,,,,)+            <$> Decoders.column (Decoders.nonNullable Decoders.text)+            <*> Decoders.column (Decoders.nonNullable Decoders.int4)+            <*> Decoders.column (Decoders.nonNullable Decoders.text)+            <*> Decoders.column (Decoders.nonNullable Decoders.bool)+            <*> Decoders.column (Decoders.nonNullable Decoders.bool)+        )+    )++guardFunctionOidStatement :: Statement () Int64+guardFunctionOidStatement =+  Statement.preparable+    """+    SELECT 'keiro_read.guard_external_read_v1(text,integer)'::regprocedure::oid::bigint+    """+    Encoders.noParams+    (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8)))++upgradedGuardFactsStatement :: Statement () (Int64, Bool, Bool)+upgradedGuardFactsStatement =+  Statement.preparable+    """+    SELECT procedures.oid::bigint,+           pg_catalog.pg_get_functiondef(procedures.oid)+             LIKE '%group_serving_epoch_before%',+           NOT pg_catalog.has_function_privilege(+             'public',+             procedures.oid,+             'EXECUTE'+           )+    FROM pg_catalog.pg_proc AS procedures+    WHERE procedures.oid =+      'keiro_read.guard_external_read_v1(text,integer)'::regprocedure+    """+    Encoders.noParams+    ( Decoders.singleRow+        ( (,,)+            <$> Decoders.column (Decoders.nonNullable Decoders.int8)+            <*> Decoders.column (Decoders.nonNullable Decoders.bool)+            <*> Decoders.column (Decoders.nonNullable Decoders.bool)+        )+    )++preCanonicalRebuildShapeStatement :: Statement () [(Text, Text, Text, Text)]+preCanonicalRebuildShapeStatement =+  Statement.preparable+    """+    SELECT groups.group_id,+           groups.slice_fingerprint,+           runs.run_id,+           runs.group_slice_fingerprint+    FROM keiro.keiro_projection_rebuild_groups AS groups+    JOIN keiro.keiro_projection_rebuild_runs AS runs+      ON runs.group_id = groups.group_id+    WHERE groups.group_id IN ('upgrade-rebuilding', 'upgrade-failed')+    ORDER BY groups.group_id+    """+    Encoders.noParams+    ( Decoders.rowList+        ( (,,,)+            <$> column Decoders.text+            <*> column Decoders.text+            <*> column Decoders.text+            <*> column Decoders.text+        )+    )+  where+    column = Decoders.column . Decoders.nonNullable++legacyGroupUpgradeStatement ::+  Statement () [(Text, Int64, Text, Text, Text, Text, Maybe Text, Maybe Text)]+legacyGroupUpgradeStatement =+  Statement.preparable+    """+    SELECT rm.name,+           rm.version,+           rm.shape_hash,+           rm.status,+           rm.rebuild_group_id,+           rg.status,+           rg.active_run_id,+           rg.failure_detail+    FROM keiro.keiro_read_models AS rm+    JOIN keiro.keiro_projection_rebuild_groups AS rg+      ON rg.group_id = rm.rebuild_group_id+    ORDER BY rm.name+    """+    Encoders.noParams+    ( Decoders.rowList+        ( (,,,,,,,)+            <$> column Decoders.text+            <*> column Decoders.int8+            <*> column Decoders.text+            <*> column Decoders.text+            <*> column Decoders.text+            <*> column Decoders.text+            <*> nullableColumn Decoders.text+            <*> nullableColumn Decoders.text+        )+    )+  where+    column = Decoders.column . Decoders.nonNullable+    nullableColumn = Decoders.column . Decoders.nullable+ assertBlockedCoddPreflight :: Text -> Expectation assertBlockedCoddPreflight sourceSchema =   withKeiroPg $ \database -> do@@ -469,14 +977,14 @@     up <- runMigrationPlan defaultRunOptions settings plan >>= requireRight     reportOutcomes up       `shouldBe` replicate 7 AlreadyApplied-        <> [AppliedNow]+        <> replicate 3 AppliedNow         <> replicate 16 AlreadyApplied-        <> [AppliedNow, AppliedNow, AppliedNow, AppliedNow]+        <> replicate 14 AppliedNow     verifiedAfterCanaries <- verifyMigrationPlan defaultRunOptions settings plan >>= requireRight     case verifiedAfterCanaries of       VerificationReport verificationIssues _ _ _ -> verificationIssues `shouldBe` []     rerun <- runMigrationPlan defaultRunOptions settings plan >>= requireRight-    reportOutcomes rerun `shouldBe` replicate 28 AlreadyApplied+    reportOutcomes rerun `shouldBe` replicate 40 AlreadyApplied     second <-       importCoddHistory defaultImportOptions config provider plan frameworkCoddHistoryMappings         >>= requireRight@@ -486,7 +994,7 @@       sourceRows <- useSession connection (Session.statement () (sourceRowCountStatement sourceSchema))       sourceRows `shouldBe` 23       facts <- useSession connection (Session.statement () importFactsStatement)-      facts `shouldBe` (28, 23, True)+      facts `shouldBe` (40, 23, True)  postCoddImportPendingIssues :: IO [VerificationIssue] postCoddImportPendingIssues =@@ -497,10 +1005,22 @@      pendingNames =       [ ("kiroku", "0008-schema-management-comment"),+        ("kiroku", "0009"),+        ("kiroku", "0010"),         ("keiro", "0017-schema-management-comment"),         ("keiro", "0018"),         ("keiro", "0019-keiro-snapshots-state-shape-hash"),-        ("keiro", "0020-keiro-workflow-children-failure-reason")+        ("keiro", "0020-keiro-workflow-children-failure-reason"),+        ("keiro", "0021-keiro-workflows-exact-discovery"),+        ("keiro", "0022"),+        ("keiro", "0023"),+        ("keiro", "0024"),+        ("keiro", "0025"),+        ("keiro", "0026"),+        ("keiro", "0027"),+        ("keiro", "0028"),+        ("keiro", "0029"),+        ("keiro", "0030")       ]  assertPoisonedLedger :: Settings.Settings -> Expectation@@ -533,7 +1053,17 @@     "0017-schema-management-comment.sql",     "0018.sql",     "0019-keiro-snapshots-state-shape-hash.sql",-    "0020-keiro-workflow-children-failure-reason.sql"+    "0020-keiro-workflow-children-failure-reason.sql",+    "0021-keiro-workflows-exact-discovery.sql",+    "0022.sql",+    "0023.sql",+    "0024.sql",+    "0025.sql",+    "0026.sql",+    "0027.sql",+    "0028.sql",+    "0029.sql",+    "0030.sql"   ]  findMigrationsDirectory :: IO FilePath@@ -709,11 +1239,16 @@       (to_regnamespace('kiroku') IS NOT NULL),       (to_regclass('kiroku.events') IS NOT NULL),       (to_regnamespace('keiro') IS NOT NULL),+      (to_regnamespace('keiro_read') IS NOT NULL),       (to_regclass('keiro.keiro_inbox') IS NOT NULL),       (to_regclass('keiro.keiro_outbox') IS NOT NULL),       (to_regclass('keiro.keiro_timers') IS NOT NULL),       (to_regclass('keiro.keiro_workflows') IS NOT NULL),-      (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0008-schema-management-comment'),+      (to_regclass('keiro.keiro_projection_group_cursors') IS NOT NULL),+      (to_regclass('keiro_read.projection_group_status_v1') IS NOT NULL),+      (obj_description(to_regnamespace('keiro_read'), 'pg_namespace') =+        'Versioned, owner-rights read contracts for out-of-process Keiro consumers.'),+      (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0010'),       (obj_description(to_regnamespace('keiro'), 'pg_namespace') = 'Managed by pg-migrate component keiro through 0017-schema-management-comment')     ) AS checks(ok)     """