-
Notifications
You must be signed in to change notification settings - Fork 2
Updated with the replay features and readme #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||
| import typing as t | ||||||||||||||||||||||||||||||||||||||
| import copy | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from ._cortexadb import ( | ||||||||||||||||||||||||||||||||||||||
| CortexaDBError, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -159,6 +160,8 @@ def __init__( | |||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||
| self._embedder = embedder | ||||||||||||||||||||||||||||||||||||||
| self._recorder = _recorder | ||||||||||||||||||||||||||||||||||||||
| self._last_replay_report: t.Optional[t.Dict[str, t.Any]] = None | ||||||||||||||||||||||||||||||||||||||
| self._last_export_replay_report: t.Optional[t.Dict[str, t.Any]] = None | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| self._inner = _cortexadb.CortexaDB.open( | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -260,6 +263,7 @@ def replay( | |||||||||||||||||||||||||||||||||||||
| db_path: str, | ||||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||||
| sync: str = "strict", | ||||||||||||||||||||||||||||||||||||||
| strict: bool = False, | ||||||||||||||||||||||||||||||||||||||
| ) -> "CortexaDB": | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| Replay a log file into a fresh database, returning the populated instance. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -271,6 +275,8 @@ def replay( | |||||||||||||||||||||||||||||||||||||
| log_path: Path to the NDJSON replay log file. | ||||||||||||||||||||||||||||||||||||||
| db_path: Directory path for the new database. | ||||||||||||||||||||||||||||||||||||||
| sync: Sync policy for the replayed database. | ||||||||||||||||||||||||||||||||||||||
| strict: If True, fail fast on malformed/failed operations. | ||||||||||||||||||||||||||||||||||||||
| If False (default), skip invalid operations and continue. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||
| A :class:`CortexaDB` instance with all recorded operations applied. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -296,51 +302,187 @@ def replay( | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # old_id → new_id mapping (connect ops use original IDs) | ||||||||||||||||||||||||||||||||||||||
| id_map: t.Dict[int, int] = {} | ||||||||||||||||||||||||||||||||||||||
| report: t.Dict[str, t.Any] = { | ||||||||||||||||||||||||||||||||||||||
| "strict": strict, | ||||||||||||||||||||||||||||||||||||||
| "total_ops": 0, | ||||||||||||||||||||||||||||||||||||||
| "applied": 0, | ||||||||||||||||||||||||||||||||||||||
| "skipped": 0, | ||||||||||||||||||||||||||||||||||||||
| "failed": 0, | ||||||||||||||||||||||||||||||||||||||
| "op_counts": { | ||||||||||||||||||||||||||||||||||||||
| "remember": 0, | ||||||||||||||||||||||||||||||||||||||
| "connect": 0, | ||||||||||||||||||||||||||||||||||||||
| "delete": 0, | ||||||||||||||||||||||||||||||||||||||
| "checkpoint": 0, | ||||||||||||||||||||||||||||||||||||||
| "compact": 0, | ||||||||||||||||||||||||||||||||||||||
| "unknown": 0, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| "failures": [], | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def add_failure( | ||||||||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||||||||
| index: int, | ||||||||||||||||||||||||||||||||||||||
| op: str, | ||||||||||||||||||||||||||||||||||||||
| reason: str, | ||||||||||||||||||||||||||||||||||||||
| record: t.Dict[str, t.Any], | ||||||||||||||||||||||||||||||||||||||
| counts_as_failed: bool = False, | ||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||
| if counts_as_failed: | ||||||||||||||||||||||||||||||||||||||
| report["failed"] += 1 | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| report["skipped"] += 1 | ||||||||||||||||||||||||||||||||||||||
| if len(report["failures"]) < 50: | ||||||||||||||||||||||||||||||||||||||
| report["failures"].append( | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| "index": index, | ||||||||||||||||||||||||||||||||||||||
| "op": op, | ||||||||||||||||||||||||||||||||||||||
| "reason": reason, | ||||||||||||||||||||||||||||||||||||||
| "record": record, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for record in reader.operations(): | ||||||||||||||||||||||||||||||||||||||
| def validate_record(op: str, record: t.Dict[str, t.Any]) -> t.Optional[str]: | ||||||||||||||||||||||||||||||||||||||
| if op == "remember": | ||||||||||||||||||||||||||||||||||||||
| if "embedding" not in record: | ||||||||||||||||||||||||||||||||||||||
| return "remember record missing 'embedding'" | ||||||||||||||||||||||||||||||||||||||
| if not isinstance(record["embedding"], list): | ||||||||||||||||||||||||||||||||||||||
| return "remember record 'embedding' must be a list" | ||||||||||||||||||||||||||||||||||||||
| elif op == "connect": | ||||||||||||||||||||||||||||||||||||||
| for field in ("from_id", "to_id", "relation"): | ||||||||||||||||||||||||||||||||||||||
| if field not in record: | ||||||||||||||||||||||||||||||||||||||
| return f"connect record missing '{field}'" | ||||||||||||||||||||||||||||||||||||||
| elif op == "delete": | ||||||||||||||||||||||||||||||||||||||
| if "id" not in record: | ||||||||||||||||||||||||||||||||||||||
| return "delete record missing 'id'" | ||||||||||||||||||||||||||||||||||||||
| elif op in ("checkpoint", "compact"): | ||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| return f"unknown replay op '{op}'" | ||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for index, record in enumerate(reader.operations(), start=1): | ||||||||||||||||||||||||||||||||||||||
| report["total_ops"] += 1 | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| report["total_ops"] += 1 | |
| report["total_ops"] += 1 | |
| # Ensure each replay record is a JSON object (dict). Non-dict records are malformed. | |
| if not isinstance(record, dict): | |
| if strict: | |
| raise CortexaDBConfigError( | |
| f"Replay op #{index} has invalid format: expected object, " | |
| f"got {type(record).__name__}: {record!r}" | |
| ) | |
| report["op_counts"]["unknown"] += 1 | |
| add_failure( | |
| index=index, | |
| op="unknown", | |
| reason="non-dict replay record (expected JSON object)", | |
| record=record, | |
| ) | |
| continue |
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In strict mode, the remember failure path re-raises a new CortexaDBError but does not chain the original exception (raise ... from e). Chaining would preserve the traceback and make replay failures significantly easier to diagnose.
| ) | |
| ) from e |
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception handlers for connect/delete/checkpoint/compact drop the original exception (except Exception:) and, in strict mode, re-raise without chaining. This makes strict-mode failures and non-strict diagnostics harder to debug compared to the remember branch which includes the exception message. Capture the exception as e, include e in the raised message / failure reason, and use exception chaining (raise ... from e) to preserve the root cause.
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delete exception handler uses except Exception: and then re-raises in strict mode without including/chaining the original exception. Capture the exception (as e), include it in the strict-mode error, and use raise ... from e so callers can see the underlying failure reason.
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkpoint exception handler drops the original exception (except Exception:) and the strict-mode error message has no underlying details. Capture the exception as e and chain it (raise ... from e) so failures are diagnosable.
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compact exception handler drops the original exception (except Exception:) and re-raises without chaining in strict mode. Capture the exception as e and use raise ... from e (and optionally include e in the message) to preserve the root cause for diagnostics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validate_record()only checks field presence forconnect/deletebut not that values are non-null and of the expected types (e.g.,id,from_id,to_idshould be ints;relationshould be a string). As written, records like{"op":"delete","id":null}pass validation and then fail later as an execution error (counted asfailed) rather than being treated as malformed input (skipped) or failing fast in strict mode. Consider extending validation to enforce basic types/non-null (and optionally checkembeddinglength against the header dimension) so strict/non-strict behavior matches the documented intent.