Simplify: dedupe file-swap rebuild, migration-check locking, and test mocks

Prompted by an automated simplification review of the branch. Applies the
highest-value findings, all behavior-preserving (full suite still green):

- vector_store.py: extract _rebuild_file() (temp-file lifecycle: open,
  populate, swap-in-or-discard-on-failure) and _rebuild_into() (create
  table, copy meta, stream rows) out of compact() and the v1->v2
  migration, which had duplicated both almost verbatim. The migration
  module shrinks to a single _rebuild_into() call instead of reaching
  into four PaperlessSqliteVecVectorStore privates.
- vector_store.py: extract _stored_schema_version() out of
  has_pending_migration() and check_and_run_migrations(), which
  duplicated the same schema_version read and its "missing key means
  current" default.
- indexing.py: extract _with_exclusive_access(), collapsing three
  identical _exclude_readers()/Timeout/log-and-skip blocks (compaction
  in update_llm_index() and llm_index_compact(), the migration check in
  _check_and_run_migrations()) to one line each. Also trims
  _check_and_run_migrations()'s docstring, which had grown to restate
  content already documented on has_pending_migration() and
  check_and_run_migrations(), including a claim that was now stale
  (llm_index_migrate() is a second consumer of the re-embed signal).
- test_ai_indexing.py: add a mock_store fixture for the
  write_store()-yields-a-MagicMock pattern that 8 tests were hand-rolling
  identically.
- migrations/__init__.py: consolidate the "how to add a migration"
  instructions to one place instead of three.
- docs/administration.md: fix two now-inaccurate claims -- the bare-metal
  migrate step doesn't run "automatically" (it's the manual step being
  documented), and the self-contradictory "if enabled... no-op if
  disabled" phrasing on the same step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
stumpylog
2026-07-28 12:32:59 -07:00
co-authored by Claude Sonnet 5
parent 470bcc1751
commit 15938945d7
6 changed files with 174 additions and 206 deletions
+9 -10
View File
@@ -212,16 +212,15 @@ following:
This is a no-op if the index is already up to date, so it is safe to
run on every upgrade.
5. If the LLM index is enabled, apply any pending LLM index schema
migrations.
5. Apply any pending LLM index schema migrations.
```shell-session
cd src
python3 manage.py document_llmindex migrate
```
This is a no-op if the index is already up to date (or the LLM index
is disabled), so it is safe to run on every upgrade.
This is a no-op if the index is already up to date, or if the LLM index
is disabled, so it is safe to run on every upgrade.
### Database Upgrades
@@ -557,12 +556,12 @@ Specify `compact` to reclaim space and optimize the on-disk vector store.
Specify `migrate` to apply any pending index schema migrations without a full reindex.
This is a no-op if the index is already up to date, so it is safe to run on every
startup or upgrade; it is what the container's startup sequence and the
[bare-metal upgrade steps](#bare-metal-updating) run automatically. If a pending
migration would require re-embedding every document, `migrate` only logs a warning and
leaves the index as-is -- re-embedding can be slow and, for a metered embedding backend,
cost money, so it is never triggered automatically. Run `rebuild` yourself when you are
ready.
startup or upgrade; the container's startup sequence runs it automatically, and the
[bare-metal upgrade steps](#bare-metal-updating) include it as a manual step. If a
pending migration would require re-embedding every document, `migrate` only logs a
warning and leaves the index as-is -- re-embedding can be slow and, for a metered
embedding backend, cost money, so it is never triggered automatically. Run `rebuild`
yourself when you are ready.
!!! note
+29 -41
View File
@@ -144,6 +144,24 @@ def _exclude_readers():
lock.close()
def _with_exclusive_access(operation: str, fn):
"""Run ``fn()`` with exclusive index access (see ``_exclude_readers()``),
for compaction/migration file swaps that must not run while readers are
active. Returns ``fn()``'s result, or None (after logging) if active
readers do not drain within ``LLM_INDEX_COMPACTION_LOCK_TIMEOUT`` --
callers skip the operation this run; it retries next time.
"""
try:
with _exclude_readers():
return fn()
except Timeout:
logger.info(
"Skipping LLM index %s: index readers are active; will retry next run.",
operation,
)
return None
@contextmanager
def write_store(embed_model_name: str | None = None):
"""Acquire the write lock and yield the vector store.
@@ -325,34 +343,18 @@ def _exclude_document_id_filter(document_id: int | str):
def _check_and_run_migrations(store: "PaperlessSqliteVecVectorStore") -> bool:
"""Run any pending structural migrations before a write. Returns True
when a pending re-embed migration was encountered, signaling the
caller must force a full rebuild.
check_and_run_migrations() never re-embeds anything itself: it only
applies cheap, local structural migrations, and for a re-embed
migration returns True as a pure signal with no side effects. Actual
re-embedding only happens in update_llm_index()'s explicit rebuild
path below, so it is safe to call this before any write -- including
delete()/upsert_document() -- without risking an unwanted (and
possibly costly, if the embedding backend is metered) re-embed.
has_pending_migration() gates the exclusive-access check: in the
common case (already at SCHEMA_VERSION) this returns immediately
without ever contending with readers or a concurrent compaction --
normal writes must not be gated by that lock.
"""Run any pending structural migrations, returning True if a pending
re-embed migration needs the caller to force a rebuild -- never
triggered automatically here. Safe to call before any write, including
delete()/upsert_document(): has_pending_migration() (see its docstring)
keeps this a no-op, with no exclusive access taken, once the store is
current.
"""
if not store.has_pending_migration():
return False
try:
with _exclude_readers():
return store.check_and_run_migrations()
except Timeout:
logger.info(
"Skipping LLM index migration check: index readers are active; "
"will retry next run.",
)
return False
return bool(
_with_exclusive_access("migration check", store.check_and_run_migrations),
)
def update_llm_index(
@@ -447,14 +449,7 @@ def update_llm_index(
else "No changes detected in LLM index."
)
try:
with _exclude_readers():
store.compact()
except Timeout:
logger.info(
"Skipping LLM index compaction: index readers are active; "
"will retry next run.",
)
_with_exclusive_access("compaction", store.compact)
return msg
@@ -501,14 +496,7 @@ def llm_index_migrate() -> None:
def llm_index_compact() -> None:
"""Compact the index immediately, rebuilding the table to reclaim space."""
with write_store() as store:
try:
with _exclude_readers():
store.compact(force=True)
except Timeout:
logger.info(
"Skipping LLM index compaction: index readers are active; "
"will retry next run.",
)
_with_exclusive_access("compaction", lambda: store.compact(force=True))
def llm_index_remove_document(document: Document):
+17 -16
View File
@@ -2,20 +2,20 @@
Each migration lives in its own module here, named ``mNNNN_description.py``
(e.g. ``m0001_add_document_chunks.py`` -- a leading digit isn't a valid
Python identifier, hence the ``m`` prefix, unlike Django's own
``NNNN_description.py`` migrations, which get away with it because Django's
migration loader uses ``importlib.import_module()`` with a dynamic string
path instead of a static import statement), and registers itself into
Python identifier, hence the ``m`` prefix, unlike Django's own numbered
migrations, which load via a dynamic ``importlib.import_module()`` call
rather than a static import statement), and registers itself into
``MIGRATIONS`` at import time. ``vector_store.py`` imports those modules at
the bottom of the file, purely for that registration side effect, after
``PaperlessSqliteVecVectorStore`` and ``_copy_rows`` are fully defined --
migrations need both to implement ``apply()``.
``PaperlessSqliteVecVectorStore`` is fully defined -- migrations need it to
implement ``apply()`` (see ``Migration`` below).
Add a new migration by adding a new ``mNNNN_description.py`` module here that
imports ``PaperlessSqliteVecVectorStore``/``_copy_rows`` from
``paperless_ai.vector_store``, defines its ``apply()``, and appends a
``Migration`` to ``MIGRATIONS``; then import that module at the bottom of
``vector_store.py`` and bump ``SCHEMA_VERSION``.
To add a new migration: add a new ``mNNNN_description.py`` module here that
imports ``PaperlessSqliteVecVectorStore`` from ``paperless_ai.vector_store``,
defines its ``apply()`` (most likely just a call to
``PaperlessSqliteVecVectorStore._rebuild_into()``, see ``Migration`` below),
and appends a ``Migration`` to ``MIGRATIONS``; then import that module at the
bottom of ``vector_store.py`` and bump ``SCHEMA_VERSION`` there.
"""
import sqlite3
@@ -30,11 +30,12 @@ class Migration:
"""A schema migration for the sqlite-vec vector store.
kind="structural": rows are copied into a new-schema file with no
re-embedding needed. Supply ``apply(src_conn, dst_conn, dim)`` which
must create the vec0 table in ``dst_conn``, copy all rows from
``src_conn``, and write ``dim`` / ``embed_model`` / ``total_inserts`` to
``dst_conn``'s ``index_meta``. ``schema_version`` is written by the
migration runner after ``apply`` returns.
re-embedding needed. Supply ``apply(src_conn, dst_conn, dim)``, which
must create the vec0 table in ``dst_conn`` and copy ``src_conn``'s rows
and index_meta into it -- usually just a call to
``PaperlessSqliteVecVectorStore._rebuild_into(src_conn, dst_conn, dim)``.
``schema_version`` is written by the migration runner after ``apply``
returns, not by ``apply`` itself.
kind="re-embed": the new schema requires fresh embeddings.
``check_and_run_migrations()`` returns True when it encounters one of
@@ -3,7 +3,6 @@ import sqlite3
from paperless_ai.migrations import MIGRATIONS
from paperless_ai.migrations import Migration
from paperless_ai.vector_store import PaperlessSqliteVecVectorStore
from paperless_ai.vector_store import _copy_rows
def _migrate_v1_to_v2_add_document_chunks(
@@ -18,22 +17,17 @@ def _migrate_v1_to_v2_add_document_chunks(
full table scan on the document_id metadata column. Every row written
before this migration predates that table, so without backfilling,
deleting a pre-migration document would find zero chunk ids and leave its
vec0 rows permanently orphaned. Backfilling is a plain row copy into the
new-schema file (``_copy_rows``, the same helper compact() uses), which
records every copied row in document_chunks as it goes.
vec0 rows permanently orphaned. Backfilling is just the same rebuild
compact() uses (_rebuild_into), minus "schema_version" -- the caller
(_run_structural_migration) sets that to this migration's target version
instead of preserving the source's.
"""
PaperlessSqliteVecVectorStore._create_vec_table(dst_conn, dim)
PaperlessSqliteVecVectorStore._meta_set_on(dst_conn, "dim", str(dim))
embed_model = PaperlessSqliteVecVectorStore._meta_get_on(src_conn, "embed_model")
if embed_model is not None:
PaperlessSqliteVecVectorStore._meta_set_on(dst_conn, "embed_model", embed_model)
dst_conn.execute("BEGIN IMMEDIATE")
live = _copy_rows(src_conn, dst_conn)
# This migration only ever copies live rows (like compact()), so the
# cumulative counter resets to match -- the new file has no bloat yet.
PaperlessSqliteVecVectorStore._meta_set_on(dst_conn, "total_inserts", str(live))
dst_conn.execute("COMMIT")
PaperlessSqliteVecVectorStore._rebuild_into(
src_conn,
dst_conn,
dim,
meta_keys=("dim", "embed_model"),
)
MIGRATIONS.append(
+25 -69
View File
@@ -37,6 +37,23 @@ def real_document(db: None) -> Document:
)
@pytest.fixture
def mock_store(mocker: pytest_mock.MockerFixture) -> MagicMock:
"""The MagicMock store yielded by every ``with write_store() as store:``
block, for tests that only care what indexing.py does with the store,
not what the store itself does.
"""
store = mocker.MagicMock()
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=store),
__exit__=mocker.MagicMock(return_value=False),
),
)
return store
@pytest.mark.django_db
def test_build_document_node(real_document: Document) -> None:
nodes = indexing.build_document_node(real_document)
@@ -762,18 +779,9 @@ class TestLlmIndexAddOrUpdateDocumentEmptyContent:
@pytest.mark.django_db
def test_llm_index_compact_uses_force(
temp_llm_index_dir: Path,
mocker: pytest_mock.MockerFixture,
mock_store: MagicMock,
) -> None:
"""compact must use force=True to rebuild the table and reclaim space immediately."""
mock_store = mocker.MagicMock()
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
indexing.llm_index_compact()
mock_store.compact.assert_called_once_with(force=True)
@@ -812,6 +820,7 @@ class TestLlmIndexMigrate:
self,
temp_llm_index_dir: Path,
mocker: pytest_mock.MockerFixture,
mock_store: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""
@@ -826,16 +835,8 @@ class TestLlmIndexMigrate:
mock_config = mocker.MagicMock()
mock_config.llm_index_enabled = True
mocker.patch("paperless_ai.indexing.AIConfig", return_value=mock_config)
mock_store = mocker.MagicMock()
mock_store.has_pending_migration.return_value = True
mock_store.check_and_run_migrations.return_value = False
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
with caplog.at_level("WARNING"):
indexing.llm_index_migrate()
@@ -847,6 +848,7 @@ class TestLlmIndexMigrate:
self,
temp_llm_index_dir: Path,
mocker: pytest_mock.MockerFixture,
mock_store: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""
@@ -864,16 +866,8 @@ class TestLlmIndexMigrate:
mock_config = mocker.MagicMock()
mock_config.llm_index_enabled = True
mocker.patch("paperless_ai.indexing.AIConfig", return_value=mock_config)
mock_store = mocker.MagicMock()
mock_store.has_pending_migration.return_value = True
mock_store.check_and_run_migrations.return_value = True
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
with caplog.at_level("WARNING"):
indexing.llm_index_migrate()
@@ -895,16 +889,9 @@ class TestLlmIndexLocking:
self,
temp_llm_index_dir: Path,
mock_embed_model: FakeEmbedding,
mock_store: MagicMock,
mocker: pytest_mock.MockerFixture,
) -> None:
mock_store = MagicMock()
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
mock_node = MagicMock()
mock_node.get_content.return_value = "fake node text"
mocker.patch(
@@ -923,6 +910,7 @@ class TestLlmIndexLocking:
self,
temp_llm_index_dir: Path,
mock_embed_model: FakeEmbedding,
mock_store: MagicMock,
mocker: pytest_mock.MockerFixture,
*,
has_pending: bool,
@@ -939,15 +927,7 @@ class TestLlmIndexLocking:
that check_and_run_migrations() requires
- upsert_document() is called either way
"""
mock_store = MagicMock()
mock_store.has_pending_migration.return_value = has_pending
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
mock_node = MagicMock()
mock_node.get_content.return_value = "fake node text"
mocker.patch(
@@ -964,17 +944,8 @@ class TestLlmIndexLocking:
def test_remove_document_uses_write_store(
self,
temp_llm_index_dir: Path,
mocker: pytest_mock.MockerFixture,
mock_store: MagicMock,
) -> None:
mock_store = MagicMock()
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
doc = MagicMock(spec=Document)
doc.id = 1
indexing.llm_index_remove_document(doc)
@@ -985,7 +956,7 @@ class TestLlmIndexLocking:
def test_remove_document_runs_migration_check_only_when_pending(
self,
temp_llm_index_dir: Path,
mocker: pytest_mock.MockerFixture,
mock_store: MagicMock,
*,
has_pending: bool,
) -> None:
@@ -1002,15 +973,7 @@ class TestLlmIndexLocking:
test_normal_write_is_not_gated_by_the_compaction_lock)
- delete() is called either way
"""
mock_store = MagicMock()
mock_store.has_pending_migration.return_value = has_pending
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
doc = DocumentFactory.build(id=1)
indexing.llm_index_remove_document(doc)
@@ -1022,16 +985,9 @@ class TestLlmIndexLocking:
self,
temp_llm_index_dir: Path,
mock_embed_model: FakeEmbedding,
mock_store: MagicMock,
mocker: pytest_mock.MockerFixture,
) -> None:
mock_store = MagicMock()
mocker.patch(
"paperless_ai.indexing.write_store",
return_value=mocker.MagicMock(
__enter__=mocker.MagicMock(return_value=mock_store),
__exit__=mocker.MagicMock(return_value=False),
),
)
mock_qs = MagicMock()
mock_qs.exists.return_value = True
mock_qs.__iter__ = MagicMock(return_value=iter([]))
+84 -54
View File
@@ -40,9 +40,8 @@ _INSERT_CHUNK_INDEX = (
"INSERT INTO document_chunks (chunk_id, document_id) VALUES (?, ?)"
)
# Current schema version. Written to index_meta at table creation and bumped
# whenever a Migration is added to MIGRATIONS. check_and_run_migrations() uses
# this to decide which migrations to run on an existing store.
# Current schema version. Bump when adding a migration -- see
# paperless_ai/migrations/__init__.py for the full procedure.
SCHEMA_VERSION = 2
# compact(): rebuild when the cumulative rowid count exceeds this multiple of
@@ -522,6 +521,67 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
result[doc_id] = str(row["modified"] or "")
return result
@property
def _db_path(self) -> str:
return str(Path(self._uri) / DB_FILENAME)
@contextmanager
def _rebuild_file(self) -> Iterator[sqlite3.Connection]:
"""Open a fresh temp database file for a file-swap rebuild (compact
or structural migration), yielding its connection for the caller to
populate.
On success, swaps the temp file in as the live database (closing
this store's current connection first -- see _swap_in_compact()).
On any exception, discards the temp file, including its -wal/-shm,
instead, and this store's own connection is left untouched.
"""
compact_path = self._db_path + ".compact"
new_conn = self._open_connection(compact_path)
try:
yield new_conn
except BaseException:
new_conn.close()
for suffix in ["", "-wal", "-shm"]:
Path(compact_path + suffix).unlink(missing_ok=True)
raise
else:
new_conn.close()
self._swap_in_compact(compact_path, self._db_path)
@staticmethod
def _rebuild_into(
src_conn: sqlite3.Connection,
dst_conn: sqlite3.Connection,
dim: int,
meta_keys: tuple[str, ...] = ("dim", "embed_model", "schema_version"),
) -> int:
"""Create the vec0 table in ``dst_conn``, copy ``meta_keys`` from
``src_conn``'s index_meta, and stream every live row across
(populating document_chunks as it goes -- see _copy_rows()).
Returns the number of rows copied.
Used by both compact() (default meta_keys: the schema is unchanged)
and structural migrations (meta_keys minus "schema_version", which
the migration sets to its own target version instead of preserving
the source's).
"""
PaperlessSqliteVecVectorStore._create_vec_table(dst_conn, dim)
for key in meta_keys:
value = PaperlessSqliteVecVectorStore._meta_get_on(src_conn, key)
if value is not None:
PaperlessSqliteVecVectorStore._meta_set_on(dst_conn, key, value)
dst_conn.execute("BEGIN IMMEDIATE")
copied = _copy_rows(src_conn, dst_conn)
# Reset the cumulative counter: after a rebuild, total_inserts == live.
PaperlessSqliteVecVectorStore._meta_set_on(
dst_conn,
"total_inserts",
str(copied),
)
dst_conn.execute("COMMIT")
return copied
def compact(self, *, force: bool = False) -> None:
"""Rebuild the database file to reclaim space left behind by DELETEs.
@@ -554,30 +614,8 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
live,
total,
)
db_path = str(Path(self._uri) / DB_FILENAME)
compact_path = db_path + ".compact"
# Copy all live rows into a fresh database file.
new_conn = self._open_connection(compact_path)
try:
self._create_vec_table(new_conn, dim)
self._meta_set_on(new_conn, "dim", str(dim))
for key in ("embed_model", "schema_version"):
value = self._meta_get(key)
if value is not None:
self._meta_set_on(new_conn, key, value)
new_conn.execute("BEGIN IMMEDIATE")
_copy_rows(self._conn, new_conn)
# Reset the cumulative counter: after compact, total_inserts == live.
self._meta_set_on(new_conn, "total_inserts", str(live))
new_conn.execute("COMMIT")
except BaseException:
new_conn.close()
for p in [compact_path, compact_path + "-wal", compact_path + "-shm"]:
Path(p).unlink(missing_ok=True)
raise
new_conn.close()
self._swap_in_compact(compact_path, db_path)
with self._rebuild_file() as new_conn:
self._rebuild_into(self._conn, new_conn, dim)
def _swap_in_compact(self, compact_path: str, db_path: str) -> None:
"""Atomically replace the live database with the compacted copy."""
@@ -589,6 +627,17 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
Path(compact_path).replace(db_path)
self._conn = self._open_connection(db_path)
def _stored_schema_version(self) -> int | None:
"""The schema_version recorded in index_meta, or None if no table
exists. A missing key (a store predating version tracking) is
treated as SCHEMA_VERSION -- i.e. already current -- since no
migration in MIGRATIONS targets a version before tracking began.
"""
if not self.table_exists():
return None
raw = self._meta_get("schema_version")
return int(raw) if raw is not None else SCHEMA_VERSION
def has_pending_migration(self) -> bool:
"""Cheaply check whether a migration is pending, with no exclusive
access needed -- just a metadata read under the connection callers
@@ -600,11 +649,8 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
case -- already at SCHEMA_VERSION -- never contends with readers
or a concurrent compaction.
"""
if not self.table_exists():
return False
raw = self._meta_get("schema_version")
current = int(raw) if raw is not None else SCHEMA_VERSION
return current < SCHEMA_VERSION
current = self._stored_schema_version()
return current is not None and current < SCHEMA_VERSION
def check_and_run_migrations(self) -> bool:
"""Apply any pending schema migrations to the store.
@@ -619,12 +665,8 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
that exclusion in the common case). No-op when the table does not
exist or is already at SCHEMA_VERSION.
"""
if not self.table_exists():
return False
raw = self._meta_get("schema_version")
current = int(raw) if raw is not None else SCHEMA_VERSION
if current >= SCHEMA_VERSION:
current = self._stored_schema_version()
if current is None or current >= SCHEMA_VERSION:
return False
pending = sorted(
@@ -658,24 +700,12 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
dim = self.vector_dim()
if dim is None: # pragma: no cover
raise RuntimeError("Cannot migrate: no stored vector dimension")
db_path = str(Path(self._uri) / DB_FILENAME)
compact_path = db_path + ".compact"
new_conn = self._open_connection(compact_path)
try:
with self._rebuild_file() as new_conn:
migration.apply(self._conn, new_conn, dim)
self._meta_set_on(new_conn, "schema_version", str(migration.to_version))
except BaseException: # pragma: no cover
new_conn.close()
for p in [compact_path, compact_path + "-wal", compact_path + "-shm"]:
Path(p).unlink(missing_ok=True)
raise
new_conn.close()
self._swap_in_compact(compact_path, db_path)
# Import migration modules for their registration side effect (each appends
# itself to MIGRATIONS on import). Must be at the bottom of this file: those
# modules import PaperlessSqliteVecVectorStore and _copy_rows from here, which
# must already be fully defined by the time they run. Add a new migration
# module's import here (and bump SCHEMA_VERSION) when the schema changes.
# Registers m0001 into MIGRATIONS; must be at the bottom (needs
# PaperlessSqliteVecVectorStore fully defined) -- see
# paperless_ai/migrations/__init__.py for the full procedure.
from paperless_ai.migrations import m0001_add_document_chunks # noqa: E402, F401