"""Idempotent Neo4j schema bootstrap. Runs at backend startup. All statements use `IF NOT EXISTS`, so calling this multiple times is safe — it's the contract integration tests rely on. """ from __future__ import annotations import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from infra.neo4j.driver import Neo4jDriver logger = logging.getLogger(__name__) CONSTRAINTS: tuple[str, ...] = ( "CREATE CONSTRAINT document_id IF NOT EXISTS FOR (d:Document) REQUIRE d.id IS UNIQUE", "CREATE CONSTRAINT element_composite IF NOT EXISTS " "FOR (e:Element) REQUIRE (e.doc_id, e.self_ref) IS UNIQUE", "CREATE CONSTRAINT page_composite IF NOT EXISTS " "FOR (p:Page) REQUIRE (p.doc_id, p.page_no) IS UNIQUE", "CREATE CONSTRAINT chunk_id IF NOT EXISTS FOR (c:Chunk) REQUIRE c.id IS UNIQUE", ) INDEXES: tuple[str, ...] = ( "CREATE INDEX element_doc IF NOT EXISTS FOR (e:Element) ON (e.doc_id)", "CREATE INDEX chunk_doc IF NOT EXISTS FOR (c:Chunk) ON (c.doc_id)", # Reasoning tunnel / bbox-highlight: looking up a Provenance by its owner # element is a hot path (one lookup per visited section). Composite index # avoids a full scan of every Provenance in the DB. "CREATE INDEX provenance_element IF NOT EXISTS " "FOR (pv:Provenance) ON (pv.doc_id, pv.element_ref)", "CREATE INDEX provenance_page IF NOT EXISTS FOR (pv:Provenance) ON (pv.doc_id, pv.page_no)", ) FULLTEXT_INDEXES: tuple[str, ...] = ( "CREATE FULLTEXT INDEX element_text IF NOT EXISTS FOR (e:Element) ON EACH [e.text]", ) async def bootstrap_schema(neo: Neo4jDriver) -> None: """Create constraints and indexes required by the graph model. Idempotent: safe to call on every startup. """ async with neo.driver.session(database=neo.database) as session: for stmt in (*CONSTRAINTS, *INDEXES, *FULLTEXT_INDEXES): await session.run(stmt) logger.info( "Neo4j schema bootstrapped (%d constraints, %d indexes, %d fulltext)", len(CONSTRAINTS), len(INDEXES), len(FULLTEXT_INDEXES), )