Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/vidxp/cli_commands/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,12 +478,14 @@ def index_clear(
typer.Option("--json", help="Emit machine-readable JSON."),
] = False,
) -> None:
"""Publish an empty active snapshot without deleting retained generations."""
"""Remove generated index data and publish an empty active snapshot."""

state = state_from_context(ctx)
if not yes:
typer.confirm(
f"Clear the active index at {state.service.index_directory}?",
f"Clear the active index at {state.service.index_directory}? "
"This removes generated index data and vector collections; "
"imported source media is preserved.",
abort=True,
)
cleared = state.service.clear_index()
Expand All @@ -493,4 +495,9 @@ def index_clear(
if effective_output_format(state, json_output) == OutputFormat.json:
emit_json(payload)
else:
typer.echo("Index cleared." if cleared else "No index was found.")
typer.echo(
"Index cleared. Generated index data and vector collections "
"were removed; imported source media is preserved."
if cleared
else "No index was found."
)
36 changes: 35 additions & 1 deletion src/vidxp/infrastructure/local_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,4 +582,38 @@ def clear(self, config: IndexConfig) -> bool:
self._require_index_directory(config.index_directory)
repository = self.repository
with repository.lease():
return repository.clear()
cleared = repository.clear()
self._discard_vector_collections(
repository,
config,
client_factory=self.chroma_clients,
)
return cleared

def _discard_vector_collections(
self,
repository: LocalSnapshotRepository,
config: IndexConfig,
*,
client_factory: ChromaClientFactory | None = None,
) -> None:
clients = client_factory or ChromaClientFactory()
cleanup_config = replace(
config,
storage_directory=repository.store,
generation_directory=None,
video_id=None,
generation_id=None,
snapshot_id=None,
snapshot_sha256=None,
)
if clients.remote or repository.store.is_dir():
try:
with IndexStorage(
cleanup_config,
create=False,
client_factory=clients,
) as storage:
storage.clear()
except FileNotFoundError:
pass
23 changes: 18 additions & 5 deletions src/vidxp/infrastructure/local_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,23 @@ def generation_directory(self, generation_id: str) -> Path:
def _snapshot_path(self, snapshot_id: str) -> Path:
return self.snapshots / f"{snapshot_id}.json"

def read_active(self, *, required: bool = False) -> IndexSnapshot | None:
resolved = self._read_active(required=required)
def read_active(
self,
*,
required: bool = False,
validate_generations: bool = True,
) -> IndexSnapshot | None:
resolved = self._read_active(
required=required,
validate_generations=validate_generations,
)
return None if resolved is None else resolved[1]

def _read_active(
self,
*,
required: bool = False,
validate_generations: bool = True,
) -> tuple[ActiveSnapshotPointer, IndexSnapshot] | None:
if not self.active_pointer.is_file():
if required:
Expand All @@ -138,6 +147,7 @@ def _read_active(
snapshot = self.read_snapshot(
pointer.snapshot_id,
expected_sha256=pointer.snapshot_sha256,
validate_generations=validate_generations,
)
return pointer, snapshot
except IndexSchemaError:
Expand All @@ -152,6 +162,7 @@ def read_snapshot(
snapshot_id: str,
*,
expected_sha256: str | None = None,
validate_generations: bool = True,
) -> IndexSnapshot:
snapshot_path = self._snapshot_path(snapshot_id)
if not snapshot_path.is_file():
Expand All @@ -177,7 +188,8 @@ def read_snapshot(
raise IndexSchemaError(
"The snapshot filename and document identifier differ."
)
self._validate_generations(snapshot)
if validate_generations:
self._validate_generations(snapshot)
return snapshot

def _validate_generations(self, snapshot: IndexSnapshot) -> None:
Expand Down Expand Up @@ -338,9 +350,10 @@ def remove(self, media_id: str) -> bool:
return True

def clear(self) -> bool:
active = self.read_active()
if active is None or not active.generations:
resolved = self._read_active(validate_generations=False)
if resolved is None or not resolved[1].generations:
return False
active = resolved[1]
self._publish(
generations={},
config_fingerprint=active.config_fingerprint,
Expand Down
19 changes: 16 additions & 3 deletions src/vidxp/infrastructure/sql_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ def _ensure_index_state(connection: Connection) -> None:
except IntegrityError:
pass

def read_active(self, *, required: bool = False) -> IndexSnapshot | None:
def read_active(
self,
*,
required: bool = False,
validate_generations: bool = True,
) -> IndexSnapshot | None:
with self.engine.connect() as connection:
row = connection.execute(
select(
Expand All @@ -122,19 +127,22 @@ def read_active(self, *, required: bool = False) -> IndexSnapshot | None:
connection,
row.active_snapshot_id,
expected_sha256=row.active_snapshot_sha256,
validate_generations=validate_generations,
)

def read_snapshot(
self,
snapshot_id: str,
*,
expected_sha256: str | None = None,
validate_generations: bool = True,
) -> IndexSnapshot:
with self.engine.connect() as connection:
return self._read_snapshot(
connection,
snapshot_id,
expected_sha256=expected_sha256,
validate_generations=validate_generations,
)

def _read_snapshot(
Expand All @@ -143,6 +151,7 @@ def _read_snapshot(
snapshot_id: str,
*,
expected_sha256: str | None,
validate_generations: bool = True,
) -> IndexSnapshot:
row = connection.execute(
select(
Expand All @@ -166,7 +175,8 @@ def _read_snapshot(
raise IndexSchemaError(
f"Index snapshot {snapshot_id} failed integrity validation."
)
self._validate_generations(snapshot)
if validate_generations:
self._validate_generations(snapshot)
return snapshot

def validate_generation(
Expand Down Expand Up @@ -247,14 +257,15 @@ def remove(self, media_id: str) -> bool:
return True

def clear(self) -> bool:
active = self.read_active()
active = self.read_active(validate_generations=False)
if active is None or not active.generations:
return False
self._publish(
replacement=None,
remove_media_id="*",
config_fingerprint=active.config_fingerprint,
configuration=dict(active.configuration),
validate_generations=False,
)
return True

Expand All @@ -265,6 +276,7 @@ def _publish(
remove_media_id: str | None,
config_fingerprint: str,
configuration: dict[str, Any],
validate_generations: bool = True,
) -> IndexSnapshot:
with self.engine.begin() as connection:
self._ensure_index_state(connection)
Expand All @@ -283,6 +295,7 @@ def _publish(
connection,
state.active_snapshot_id,
expected_sha256=state.active_snapshot_sha256,
validate_generations=validate_generations,
)
)
generations = dict(active.generations) if active is not None else {}
Expand Down
Loading
Loading