From 5496e010303728b310bf3476dd274e4025b8af83 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 20:34:44 -0700 Subject: [PATCH 1/2] fix: route the usage table through getTableName and make setup.sql re-applicable (GIT-84) Scar usage was written to and read from the literal table "scar_usage". That table does not exist on a customer store (setup.sql creates gitmem_scar_usage), so every usage write 404'd and the loss went unnoticed. On a blank Supabase, gitmem_scar_usage stayed at 0 rows after recall, confirm_scars and session_close. - record-scar-usage, record-scar-usage-batch, analytics and behavioral-decay now use getTableName("scar_usage"). The two storage.upsert("scar_usage") sites are unchanged: the storage layer already applies the prefix. - Guard test: fails on a bare table-name literal passed to directUpsert, directQuery, directQueryAll or directPatch, in a `table:` option, or in a REST URL. It also fails if storage is handed an already-prefixed name. Literals that setup.sql creates under exactly that name are allowlisted pending a ruling. - setup.sql: - refresh_scar_behavioral_scores() only rewrites rows whose decay_multiplier actually changes. Otherwise, once usage rows land, it bumps updated_at on every session_start and invalidates the GIT-98 disk cache. - Adds the missing DROP POLICY IF EXISTS for gitmem_licenses and gitmem_license_activations, so a second run no longer fails at statement 72. - schema/migrations/GIT-84-ntegs-scar-usage-rename.sql: NOT EXECUTED. Needs human review. nTEG's install (prefix orchestra_) keeps usage in plain scar_usage; this renames it to orchestra_scar_usage and leaves a security_invoker compatibility view. Tests: +8 (4 table-name guard, 4 setup.sql re-apply safety). Co-Authored-By: Claude Opus 5 --- .../GIT-84-ntegs-scar-usage-rename.sql | 104 ++++++++++++++++ schema/setup.sql | 10 +- src/services/analytics.ts | 2 +- src/services/behavioral-decay.ts | 3 +- src/tools/record-scar-usage-batch.ts | 2 +- src/tools/record-scar-usage.ts | 4 +- tests/unit/schema-setup-reapply.test.ts | 57 +++++++++ tests/unit/table-name-literals.test.ts | 115 ++++++++++++++++++ 8 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 schema/migrations/GIT-84-ntegs-scar-usage-rename.sql create mode 100644 tests/unit/schema-setup-reapply.test.ts create mode 100644 tests/unit/table-name-literals.test.ts diff --git a/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql b/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql new file mode 100644 index 0000000..73d0fbe --- /dev/null +++ b/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql @@ -0,0 +1,104 @@ +-- ============================================================================ +-- GIT-84 — nTEG production only: rename the usage table to match the prefix +-- ============================================================================ +-- +-- ⚠️ HUMAN REVIEW REQUIRED. NOT EXECUTED ANYWHERE. NOT FOR CUSTOMER STORES. +-- +-- WHO NEEDS THIS +-- Only nTEG's own install, which runs with GITMEM_TABLE_PREFIX=orchestra_ but +-- whose usage table is named plain `scar_usage`. Customer stores created from +-- schema/setup.sql already have `gitmem_scar_usage` and need nothing. +-- +-- WHY +-- Before GIT-84 the code wrote and read the literal table "scar_usage". After +-- GIT-84 it uses getTableName("scar_usage"), which on nTEG's install resolves +-- to `orchestra_scar_usage`. Upgrading nTEG's install without this migration +-- makes every usage write and read 404 there — the same silent loss GIT-84 +-- fixes for customers. +-- +-- WHAT IT DOES (one transaction; aborts on any surprise) +-- 1. Asserts public.scar_usage is a table and public.orchestra_scar_usage does +-- not exist yet. +-- 2. Renames scar_usage -> orchestra_scar_usage. Indexes, constraints, RLS +-- policies, grants and triggers move with the table. +-- 3. Creates a compatibility VIEW named scar_usage over the renamed table, so +-- anything still using the old name (older gitmem clients, SQL functions, +-- edge functions, dashboards) keeps working unchanged. security_invoker +-- makes the view enforce the underlying table's RLS for the caller; without +-- it the view would run as its owner and bypass RLS. +-- 4. Lists (RAISE NOTICE) every function in public whose body mentions +-- scar_usage, for the reviewer to update at leisure. PL/pgSQL resolves +-- table names at execution time, so those functions keep working through +-- the view in the meantime. +-- +-- WHAT IT DOES NOT DO +-- - It does not rewrite nTEG's refresh_scar_behavioral_scores(). setup.sql's +-- copy gained an `IS DISTINCT FROM` guard in GIT-84 so unchanged rows are not +-- rewritten (which invalidated GIT-98's disk cache on every session_start). +-- nTEG's copy reads orchestra_* tables and was not inspected; apply the same +-- guard there by hand after reviewing its current definition (section 5). +-- +-- BEFORE RUNNING +-- - Take a backup / confirm PITR. +-- - Run against a branch or staging copy first. +-- - Check the NOTICE output from step 4 and any edge functions that query +-- /rest/v1/scar_usage (they will keep working through the view). +-- +-- ROLLBACK +-- BEGIN; +-- DROP VIEW public.scar_usage; +-- ALTER TABLE public.orchestra_scar_usage RENAME TO scar_usage; +-- COMMIT; +-- ============================================================================ + +BEGIN; + +-- 1. Preconditions +DO $$ +BEGIN + IF to_regclass('public.scar_usage') IS NULL THEN + RAISE EXCEPTION 'GIT-84: public.scar_usage not found — wrong database, or already migrated'; + END IF; + IF (SELECT relkind FROM pg_class WHERE oid = 'public.scar_usage'::regclass) <> 'r' THEN + RAISE EXCEPTION 'GIT-84: public.scar_usage is not a plain table (already migrated to a view?)'; + END IF; + IF to_regclass('public.orchestra_scar_usage') IS NOT NULL THEN + RAISE EXCEPTION 'GIT-84: public.orchestra_scar_usage already exists — refusing to overwrite'; + END IF; +END $$; + +-- 2. Rename (policies, indexes, constraints, grants, triggers follow the table) +ALTER TABLE public.scar_usage RENAME TO orchestra_scar_usage; + +-- 3. Compatibility alias under the old name. A single-table SELECT * view is +-- auto-updatable, so INSERT/UPDATE/DELETE through it still work. +CREATE VIEW public.scar_usage + WITH (security_invoker = true) + AS SELECT * FROM public.orchestra_scar_usage; + +COMMENT ON VIEW public.scar_usage IS + 'GIT-84 compatibility alias for orchestra_scar_usage. Drop once nothing reads the old name.'; + +-- Match the table's grants for the Supabase API roles (RLS still applies via security_invoker). +GRANT SELECT, INSERT, UPDATE, DELETE ON public.scar_usage TO service_role; + +-- 4. Report functions that still name the old table (informational) +DO $$ +DECLARE r record; +BEGIN + FOR r IN + SELECT p.oid::regprocedure AS fn + FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'public' + AND p.prosrc ~ '\mscar_usage\M' + LOOP + RAISE NOTICE 'GIT-84: function % mentions scar_usage (works via the view; update when convenient)', r.fn; + END LOOP; +END $$; + +-- 5. (Manual, after review) Apply the GIT-84 guard to nTEG's +-- refresh_scar_behavioral_scores(): add to its UPDATE ... WHERE clause +-- AND l.decay_multiplier IS DISTINCT FROM +-- so rows whose multiplier does not change are not rewritten. + +COMMIT; diff --git a/schema/setup.sql b/schema/setup.sql index badd255..70cd62e 100644 --- a/schema/setup.sql +++ b/schema/setup.sql @@ -404,7 +404,12 @@ BEGIN updated_at = NOW() FROM usage_stats us WHERE l.id = us.scar_id - AND COALESCE(l.is_active, true) = true; + AND COALESCE(l.is_active, true) = true + -- Only touch rows whose multiplier actually changes. Rewriting unchanged + -- rows bumped updated_at on every session_start, which invalidates every + -- client's on-disk embedding cache (GIT-98) for no reason. + AND l.decay_multiplier IS DISTINCT FROM + GREATEST(0.1, 1.0 - (us.times_dismissed::FLOAT / us.times_surfaced::FLOAT) * 0.8); GET DIAGNOSTICS v_updated = ROW_COUNT; @@ -529,9 +534,12 @@ CREATE TABLE IF NOT EXISTS gitmem_license_activations ( ALTER TABLE gitmem_licenses ENABLE ROW LEVEL SECURITY; ALTER TABLE gitmem_license_activations ENABLE ROW LEVEL SECURITY; +-- Drop-then-create so setup.sql can be re-applied (CREATE POLICY has no IF NOT EXISTS) +DROP POLICY IF EXISTS "Service role full access" ON gitmem_licenses; CREATE POLICY "Service role full access" ON gitmem_licenses FOR ALL USING (auth.role() = 'service_role'); +DROP POLICY IF EXISTS "Service role full access" ON gitmem_license_activations; CREATE POLICY "Service role full access" ON gitmem_license_activations FOR ALL USING (auth.role() = 'service_role'); diff --git a/src/services/analytics.ts b/src/services/analytics.ts index a1a637e..e42e827 100644 --- a/src/services/analytics.ts +++ b/src/services/analytics.ts @@ -201,7 +201,7 @@ export async function queryScarUsageByDateRange( surfaced_at: `gte.${startDate}`, }; - return directQueryAll("scar_usage", { + return directQueryAll(getTableName("scar_usage"), { select: "scar_id,scar_title,scar_severity,agent,reference_type,execution_successful,surfaced_at", filters, order: "surfaced_at.desc", diff --git a/src/services/behavioral-decay.ts b/src/services/behavioral-decay.ts index f29071f..652e715 100644 --- a/src/services/behavioral-decay.ts +++ b/src/services/behavioral-decay.ts @@ -12,6 +12,7 @@ */ import { isConfigured, safeInFilter } from "./supabase-client.js"; +import { getTableName } from "./tier.js"; const SUPABASE_URL = process.env.SUPABASE_URL || ""; const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY || ""; @@ -98,7 +99,7 @@ export async function fetchDismissalCounts( try { // Query scar_usage for the given scar IDs (last 90 days) - const url = new URL(`${SUPABASE_REST_URL}/scar_usage`); + const url = new URL(`${SUPABASE_REST_URL}/${getTableName("scar_usage")}`); url.searchParams.set("select", "scar_id,reference_type"); url.searchParams.set("scar_id", safeInFilter(scarIds)); url.searchParams.set("surfaced_at", `gte.${new Date(Date.now() - 90 * 86400000).toISOString()}`); diff --git a/src/tools/record-scar-usage-batch.ts b/src/tools/record-scar-usage-batch.ts index b50c5ab..01c3783 100644 --- a/src/tools/record-scar-usage-batch.ts +++ b/src/tools/record-scar-usage-batch.ts @@ -161,7 +161,7 @@ export async function recordScarUsageBatch( // Insert all usage records in parallel const insertPromises = usageRecords.map(async (record) => { const result = await supabase.directUpsert<{ id: string }>( - "scar_usage", + getTableName("scar_usage"), record ); return result?.id || null; diff --git a/src/tools/record-scar-usage.ts b/src/tools/record-scar-usage.ts index b6e5146..40ca688 100644 --- a/src/tools/record-scar-usage.ts +++ b/src/tools/record-scar-usage.ts @@ -10,7 +10,7 @@ import { v4 as uuidv4 } from "uuid"; import { wrapDisplay } from "../services/display-protocol.js"; import * as supabase from "../services/supabase-client.js"; -import { hasSupabase } from "../services/tier.js"; +import { hasSupabase, getTableName } from "../services/tier.js"; import { getStorage } from "../services/storage.js"; import { detectAgent } from "../services/agent-detection.js"; import { getCurrentSession } from "../services/session-state.js"; @@ -61,7 +61,7 @@ export async function recordScarUsage( if (hasSupabase()) { const upsertStart = Date.now(); - await supabase.directUpsert("scar_usage", usageData); + await supabase.directUpsert(getTableName("scar_usage"), usageData); breakdown.upsert = { latency_ms: Date.now() - upsertStart, source: "supabase", diff --git a/tests/unit/schema-setup-reapply.test.ts b/tests/unit/schema-setup-reapply.test.ts new file mode 100644 index 0000000..af11f8e --- /dev/null +++ b/tests/unit/schema-setup-reapply.test.ts @@ -0,0 +1,57 @@ +/** + * schema/setup.sql must be safe to apply twice (GIT-84). + * + * Customers re-run setup.sql to pick up schema changes. CREATE POLICY and + * CREATE TRIGGER have no IF NOT EXISTS, so each needs a DROP ... IF EXISTS + * first; one missing pair (gitmem_licenses) made the second run fail at + * statement 72 on a real Supabase. + * + * Also pins the GIT-84 guard on refresh_scar_behavioral_scores(): rows whose + * decay_multiplier would not change must not be rewritten, because the rewrite + * bumps updated_at and invalidates every client's GIT-98 disk cache. + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync } from "fs"; +import { join } from "path"; + +const SQL = readFileSync(join(__dirname, "../../schema/setup.sql"), "utf-8") + // strip -- comments so commented-out statements don't count + .replace(/--[^\n]*/g, ""); + +const norm = (s: string) => s.replace(/\s+/g, " ").trim().toLowerCase(); + +describe("schema/setup.sql re-apply safety (GIT-84)", () => { + it("drops every policy before creating it", () => { + const created = [...SQL.matchAll(/CREATE POLICY\s+("[^"]+")\s+ON\s+([\w.]+)/gi)].map((m) => `${m[1]} ON ${m[2]}`); + expect(created.length).toBeGreaterThan(0); + const missing = created.filter((p) => { + const [name, table] = p.split(" ON "); + const drop = new RegExp(`DROP POLICY IF EXISTS\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+ON\\s+${table}\\s*;`, "i"); + const dropAt = SQL.search(drop); + const createAt = SQL.search(new RegExp(`CREATE POLICY\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+ON\\s+${table}\\b`, "i")); + return dropAt < 0 || dropAt > createAt; + }); + expect(missing).toEqual([]); + }); + + it("drops every trigger before creating it", () => { + const created = [...SQL.matchAll(/CREATE TRIGGER\s+(\w+)[\s\S]*?\bON\s+([\w.]+)/gi)].map((m) => [m[1], m[2]]); + expect(created.length).toBeGreaterThan(0); + const missing = created.filter(([name, table]) => !new RegExp(`DROP TRIGGER IF EXISTS\\s+${name}\\s+ON\\s+${table}\\s*;`, "i").test(SQL)); + expect(missing).toEqual([]); + }); + + it("creates tables and indexes with IF NOT EXISTS", () => { + const bare = [...SQL.matchAll(/CREATE\s+(?:UNIQUE\s+)?(TABLE|INDEX)\s+(?!IF NOT EXISTS)(\w+)/gi)].map((m) => `${m[1]} ${m[2]}`); + expect(bare).toEqual([]); + }); + + it("refresh_scar_behavioral_scores() only rewrites rows whose multiplier changes", () => { + const fn = SQL.slice(SQL.search(/CREATE OR REPLACE FUNCTION refresh_scar_behavioral_scores/i)); + const update = norm(fn.slice(0, fn.indexOf("GET DIAGNOSTICS"))); + const expr = "greatest(0.1, 1.0 - (us.times_dismissed::float / us.times_surfaced::float) * 0.8)"; + expect(update).toContain(`set decay_multiplier = ${expr}`); + expect(update).toContain(`and l.decay_multiplier is distinct from ${expr}`); + }); +}); diff --git a/tests/unit/table-name-literals.test.ts b/tests/unit/table-name-literals.test.ts new file mode 100644 index 0000000..569f046 --- /dev/null +++ b/tests/unit/table-name-literals.test.ts @@ -0,0 +1,115 @@ +/** + * Guard: Supabase table names must go through getTableName() (GIT-84). + * + * The user schema is prefixed (gitmem_ by default, GITMEM_TABLE_PREFIX to + * override). A bare literal like "scar_usage" passed straight to PostgREST + * silently 404s on every customer project — that is how scar usage was lost. + * + * Checked: + * - directUpsert / directQuery / directQueryAll / directPatch whose table + * argument is a string literal + * - `table: "..."` option objects (listRecords and friends) + * - REST URLs built as `${SUPABASE_REST_URL}/name` or `/rest/v1/name` + * The storage layer (getStorage().upsert/query/get) takes a COLLECTION name + * and applies the prefix itself, so there the rule is reversed: passing a + * prefixed name or getTableName() would double-prefix. + * + * Known literals awaiting a ruling are allowlisted below, each with a reason. + * Removing an entry is the point — the list should only shrink. + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync, readdirSync, statSync } from "fs"; +import { join, relative } from "path"; + +const SRC = join(__dirname, "../../src"); + +function collectTsFiles(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) out.push(...collectTsFiles(full)); + else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".d.ts")) out.push(full); + } + return out; +} + +/** + * Literal table names that setup.sql creates under exactly that name (or that + * live outside the user schema). Changing the code alone would break them, so + * they stay literal until ruled on. Keyed by table name. + */ +const ALLOWED_LITERALS: Record = { + gitmem_query_metrics: "setup.sql creates it with a hard-coded gitmem_ prefix — ruling needed (GIT-84 sweep)", + knowledge_triples: "setup.sql creates it unprefixed — ruling needed (GIT-84 sweep)", + scar_enforcement_variants: "setup.sql creates it unprefixed; column drift tracked in GIT-106", + variant_assignments: "not created by setup.sql at all — ruling needed (GIT-84 sweep)", + variant_performance_metrics: "not created by setup.sql at all — ruling needed (GIT-84 sweep)", + community_feedback: "gitmem's own feedback endpoint (feedback-remote.ts), not the user schema", +}; + +/** Files that build REST URLs for diagnostics with a literal prefix — ruling needed (GIT-84 sweep). */ +const ALLOWED_URL_FILES = new Set(["src/commands/check.ts"]); + +const DIRECT_CALL = /\b(directUpsert|directQuery|directQueryAll|directPatch)\s*(?:<(?:[^<>]|<[^<>]*>)*>)?\s*\(\s*(["'`])([A-Za-z_][\w]*)\2/g; +const TABLE_OPTION = /\btable:\s*(["'`])([A-Za-z_][\w]*)\1/g; +const REST_URL = /(?:SUPABASE_REST_URL\}|\/rest\/v1)\/([A-Za-z_][\w]*)/g; +const STORAGE_CALL = /\b(?:storage|getStorage\(\))\s*\.\s*(upsert|query|get)\s*(?:<(?:[^<>]|<[^<>]*>)*>)?\s*\(\s*([^,)]+)/g; + +interface Finding { file: string; line: number; text: string } + +function scan(): { bare: Finding[]; doublePrefixed: Finding[]; allowlistedSeen: Set } { + const bare: Finding[] = []; + const doublePrefixed: Finding[] = []; + const allowlistedSeen = new Set(); + + for (const full of collectTsFiles(SRC)) { + const file = relative(join(SRC, ".."), full); + const src = readFileSync(full, "utf-8"); + const lineOf = (i: number) => src.slice(0, i).split("\n").length; + const flag = (name: string, i: number, text: string) => { + if (name in ALLOWED_LITERALS) { allowlistedSeen.add(name); return; } + bare.push({ file, line: lineOf(i), text }); + }; + + for (const m of src.matchAll(DIRECT_CALL)) flag(m[3], m.index!, `${m[1]}("${m[3]}")`); + for (const m of src.matchAll(TABLE_OPTION)) flag(m[2], m.index!, `table: "${m[2]}"`); + for (const m of src.matchAll(REST_URL)) { + if (m[1] === "rpc") continue; // RPC function names are not tables + if (ALLOWED_URL_FILES.has(file)) continue; + flag(m[1], m.index!, `REST /${m[1]}`); + } + for (const m of src.matchAll(STORAGE_CALL)) { + const arg = m[2].trim(); + if (/getTableName\s*\(/.test(arg) || /^["'`]gitmem_/.test(arg)) { + doublePrefixed.push({ file, line: lineOf(m.index!), text: `storage.${m[1]}(${arg})` }); + } + } + } + return { bare, doublePrefixed, allowlistedSeen }; +} + +describe("Supabase table names go through getTableName() (GIT-84)", () => { + const { bare, doublePrefixed, allowlistedSeen } = scan(); + const fmt = (fs: Finding[]) => fs.map((f) => `${f.file}:${f.line} ${f.text}`).join("\n"); + + it("passes no bare table-name literal to directUpsert/directQuery/directQueryAll/directPatch, table: options or REST URLs", () => { + expect(fmt(bare), "wrap these in getTableName()").toBe(""); + }); + + it("never hands the storage layer an already-prefixed name (it prefixes collections itself)", () => { + expect(fmt(doublePrefixed), "pass the bare collection name to storage").toBe(""); + }); + + it("keeps the allowlist honest: every entry is still in use", () => { + const stale = Object.keys(ALLOWED_LITERALS).filter((name) => !allowlistedSeen.has(name)); + expect(stale, "remove allowlist entries that no longer occur").toEqual([]); + }); + + it("would catch the GIT-84 bug (self-test on a synthetic source)", () => { + const sample = `await supabase.directUpsert<{ id: string }>(\n "scar_usage",\n record\n);`; + const hits = [...sample.matchAll(DIRECT_CALL)].map((m) => m[3]); + expect(hits).toEqual(["scar_usage"]); + expect([...`new URL(\`\${SUPABASE_REST_URL}/scar_usage\`)`.matchAll(REST_URL)].map((m) => m[1])).toEqual(["scar_usage"]); + }); +}); From b0a5c6cc525990c18600549d7db6ed0fd3dfb492 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 21:31:02 -0700 Subject: [PATCH 2/2] chore: move the nTEG-only scar_usage migration out of the public package (GIT-84) schema/ ships in the npm package, so the nTEG-specific rename migration would have gone to every customer. It now lives in the private orchestra repo (ops/migrations/). nTEG's own upgrade still needs that privately held migration before it runs this release. Co-Authored-By: Claude Opus 5 --- .../GIT-84-ntegs-scar-usage-rename.sql | 104 ------------------ 1 file changed, 104 deletions(-) delete mode 100644 schema/migrations/GIT-84-ntegs-scar-usage-rename.sql diff --git a/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql b/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql deleted file mode 100644 index 73d0fbe..0000000 --- a/schema/migrations/GIT-84-ntegs-scar-usage-rename.sql +++ /dev/null @@ -1,104 +0,0 @@ --- ============================================================================ --- GIT-84 — nTEG production only: rename the usage table to match the prefix --- ============================================================================ --- --- ⚠️ HUMAN REVIEW REQUIRED. NOT EXECUTED ANYWHERE. NOT FOR CUSTOMER STORES. --- --- WHO NEEDS THIS --- Only nTEG's own install, which runs with GITMEM_TABLE_PREFIX=orchestra_ but --- whose usage table is named plain `scar_usage`. Customer stores created from --- schema/setup.sql already have `gitmem_scar_usage` and need nothing. --- --- WHY --- Before GIT-84 the code wrote and read the literal table "scar_usage". After --- GIT-84 it uses getTableName("scar_usage"), which on nTEG's install resolves --- to `orchestra_scar_usage`. Upgrading nTEG's install without this migration --- makes every usage write and read 404 there — the same silent loss GIT-84 --- fixes for customers. --- --- WHAT IT DOES (one transaction; aborts on any surprise) --- 1. Asserts public.scar_usage is a table and public.orchestra_scar_usage does --- not exist yet. --- 2. Renames scar_usage -> orchestra_scar_usage. Indexes, constraints, RLS --- policies, grants and triggers move with the table. --- 3. Creates a compatibility VIEW named scar_usage over the renamed table, so --- anything still using the old name (older gitmem clients, SQL functions, --- edge functions, dashboards) keeps working unchanged. security_invoker --- makes the view enforce the underlying table's RLS for the caller; without --- it the view would run as its owner and bypass RLS. --- 4. Lists (RAISE NOTICE) every function in public whose body mentions --- scar_usage, for the reviewer to update at leisure. PL/pgSQL resolves --- table names at execution time, so those functions keep working through --- the view in the meantime. --- --- WHAT IT DOES NOT DO --- - It does not rewrite nTEG's refresh_scar_behavioral_scores(). setup.sql's --- copy gained an `IS DISTINCT FROM` guard in GIT-84 so unchanged rows are not --- rewritten (which invalidated GIT-98's disk cache on every session_start). --- nTEG's copy reads orchestra_* tables and was not inspected; apply the same --- guard there by hand after reviewing its current definition (section 5). --- --- BEFORE RUNNING --- - Take a backup / confirm PITR. --- - Run against a branch or staging copy first. --- - Check the NOTICE output from step 4 and any edge functions that query --- /rest/v1/scar_usage (they will keep working through the view). --- --- ROLLBACK --- BEGIN; --- DROP VIEW public.scar_usage; --- ALTER TABLE public.orchestra_scar_usage RENAME TO scar_usage; --- COMMIT; --- ============================================================================ - -BEGIN; - --- 1. Preconditions -DO $$ -BEGIN - IF to_regclass('public.scar_usage') IS NULL THEN - RAISE EXCEPTION 'GIT-84: public.scar_usage not found — wrong database, or already migrated'; - END IF; - IF (SELECT relkind FROM pg_class WHERE oid = 'public.scar_usage'::regclass) <> 'r' THEN - RAISE EXCEPTION 'GIT-84: public.scar_usage is not a plain table (already migrated to a view?)'; - END IF; - IF to_regclass('public.orchestra_scar_usage') IS NOT NULL THEN - RAISE EXCEPTION 'GIT-84: public.orchestra_scar_usage already exists — refusing to overwrite'; - END IF; -END $$; - --- 2. Rename (policies, indexes, constraints, grants, triggers follow the table) -ALTER TABLE public.scar_usage RENAME TO orchestra_scar_usage; - --- 3. Compatibility alias under the old name. A single-table SELECT * view is --- auto-updatable, so INSERT/UPDATE/DELETE through it still work. -CREATE VIEW public.scar_usage - WITH (security_invoker = true) - AS SELECT * FROM public.orchestra_scar_usage; - -COMMENT ON VIEW public.scar_usage IS - 'GIT-84 compatibility alias for orchestra_scar_usage. Drop once nothing reads the old name.'; - --- Match the table's grants for the Supabase API roles (RLS still applies via security_invoker). -GRANT SELECT, INSERT, UPDATE, DELETE ON public.scar_usage TO service_role; - --- 4. Report functions that still name the old table (informational) -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT p.oid::regprocedure AS fn - FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace - WHERE n.nspname = 'public' - AND p.prosrc ~ '\mscar_usage\M' - LOOP - RAISE NOTICE 'GIT-84: function % mentions scar_usage (works via the view; update when convenient)', r.fn; - END LOOP; -END $$; - --- 5. (Manual, after review) Apply the GIT-84 guard to nTEG's --- refresh_scar_behavioral_scores(): add to its UPDATE ... WHERE clause --- AND l.decay_multiplier IS DISTINCT FROM --- so rows whose multiplier does not change are not rewritten. - -COMMIT;