ceo-api/drizzle/0005_projection_kernel.sql
valentinbvro eb652446f6 feat: Sprint 1 -- Projection Kernel (read models pentru dashboard)
Repara criteriul de acceptare "dashboardurile citesc read models, nu
interogheaza haotic toate modulele".

- projection_processed_events (unique: name+version+event_id) = garantia de
  idempotency. Checkpointul e doar optimizare de scanare, nu corectitudine:
  rescanam cu un safety lag de 60s si sarim ce s-a aplicat deja, ca sa nu
  pierdem evenimente comise dupa unul cu created_at mai mare.
- ProjectionRegistry cu ProjectionDefinition (name, version, subscribedEvents,
  rebuildStrategy, apply, rebuildTenant).
- executive_dashboard foloseste rebuildStrategy 'canonical-tables':
  recalculeaza contorii din tabelele canonice, nu incrementeaza. Contorii
  incrementali pot deriva daca un eveniment se pierde sau se dubleaza;
  recalcularea e corecta prin constructie si idempotenta natural. Costul e o
  interogare per eveniment relevant -- ok la volumul actual.
- GET /v1/dashboard/executive citeste proiectia. Cand proiectia inca n-a rulat
  pentru workspace, intoarce status 'building' explicit, NU zerouri care ar
  parea date reale.
- POST /v1/dashboard/executive/rebuild -- owner/admin, auditat.
- Serviciile de taskuri/organizatii/segmente emit acum evenimente in outbox
  (in aceeasi tranzactie cu scrierea). Fara ele proiectia era cod mort.
- Campurile din spec care depind de module neconstruite (documents,
  transactions, approvals) raman 0 explicit, nu inventate.
2026-07-29 12:03:34 +02:00

47 lines
2.1 KiB
SQL

-- Projection Kernel (Sprint 1). Read models: NU sunt sursa de adevar, pot fi
-- sterse si reconstruite oricand din tabelele canonice.
-- Garantia de idempotency: acelasi eveniment nu se aplica de doua ori.
CREATE TABLE IF NOT EXISTS "projection_processed_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"projection_name" text NOT NULL,
"projection_version" integer NOT NULL,
"event_id" uuid NOT NULL,
"processed_at" timestamp DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "projection_processed_uq"
ON "projection_processed_events" ("projection_name","projection_version","event_id");
-- Checkpoint = optimizare (de unde reia scanarea), nu garantia de corectitudine.
CREATE TABLE IF NOT EXISTS "projection_checkpoints" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"projection_name" text NOT NULL,
"projection_version" integer NOT NULL,
"last_event_at" timestamp,
"last_event_id" uuid,
"status" text DEFAULT 'active' NOT NULL,
"last_error" text,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "projection_checkpoint_uq"
ON "projection_checkpoints" ("projection_name","projection_version");
CREATE TABLE IF NOT EXISTS "executive_dashboard_projection" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"workspace_id" uuid NOT NULL,
"open_tasks_count" integer DEFAULT 0 NOT NULL,
"overdue_tasks_count" integer DEFAULT 0 NOT NULL,
"deadlines_next_7_days" integer DEFAULT 0 NOT NULL,
"organizations_count" integer DEFAULT 0 NOT NULL,
"segments_count" integer DEFAULT 0 NOT NULL,
"research_briefs_count" integer DEFAULT 0 NOT NULL,
"documents_missing_count" integer DEFAULT 0 NOT NULL,
"transactions_unclassified_count" integer DEFAULT 0 NOT NULL,
"pending_approvals_count" integer DEFAULT 0 NOT NULL,
"generated_at" timestamp DEFAULT now() NOT NULL,
"projection_version" integer DEFAULT 1 NOT NULL
);
-- Cheia de UPSERT (spec §5.3): o linie per tenant+workspace.
CREATE UNIQUE INDEX IF NOT EXISTS "executive_dashboard_tenant_ws_uq"
ON "executive_dashboard_projection" ("tenant_id","workspace_id");