Files
paliad/internal/db/migrations/047_deadline_search_view.down.sql
m b45278b060 feat(t-paliad-131): Phase C — search backend (matview + service + handler)
Closes the search half of the unified Fristenrechner. Phase D (concept-card
UI on /tools/fristenrechner) follows in a subsequent shift.

Migration 047:
  - Seed the missing `wiedereinsetzung` concept and re-point the four
    Wiedereinsetzung trigger_events (200..203) at it. PR-7 referenced
    the slug `re-establishment-of-rights` but never seeded the concept,
    so the four cross-cutting triggers were dropping out of any concept-
    JOINing query. Per m's slug rule (Q1: shared cross-cutting concepts
    use DE slug because German term dominates HLC vocabulary).
  - Create paliad.deadline_search materialised view: UNION ALL of
    (deadline_rules joined to deadline_concepts) and (trigger_events
    joined to deadline_concepts via slug). Trigram GIN indexes on
    legal_source / concept_name_de / concept_name_en / rule_name_de /
    rule_name_en / rule_code; gin (concept_aliases) for array
    containment; UNIQUE INDEX on a synthetic row_key so refresh can
    run CONCURRENTLY.

Refresh strategy: data only mutates via migration files at server
startup, so no AFTER triggers and no pg_cron — main.go calls
services.RefreshSearchView right after db.ApplyMigrations. CONCURRENTLY
keeps reads online and stays well under 100 ms at < 1k rows.

Service `internal/services/deadline_search_service.go`:
  - Two-query pipeline per request: (1) rank concept_ids by
    GREATEST(similarity()) across name / aliases / legal_source / rule_code
    plus a 0.2 alias-hit boost; (2) load all matview rows for the top-N
    concepts and assemble per-pill JSON.
  - normalizeQuery strips legal-prefix noise (`§`, `Art.`, `Section`,
    `Rule `) so users typing `§ 82` find DE.PatG.82.1 even though the
    structured legal_source column doesn't carry the prefix.
  - FormatLegalSourceDisplay renders structured codes back to the
    pleading form HLC users expect:
        UPC.RoP.23.1   → "UPC RoP R.23(1)"
        DE.PatG.82.1   → "PatG §82(1)"
        EU.EPÜ.108     → "EPÜ Art.108"
        EU.EPC-R.79.1  → "EPC R.79(1)"
        EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
  - Drill URLs route per kind: rule pills → ?proc=…&focus=…, trigger
    pills → ?mode=event&triggerId=…

Handler `GET /api/tools/fristenrechner/search?q=&party=&proc=&source=&limit=`:
  - Returns the JSON shape from design §6.1 (cards-with-pills).
  - 503 with friendly DE message when DATABASE_URL is unset, mirroring
    the other Fristenrechner endpoints.
  - Empty q returns an empty cards array (browse surface is Phase D).

Tests:
  - Pure-Go: TestFormatLegalSourceDisplay (12 cases across all known
    prefixes) + TestNormalizeQuery (8 cases).
  - Integration (skipped without TEST_DATABASE_URL): golden table
    pinning the design's binding queries — Klageerwiderung returns the
    statement-of-defence card with UPC.RoP.23.1, DE.ZPO.276.1,
    DE.PatG.82.1, EU.EPC-R.79.1, DE.PatG.59.3 pills; "RoP 23" returns
    the same card; "§ 82" → normalized "82" → BPatG hit; Wiedereinsetzung
    returns one card with exactly 4 trigger pills (ids 200..203);
    party / source filters narrow as expected; limit cap honoured.
  - SQL semantics validated against live data via supabase MCP using a
    CTE-inlined matview definition with the slug fix simulated; results
    match the golden table.

Per design doc `docs/plans/unified-fristenrechner.md` §4.6 (matview
shape) + §6 (search ranking + API).
2026-05-05 04:32:50 +02:00

13 lines
458 B
SQL

-- t-paliad-131 Phase C rollback.
DROP MATERIALIZED VIEW IF EXISTS paliad.deadline_search;
-- Reverse the dangling-slug fix only if the wiedereinsetzung concept
-- exists (otherwise this migration was already partially rolled back).
UPDATE paliad.trigger_events
SET concept_id = 're-establishment-of-rights'
WHERE id IN (200, 201, 202, 203)
AND concept_id = 'wiedereinsetzung';
DELETE FROM paliad.deadline_concepts WHERE slug = 'wiedereinsetzung';