packages feed

kioku-migrations-0.1.0.0: migrations/0010-kioku-scope-identity-recompute.sql

-- codd: in-txn

-- Migration: kioku-scope-identity-recompute
-- Created: 2026-07-11-18-18-36 UTC
--
-- Scene and persona primary keys are derived from the scope. The old derivation joined
-- namespace/kind/ref with '/' and no escaping, so two genuinely different scopes could
-- produce one id:
--
--   ScopeGlobal (Namespace 'a/b/c')                 -> 'a/b/c'
--   ScopeEntity (Namespace 'a') (ScopeKind 'b') 'c' -> 'a/b/c'
--
-- Both then wrote the same row, and since the upserts do not update the scope columns on
-- conflict, the second scope's body landed on a row still attributed to the first.
--
-- Kioku.Distill.ScopeIdentity now percent-escapes each component before joining. This
-- migration brings persisted ids in line with it. Components containing none of '%', '/' or
-- ':' escape to themselves, so almost every database has nothing to rewrite -- the WHERE
-- clauses below match only the ambiguous rows.
--
-- A row that had absorbed a second scope's content keeps the id derived from its *stored*
-- scope columns, i.e. it stays with the first writer. The second scope's scene or persona
-- simply regenerates under its own, now-distinct id on the next distillation timer: scenes
-- and personas are caches of kioku_memories, not records of truth.
--
-- Re-running recomputes the same values, so this is idempotent.
SET search_path TO kiroku, pg_catalog;

CREATE OR REPLACE FUNCTION pg_temp.kioku_escape_scope_component(component text)
RETURNS text
LANGUAGE sql IMMUTABLE
AS $$
  -- '%' first, or the encoding is not injective.
  SELECT replace(replace(replace(component, '%', '%25'), '/', '%2F'), ':', '%3A')
$$;

-- The COALESCE collapses the kind/ref segment for global rows. The scope-pair CHECK added by
-- the schema-hardening migration guarantees kind and ref are NULL together, so there is no
-- half-populated case to worry about.
CREATE OR REPLACE FUNCTION pg_temp.kioku_scope_identity(namespace text, scope_kind text, scope_ref text)
RETURNS text
LANGUAGE sql IMMUTABLE
AS $$
  SELECT pg_temp.kioku_escape_scope_component(namespace)
      || COALESCE(
           '/' || pg_temp.kioku_escape_scope_component(scope_kind)
               || '/' || pg_temp.kioku_escape_scope_component(scope_ref),
           ''
         )
$$;

UPDATE kioku_scenes
   SET scene_id = 'kioku_scene:'
               || pg_temp.kioku_scope_identity(namespace, scope_kind, scope_ref)
               || ':'
               || pg_temp.kioku_escape_scope_component(scene_key)
 WHERE namespace ~ '[%/:]'
    OR scope_kind ~ '[%/:]'
    OR scope_ref ~ '[%/:]'
    OR scene_key ~ '[%/:]';

UPDATE kioku_personas
   SET persona_id = 'kioku_persona:'
                 || pg_temp.kioku_scope_identity(namespace, scope_kind, scope_ref)
 WHERE namespace ~ '[%/:]'
    OR scope_kind ~ '[%/:]'
    OR scope_ref ~ '[%/:]';