memgres 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgres-0.3.0 → memgres-0.3.2}/PKG-INFO +1 -1
- {memgres-0.3.0 → memgres-0.3.2}/memgres/diffing.py +11 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/mcp_server.py +4 -1
- {memgres-0.3.0 → memgres-0.3.2}/memgres/store.py +6 -1
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/PKG-INFO +1 -1
- {memgres-0.3.0 → memgres-0.3.2}/pyproject.toml +1 -1
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_diffing.py +23 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_store_integration.py +16 -1
- {memgres-0.3.0 → memgres-0.3.2}/LICENSE +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/README.md +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/__init__.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/blame.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/config.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/embeddings.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/identity.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/info.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/migrations/0001_core.sql +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/migrations/0002_identity.sql +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/schema.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/search.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/segments.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/server.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/__init__.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/base.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/pgvector.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres/vector/qdrant.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/SOURCES.txt +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/dependency_links.txt +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/entry_points.txt +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/requires.txt +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/memgres.egg-info/top_level.txt +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/setup.cfg +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_blame_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_config.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_embeddings.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_identity_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_lexical_match.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_limits.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_list.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_mcp_recall_schema.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_qdrant_ca.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_qdrant_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_search_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_security_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_segments.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_segments_store.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_server_info.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_server_integration.py +0 -0
- {memgres-0.3.0 → memgres-0.3.2}/tests/test_snippets.py +0 -0
|
@@ -69,6 +69,7 @@ def apply_diff(old: str, patch: str) -> str:
|
|
|
69
69
|
lines = patch.split("\n")
|
|
70
70
|
n = len(lines)
|
|
71
71
|
p = 0
|
|
72
|
+
hunks = 0 # guard: a non-empty patch must apply ≥1 hunk
|
|
72
73
|
|
|
73
74
|
def marker_next(idx: int) -> bool:
|
|
74
75
|
return idx + 1 < n and lines[idx + 1].startswith("\\ No newline")
|
|
@@ -80,8 +81,13 @@ def apply_diff(old: str, patch: str) -> str:
|
|
|
80
81
|
continue
|
|
81
82
|
m = _HUNK.match(line)
|
|
82
83
|
if not m:
|
|
84
|
+
# A `@@`-line that isn't a valid header is a malformed hunk, not a
|
|
85
|
+
# skippable stray — reject it rather than silently dropping the edit.
|
|
86
|
+
if line.startswith("@@"):
|
|
87
|
+
raise DiffConflict(f"malformed hunk header: {line!r}")
|
|
83
88
|
p += 1
|
|
84
89
|
continue
|
|
90
|
+
hunks += 1
|
|
85
91
|
|
|
86
92
|
old_start = int(m.group(1))
|
|
87
93
|
# @@ lines are 1-based; a hunk adding to an empty side uses -0 → 0.
|
|
@@ -114,6 +120,11 @@ def apply_diff(old: str, patch: str) -> str:
|
|
|
114
120
|
p += 1
|
|
115
121
|
|
|
116
122
|
out.extend(src[i:])
|
|
123
|
+
# A non-empty patch that matched no hunk header applied nothing — that is a
|
|
124
|
+
# malformed / non-unified-diff payload, not a legitimate no-op. Fail loudly
|
|
125
|
+
# instead of silently returning the body unchanged.
|
|
126
|
+
if hunks == 0:
|
|
127
|
+
raise DiffConflict("patch contains no @@ hunk — malformed or not a unified diff")
|
|
117
128
|
return "".join(out)
|
|
118
129
|
|
|
119
130
|
|
|
@@ -27,7 +27,10 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
from typing import List, Literal, Optional
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
try: # mcp SDK >= 2.0 renamed the module fastmcp -> mcpserver
|
|
31
|
+
from mcp.server.mcpserver import Context
|
|
32
|
+
except ImportError: # mcp SDK 1.x
|
|
33
|
+
from mcp.server.fastmcp import Context
|
|
31
34
|
|
|
32
35
|
from . import identity
|
|
33
36
|
from .config import Config, load
|
|
@@ -24,7 +24,7 @@ import psycopg
|
|
|
24
24
|
|
|
25
25
|
from . import identity
|
|
26
26
|
from .config import Config
|
|
27
|
-
from .diffing import apply_diff, byte_len, content_hash, make_diff
|
|
27
|
+
from .diffing import DiffConflict, apply_diff, byte_len, content_hash, make_diff
|
|
28
28
|
from .embeddings import Embedder, get_embedder
|
|
29
29
|
from .vector import make_backend
|
|
30
30
|
|
|
@@ -221,6 +221,11 @@ class Store:
|
|
|
221
221
|
raise Conflict(f"stale diff: base {base_hash[:12]} != current {cur_hash[:12]}")
|
|
222
222
|
self._check_write_size(diff)
|
|
223
223
|
new_body = apply_diff(cur_body, diff)
|
|
224
|
+
# A diff that leaves the body identical applied nothing meaningful —
|
|
225
|
+
# never bump seq on a silent no-op (belt to apply_diff's malformed
|
|
226
|
+
# guard: also catches an empty or net-zero diff).
|
|
227
|
+
if new_body == cur_body:
|
|
228
|
+
raise DiffConflict("diff applied but changed nothing — empty or no-op diff")
|
|
224
229
|
op = "diff"
|
|
225
230
|
elif body is not None:
|
|
226
231
|
if base_hash is not None and base_hash != cur_hash:
|
|
@@ -53,6 +53,29 @@ def test_apply_to_changed_base_raises():
|
|
|
53
53
|
apply_diff(drifted, patch)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
# ─── malformed patches must fail loudly, never silently no-op ────────────
|
|
57
|
+
def test_malformed_hunk_header_raises():
|
|
58
|
+
# A bare `@@` (no `-a,b +c,d` line spec) is not a valid hunk header. It used
|
|
59
|
+
# to be silently skipped, so the whole patch applied nothing and the body
|
|
60
|
+
# came back unchanged — a silent no-op. Now it raises.
|
|
61
|
+
bad = "@@\n-old line\n+new line\n"
|
|
62
|
+
with pytest.raises(DiffConflict):
|
|
63
|
+
apply_diff("old line\n", bad)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_patch_without_any_hunk_raises():
|
|
67
|
+
# Non-empty payload that contains no @@ hunk at all applied nothing → raise,
|
|
68
|
+
# rather than returning the source body untouched.
|
|
69
|
+
with pytest.raises(DiffConflict):
|
|
70
|
+
apply_diff("body\n", "-body\n+new\n")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_empty_patch_is_still_a_noop():
|
|
74
|
+
# A genuinely empty patch is the one legitimate no-op (unchanged behavior).
|
|
75
|
+
assert apply_diff("keep\n", "") == "keep\n"
|
|
76
|
+
assert apply_diff("keep\n", " \n") == "keep\n"
|
|
77
|
+
|
|
78
|
+
|
|
56
79
|
# ─── byte length is measured in UTF-8, not characters ────────────────────
|
|
57
80
|
def test_byte_len_utf8():
|
|
58
81
|
assert byte_len("abc") == 3
|
|
@@ -14,7 +14,7 @@ import pytest # noqa: E402
|
|
|
14
14
|
psycopg = pytest.importorskip("psycopg")
|
|
15
15
|
|
|
16
16
|
from memgres.config import load # noqa: E402
|
|
17
|
-
from memgres.diffing import make_diff, content_hash # noqa: E402
|
|
17
|
+
from memgres.diffing import make_diff, content_hash, DiffConflict # noqa: E402
|
|
18
18
|
from memgres.schema import migrate # noqa: E402
|
|
19
19
|
from memgres.store import Store, Conflict, NotFound, TooLarge, NoParent # noqa: E402
|
|
20
20
|
|
|
@@ -80,6 +80,21 @@ def test_diff_requires_base_hash(store):
|
|
|
80
80
|
store.write(id=m.id, diff=make_diff("x\n", "y\n"))
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_malformed_diff_raises_and_does_not_bump_seq(store):
|
|
84
|
+
# Regression (diff_silently_noops): a diff whose hunk header doesn't parse
|
|
85
|
+
# used to silently apply nothing — body unchanged but seq++ / updated_at
|
|
86
|
+
# moved, looking like a successful write. Now it raises and touches nothing.
|
|
87
|
+
m = store.write(body="alpha\nbeta\n")
|
|
88
|
+
assert m.seq == 1
|
|
89
|
+
bad = "@@\n-alpha\n+ALPHA\n" # bare @@ — not a valid unified-diff hunk
|
|
90
|
+
with pytest.raises(DiffConflict):
|
|
91
|
+
store.write(id=m.id, diff=bad, base_hash=m.content_hash)
|
|
92
|
+
again = store.get(None, m.id)
|
|
93
|
+
assert again.body == "alpha\nbeta\n" # body untouched
|
|
94
|
+
assert again.content_hash == m.content_hash # hash unchanged
|
|
95
|
+
assert again.seq == 1 # seq NOT bumped
|
|
96
|
+
|
|
97
|
+
|
|
83
98
|
def test_replace_and_retag(store):
|
|
84
99
|
m = store.write(body="body\n", tags=["old"])
|
|
85
100
|
m2 = store.write(id=m.id, body="new body\n", tags=["new"], reason="replace")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|