Embedded graph store backed by LadybugDB. Schema:: Node(id PK, type, name, properties, search_text) RELATES(FROM Node TO Node, id STRING, type STRING, properties STRING) FTS index ``node_fts`` on ``search_text`` with Porter stemmer
| 146 | |
| 147 | |
| 148 | class GraphStore: |
| 149 | """Embedded graph store backed by LadybugDB. |
| 150 | |
| 151 | Schema:: |
| 152 | |
| 153 | Node(id PK, type, name, properties, search_text) |
| 154 | RELATES(FROM Node TO Node, id STRING, type STRING, properties STRING) |
| 155 | FTS index ``node_fts`` on ``search_text`` with Porter stemmer |
| 156 | """ |
| 157 | |
| 158 | def __init__(self, db_path: str, *, read_only: bool = False) -> None: |
| 159 | self._db = ladybug.Database(db_path, read_only=read_only) |
| 160 | self._conn = ladybug.Connection(self._db) |
| 161 | self._load_extensions() |
| 162 | if not read_only: |
| 163 | self._ensure_schema() |
| 164 | |
| 165 | # -- schema ---------------------------------------------------------- |
| 166 | |
| 167 | def _load_extensions(self) -> None: |
| 168 | """Install and load the FTS extension (required for full-text search).""" |
| 169 | try: |
| 170 | self._conn.execute("INSTALL FTS") |
| 171 | self._conn.execute("LOAD EXTENSION FTS") |
| 172 | except RuntimeError: |
| 173 | pass # already installed/loaded |
| 174 | |
| 175 | def _ensure_schema(self) -> None: |
| 176 | stmts = [ |
| 177 | "CREATE NODE TABLE IF NOT EXISTS Node(id STRING PRIMARY KEY, type STRING, name STRING, properties STRING)", |
| 178 | "CREATE REL TABLE IF NOT EXISTS RELATES(FROM Node TO Node, id STRING, type STRING, properties STRING)", |
| 179 | ] |
| 180 | for stmt in stmts: |
| 181 | self._conn.execute(stmt) |
| 182 | |
| 183 | # Add search_text column (ALTER not idempotent — ignore if exists). |
| 184 | try: |
| 185 | self._conn.execute("ALTER TABLE Node ADD search_text STRING DEFAULT ''") |
| 186 | except RuntimeError: |
| 187 | pass # column already exists |
| 188 | |
| 189 | # FTS index (also not idempotent). |
| 190 | try: |
| 191 | self._conn.execute("CALL CREATE_FTS_INDEX('Node', 'node_fts', ['search_text'], stemmer := 'porter')") |
| 192 | except RuntimeError: |
| 193 | pass # index already exists |
| 194 | |
| 195 | # -- write ----------------------------------------------------------- |
| 196 | |
| 197 | def add_node( |
| 198 | self, |
| 199 | id: str, |
| 200 | node_type: str, |
| 201 | name: str, |
| 202 | properties: dict[str, Any] | None = None, |
| 203 | ) -> None: |
| 204 | """Insert or update a single node (MERGE).""" |
| 205 | props_json = _marshal_props(properties) |
no outgoing calls