Merge pull request #1838 from MemPalace/fix/sqlite-ro-uri-encoding
fix: percent-encode sqlite read-only URIs for spaced/special-char paths
This commit is contained in:
commit
fe460c4b8b
|
|
@ -17,6 +17,7 @@ from typing import Any, Optional
|
|||
import chromadb
|
||||
from chromadb.errors import NotFoundError as _ChromaNotFoundError
|
||||
|
||||
from ..config import sqlite_read_uri
|
||||
from ._sidecar import EMBEDDER_SIDECAR_FILENAME, read_embedder_sidecar, write_embedder_sidecar
|
||||
from .base import (
|
||||
BaseBackend,
|
||||
|
|
@ -457,7 +458,7 @@ def _vector_segment_id(palace_path: str, collection_name: str) -> Optional[str]:
|
|||
if not os.path.isfile(db_path):
|
||||
return None
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"""
|
||||
|
|
@ -626,7 +627,7 @@ def _read_sync_threshold(palace_path: str, collection_name: str) -> int:
|
|||
if not os.path.isfile(db_path):
|
||||
return 1000
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
|
|
@ -746,7 +747,7 @@ def _sqlite_embedding_count(palace_path: str, collection_name: str) -> Optional[
|
|||
if not os.path.isfile(db_path):
|
||||
return None
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"""
|
||||
|
|
@ -807,7 +808,7 @@ def _sqlite_wing_room_counts(
|
|||
if not os.path.isfile(db_path):
|
||||
return None
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
try:
|
||||
# Wait out a transient writer/checkpoint lock rather than falling
|
||||
# straight back to the expensive vector-index path (#1681).
|
||||
|
|
@ -1570,7 +1571,7 @@ class ChromaCollection(BaseCollection):
|
|||
# rowid, embedding_id is the user-facing drawer id.
|
||||
public_ids: dict[int, str] = {}
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
conn.row_factory = sqlite3.Row
|
||||
except sqlite3.Error:
|
||||
logger.debug("Chroma lexical sqlite open failed", exc_info=True)
|
||||
|
|
|
|||
|
|
@ -205,6 +205,21 @@ DEFAULT_BACKEND = "chroma"
|
|||
DEFAULT_MAX_BACKUPS = 10
|
||||
|
||||
|
||||
def sqlite_read_uri(db_path: str) -> str:
|
||||
"""Return a read-only ``file:`` URI for ``sqlite3.connect(..., uri=True)``.
|
||||
|
||||
A bare ``f"file:{db_path}?mode=ro"`` mis-parses paths containing spaces or
|
||||
other URI-reserved characters — common in real home directories (a Windows
|
||||
user folder like ``First Last``, many macOS paths). ``pathname2url``
|
||||
percent-encodes the path and normalizes separators so the database opens on
|
||||
every platform.
|
||||
"""
|
||||
from urllib.request import pathname2url
|
||||
|
||||
db_path = os.fspath(db_path)
|
||||
return f"file:{pathname2url(db_path)}?mode=ro"
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_configured_collection_name() -> str:
|
||||
"""Return the configured drawer collection name without repeated config-file reads."""
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ from .config import ( # noqa: E402
|
|||
sanitize_name,
|
||||
sanitize_content,
|
||||
sanitize_iso_temporal,
|
||||
sqlite_read_uri,
|
||||
strip_lone_surrogates,
|
||||
)
|
||||
from .version import __version__ # noqa: E402
|
||||
|
|
@ -920,7 +921,7 @@ def _tool_status_via_sqlite() -> dict:
|
|||
rooms: dict = {}
|
||||
total = 0
|
||||
try:
|
||||
conn = _sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = _sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from typing import Callable, Iterator, Optional
|
|||
from chromadb.errors import NotFoundError as ChromaNotFoundError
|
||||
|
||||
from .backends.chroma import ChromaBackend, hnsw_capacity_status
|
||||
from .config import sqlite_read_uri
|
||||
|
||||
|
||||
COLLECTION_NAME = "mempalace_drawers"
|
||||
|
|
@ -476,7 +477,7 @@ def sqlite_drawer_count(palace_path: str, collection_name: Optional[str] = None)
|
|||
try:
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect(f"file:{sqlite_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(sqlite_path), uri=True)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"""
|
||||
|
|
@ -516,7 +517,7 @@ def sqlite_integrity_errors(palace_path: str) -> list[str]:
|
|||
return []
|
||||
|
||||
try:
|
||||
with sqlite3.connect(f"file:{sqlite_path}?mode=ro", uri=True) as conn:
|
||||
with sqlite3.connect(sqlite_read_uri(sqlite_path), uri=True) as conn:
|
||||
rows = conn.execute("PRAGMA quick_check").fetchall()
|
||||
except sqlite3.Error as e:
|
||||
return [f"PRAGMA quick_check failed: {e}"]
|
||||
|
|
@ -1013,7 +1014,7 @@ def extract_via_sqlite(palace_path: str, collection_name: str) -> Iterator[tuple
|
|||
if not os.path.isfile(sqlite_path):
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(f"file:{sqlite_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(sqlite_path), uri=True)
|
||||
try:
|
||||
seg_row = conn.execute(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from .backends import (
|
|||
PalaceNotFoundError,
|
||||
UnsupportedCapabilityError,
|
||||
)
|
||||
from .config import sqlite_read_uri
|
||||
from .palace import (
|
||||
_open_collection_or_explain,
|
||||
get_closets_collection,
|
||||
|
|
@ -533,7 +534,7 @@ def _bm25_only_via_sqlite(
|
|||
return "".join(clauses), params
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
conn = sqlite3.connect(sqlite_read_uri(db_path), uri=True)
|
||||
except sqlite3.Error as e:
|
||||
return {"error": f"sqlite open failed: {e}"}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
|
@ -10,6 +11,7 @@ from mempalace.config import (
|
|||
sanitize_iso_temporal,
|
||||
sanitize_kg_value,
|
||||
sanitize_name,
|
||||
sqlite_read_uri,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -148,6 +150,31 @@ def test_embedding_threads_invalid_falls_back_to_auto(tmp_path, monkeypatch):
|
|||
assert cfg.embedding_threads == 2
|
||||
|
||||
|
||||
def test_sqlite_read_uri_opens_path_with_spaces(tmp_path):
|
||||
"""sqlite_read_uri must open a read-only DB whose path contains spaces,
|
||||
which a bare f"file:{path}?mode=ro" mis-parses (especially on Windows)."""
|
||||
db_dir = tmp_path / "palace with spaces"
|
||||
db_dir.mkdir()
|
||||
db_path = db_dir / "chroma.sqlite3"
|
||||
setup = sqlite3.connect(str(db_path))
|
||||
setup.execute("CREATE TABLE t (x INTEGER)")
|
||||
setup.execute("INSERT INTO t VALUES (42)")
|
||||
setup.commit()
|
||||
setup.close()
|
||||
|
||||
uri = sqlite_read_uri(str(db_path))
|
||||
assert "%20" in uri # the space is percent-encoded, not left raw
|
||||
|
||||
conn = sqlite3.connect(uri, uri=True)
|
||||
try:
|
||||
assert conn.execute("SELECT x FROM t").fetchone()[0] == 42
|
||||
# mode=ro is still honored through the encoded URI
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
conn.execute("INSERT INTO t VALUES (1)")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_env_override():
|
||||
raw = "/env/palace"
|
||||
os.environ["MEMPALACE_PALACE_PATH"] = raw
|
||||
|
|
|
|||
Loading…
Reference in New Issue