Skip to content

Commit 395b5cb

Browse files
jensensclaude
andcommitted
Fix new objects not indexed: use obj.__dict__ fallback when _p_oid is None
ZODB assigns OIDs in Connection.commit(), which runs after before_commit hooks (where IndexQueue flushes). All new objects therefore have _p_oid=None at catalog_object() time, causing the catalog to silently skip them. When zoid is None, store pending catalog data in obj.__dict__[ANNOTATION_KEY] instead of set_pending(). CatalogStateProcessor.process() has an existing fallback that pops ANNOTATION_KEY from the state dict during store(), so the annotation is never persisted to the database. Fixes #27. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc7645f commit 395b5cb

3 files changed

Lines changed: 64 additions & 17 deletions

File tree

CHANGES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 1.0.0b14
4+
5+
### Fixed
6+
7+
- Fix new objects not being indexed in PostgreSQL.
8+
ZODB assigns object IDs (`_p_oid`) during `Connection.commit()`, which runs
9+
*after* `before_commit` hooks (where the IndexQueue flushes). All new objects
10+
therefore have `_p_oid=None` at `catalog_object()` call time, causing the
11+
catalog to silently skip them. The fix stores pending catalog data directly
12+
in `obj.__dict__` under the `_pgcatalog_pending` key when no OID is available
13+
yet; `CatalogStateProcessor.process()` pops and uses it during `store()` so
14+
the annotation is never persisted to the database. Fixes #27.
15+
316
## 1.0.0b13
417

518
### Fixed

src/plone/pgcatalog/catalog.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from plone.pgcatalog.pool import get_pool
5757
from plone.pgcatalog.pool import get_request_connection
5858
from plone.pgcatalog.pool import get_storage_connection
59+
from plone.pgcatalog.processor import ANNOTATION_KEY
5960
from plone.pgcatalog.query import apply_security_filters
6061
from plone.pgcatalog.search import _PendingBrain
6162
from plone.pgcatalog.search import _run_search
@@ -423,11 +424,6 @@ def _set_pg_annotation(self, obj, uid=None):
423424
return False
424425

425426
zoid = self._obj_to_zoid(obj)
426-
if zoid is None:
427-
log.debug(
428-
"_set_pg_annotation: no _p_oid on %s at %s", type(obj).__name__, uid
429-
)
430-
return False
431427

432428
wrapper = self._wrap_object(obj)
433429
idx = self._extract_idx(wrapper)
@@ -440,15 +436,38 @@ def _set_pg_annotation(self, obj, uid=None):
440436
idx["path_parent"] = parent_path
441437
idx["path_depth"] = path_depth
442438

443-
set_pending(
444-
zoid,
445-
{
446-
"path": uid,
447-
"idx": idx,
448-
"searchable_text": searchable_text,
449-
"content_type": content_type,
450-
},
451-
)
439+
pending_data = {
440+
"path": uid,
441+
"idx": idx,
442+
"searchable_text": searchable_text,
443+
"content_type": content_type,
444+
}
445+
446+
if zoid is None:
447+
# New object: _p_oid not yet assigned by ZODB (OIDs are assigned
448+
# during Connection.commit(), after before-commit hooks run).
449+
# Store catalog data directly in the object's __dict__ as
450+
# ANNOTATION_KEY. When ZODB later calls storage.store() for this
451+
# object, CatalogStateProcessor.process() finds and pops the key
452+
# from the JSON state before writing to PG, so it does not persist
453+
# in the database. Safe for new objects because CMFEditions does
454+
# not version content at creation time.
455+
log.debug(
456+
"_set_pg_annotation: no _p_oid on %s at %s, using __dict__ fallback",
457+
type(obj).__name__,
458+
uid,
459+
)
460+
try:
461+
obj.__dict__[ANNOTATION_KEY] = pending_data
462+
except AttributeError:
463+
log.warning(
464+
"_set_pg_annotation: can't store annotation on %s (no __dict__)",
465+
type(obj).__name__,
466+
)
467+
return False
468+
return True
469+
470+
set_pending(zoid, pending_data)
452471
# Mark the object as dirty so ZODB stores it (triggering the processor)
453472
obj._p_changed = True
454473
log.debug(

tests/test_catalog_plone.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,29 @@ def _cm(_self):
229229

230230

231231
class TestCatalogObjectWritePath:
232-
def test_skips_pg_without_p_oid(self):
232+
def test_new_object_uses_dict_fallback(self):
233+
"""New objects (no _p_oid yet) store catalog data in obj.__dict__."""
234+
from plone.pgcatalog.processor import ANNOTATION_KEY
235+
233236
tool = PlonePGCatalogTool.__new__(PlonePGCatalogTool)
234237
obj = mock.Mock(spec=["getPhysicalPath"])
235238
obj.getPhysicalPath.return_value = ("", "plone", "doc")
236-
# No _p_oid → _set_pg_annotation bails early, no pending write
237-
with mock.patch("plone.pgcatalog.catalog.set_pending") as pending_mock:
239+
# No _p_oid → uses __dict__ fallback, set_pending NOT called
240+
with (
241+
mock.patch.object(PlonePGCatalogTool, "_wrap_object", return_value=obj),
242+
mock.patch.object(PlonePGCatalogTool, "_extract_idx", return_value={}),
243+
mock.patch.object(
244+
PlonePGCatalogTool, "_extract_searchable_text", return_value=None
245+
),
246+
mock.patch(
247+
"plone.pgcatalog.catalog.extract_content_type", return_value=None
248+
),
249+
mock.patch("plone.pgcatalog.catalog.set_pending") as pending_mock,
250+
):
238251
tool.catalog_object(obj)
239252
pending_mock.assert_not_called()
253+
assert ANNOTATION_KEY in obj.__dict__
254+
assert obj.__dict__[ANNOTATION_KEY]["path"] == "/plone/doc"
240255

241256
def test_sets_pending_annotation(self):
242257
"""catalog_object() sets PG pending annotation (no BTree writes)."""

0 commit comments

Comments
 (0)