Coverage for node / src / stigmem_node / storage / __init__.py: 67%
33 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Storage backend registry and factory.
3Usage::
5 from stigmem_node.storage import make_backend
7 # Relies on settings.storage_backend (and related env vars)
8 backend = make_backend(_settings=settings)
10 with backend.connection() as conn:
11 conn.execute("SELECT ...")
13 # Explicit path always returns a plaintext SQLiteBackend (backward-compat
14 # for CLI tools and test fixtures that create their own temp databases).
15 backend = make_backend(db_path="/tmp/test.db")
16"""
18from __future__ import annotations
20import os
21from typing import TYPE_CHECKING, Any
23from .base import StorageBackend
24from .libsql_backend import LibSQLBackend
25from .postgres_backend import PostgresBackend
26from .sqlite_backend import SQLiteBackend
28if TYPE_CHECKING:
29 pass
31__all__ = ["StorageBackend", "SQLiteBackend", "LibSQLBackend", "PostgresBackend", "make_backend"]
34def make_backend(
35 db_path: str | None = None,
36 _settings: Any | None = None,
37) -> StorageBackend:
38 """Return the appropriate ``StorageBackend`` instance.
40 Args:
41 db_path: When provided, always returns a plaintext ``SQLiteBackend``
42 at this path. This preserves backward-compatibility for CLI tools,
43 ``apply_migrations(db_path=…)`` calls, and test fixtures that
44 supply a temporary database path directly. Encryption settings
45 are ignored when *db_path* is given.
46 _settings: A ``Settings`` object to read ``storage_backend``,
47 ``db_path``, ``libsql_url``, ``libsql_auth_token``, and
48 ``at_rest_encryption`` from. Defaults to the live
49 ``stigmem_node.settings.settings`` singleton so that test fixtures
50 can control backend selection by patching ``db_mod.settings``.
52 Raises:
53 RuntimeError: If ``at_rest_encryption="on"`` is set but no key source
54 is configured (``at_rest_key_passphrase_env`` / ``at_rest_key_kms_uri``
55 both empty). The node refuses to start in this state.
56 """
57 # An explicit path always means plaintext SQLite — backward-compat guarantee.
58 if db_path is not None:
59 return SQLiteBackend(db_path)
61 if _settings is None: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 from stigmem_node.settings import settings # lazy to avoid import cycles
64 _settings = settings
66 # Resolve encryption key (Phase 8 / ACM-184). load_key() raises immediately
67 # if encryption is on but no key source is configured — this surfaces the
68 # misconfiguration at app startup (apply_migrations call) rather than lazily.
69 encryption_key: bytes | None = None
70 if getattr(_settings, "at_rest_encryption", "off") == "on":
71 from .encryption import load_key
73 encryption_key = load_key(_settings)
75 backend_name: str = getattr(_settings, "storage_backend", "sqlite")
76 path: str = _settings.db_path
78 if backend_name == "libsql": 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 return LibSQLBackend(
80 db_path=path,
81 sync_url=getattr(_settings, "libsql_url", ""),
82 auth_token=getattr(_settings, "libsql_auth_token", ""),
83 encryption_key=encryption_key,
84 )
86 if backend_name == "postgres": 86 ↛ 88line 86 didn't jump to line 88 because the condition on line 86 was never true
87 # Accept STIGMEM_PG_DSN, STIGMEM_DATABASE_URL, or bare DATABASE_URL.
88 dsn: str = (
89 getattr(_settings, "pg_dsn", "")
90 or getattr(_settings, "database_url", "")
91 or os.environ.get("DATABASE_URL", "")
92 )
93 if not dsn:
94 raise RuntimeError(
95 "storage_backend='postgres' requires a connection string. "
96 "Set STIGMEM_PG_DSN, STIGMEM_DATABASE_URL, or DATABASE_URL."
97 )
98 schema: str = getattr(_settings, "pg_schema", "public") or "public"
99 embed_enabled_pg: bool = getattr(_settings, "embed_enabled", False)
100 embed_dimension_pg: int = int(getattr(_settings, "embed_dimension", 768))
101 return PostgresBackend(
102 dsn=dsn,
103 schema=schema,
104 pool_min=int(getattr(_settings, "postgres_pool_min", 2)),
105 pool_max=int(getattr(_settings, "postgres_pool_max", 10)),
106 embed_enabled=embed_enabled_pg,
107 embed_dimension=embed_dimension_pg,
108 )
110 embed_enabled: bool = getattr(_settings, "embed_enabled", False)
111 embed_dimension: int = int(getattr(_settings, "embed_dimension", 768))
112 return SQLiteBackend(
113 path,
114 encryption_key=encryption_key,
115 embed_enabled=embed_enabled,
116 embed_dimension=embed_dimension,
117 )