Changelog¶
1.0.0b64¶
Fixed¶
_process_sortnow emitsidx->'{key}'(JSONB operator) instead ofidx->>'{key}'(text operator) for theFieldIndexfallback. Text-cast sorting compared everything lexicographically, so aFieldIndexover a numeric attribute ranked"10"before"2". JSONB comparison is type-aware: numbers sort numerically, strings lexically, so a homogeneousFieldIndexnow always sorts correctly regardless of value type. Affects anyFieldIndexwith numeric source data (counters, priorities, prices, weights). #158
1.0.0b63¶
Changed¶
PGCatalogBrainno longer stores a reference to the catalog tool and no longer consultsself._catalog.REQUESTfor URL rendering.getURLuseszope.globalrequest.getRequest();getObjectand_unrestrictedGetObjectresolve a traversal root lazily via a new_traversal_root()helper (getSite().getPhysicalRoot()first,getRequest().PARENTS[-1]as fallback). This keeps brains catalog-independent so callers can cache / pickle / re-queue them without dragging the Acquisition chain along. The catalog reference that the ZODB-prefetch batch needs has moved onto the transientCatalogSearchResultscontainer, where it belongs.The
catalog=keyword onPGCatalogBrain.__init__is accepted but ignored — kept until the next major version for signature compatibility with direct callers.
1.0.0b62¶
Fixed¶
_process_indexnow falls back to the correct handler for built-in Plone indexes when they are missing from theIndexRegistry— previously the miss fell through to_handle_fieldwhich emitsidx->>'name', bypassing the dedicated typed columns and their indexes. On aaf-6 prod this produced 4-9 second seq-scans over a 450k-rowobject_statetable for every folder-listing query, saturating the worker pool and causing intermittent Varnish backend-fetch errors. Resolution happens via a new_builtin_index_type(name)helper that combines two sources: the three Plone-native specials (path,effectiveRange,SearchableText) hardcoded because their SQL lives inside the handler, and everyTEXT[]-typedExtraIdxColumn(derived at dispatch time — currentlyallowedRolesAndUsersandobject_provides, extensible viaregister_extra_idx_column). The correct handler then usespath/allowed_roles/object_provides/searchable_textcolumns and the DateRangeIndex composite clause. Explicit registry entries still win so addons can override behavior. Closes #154._handle_keywordno longer crashes withTypeError: 'DateTime' object is not iterablewhen a caller passes a non-str, non-iterable value (e.g. a ZopeDateTimeor a Pythondatetime) as the query for a KeywordIndex. The old coercion assumed the value was either astror iterable; anything else (DateTime,int, …) hitlist(value)and raised. Values are now coerced viastr()into a single-element list, matching the JSONB storage shape (keyword arrays always contain strings). Closes #152.
1.0.0b61¶
Fixed¶
_field_rangeon FieldIndex no longer silently returns zero rows for numeric range queries. Two stacked bugs:[max, min]order from the caller was not normalized (produced always-false SQL), andidx->>'key'comparisons ran lexicographically on text — so'46.1' <= '5.0' <= '49.0'included values outside the range._field_rangenow sorts min/max and castsidx->>'key'to::numericwhen the range values areint/float. String values (ISO dates etc.) keep text comparison. Affected aaf-6 prod map-widget bbox filter viacollective.collectionfilter— closes #150._CatalogCompat.getIndexand_CatalogIndexesView.__getitem__no longer silently fall back to the raw ZCatalog index when they cannot find the catalog tool — that fallback returned empty BTrees and masked #143 / #146 for weeks. A new private helper_resolve_catalogtries three paths in order (__parent__→ Acquisition chain →zope.component.hooks.getSite().portal_catalog) and raisesRuntimeErrorif all three fail._CatalogCompat.indexesproperty now self-heals a missing__parent__on first access viagetSite().portal_catalog. First page render after deploy persists__parent__; no second upgrade-step click required on sites where the #139 upgrade ran before that fix landed. Zero-touch prod recovery.
Added¶
PGIndex._apply_index(request, resultset=None)— ZCatalog- compatible low-level query entry point. Returns(IITreeSet(zoids), (index_name,)). Reuses_QueryBuilder._process_indexso every registered IndexType (FIELD, KEYWORD, PATH, DATE, DATE_RANGE, UUID, TEXT, BOOLEAN, GOPIP) plus everyIPGIndexTranslatorutility works for free. No implicit security filtering — matches ZCatalog semantics; usecatalog(**query)for secured results. Emits aDeprecationWarningonce per caller site._PGIndexMapping.__getitem__/__len__— round out the PG-backed mapping so Plone core callers (plone.app.uuid.utils,plone.app.vocabularies.Keywords) work againstcatalog._catalog.getIndex(name)._indexwithout needingcatalog.Indexes[name]acquisition._PGIndexMapping.items()/values()raiseNotImplementedErrorwith guidance pointing atuniqueValues,_apply_index, andcatalog(**query)as alternatives. No Plone-core caller uses them on a wrapped index; a concrete usecase can land in a future issue with server-side-cursor streaming.PGIndex._indexproperty emits aDeprecationWarningon access — signals callers that the BTree-shaped API is an emulation and suggests the preferred pgcatalog-native alternatives.
Closes #146.
1.0.0b60¶
Fixed¶
_PGIndexMapping(backingPGIndex._index) is now iterable and branches its SQL on the index type.plone.app.vocabularies.Keywordsiteratesindex._indexdirectly to populate the tag-autocomplete widget in the Plone edit form — the previous mapping had no__iter__and itskeys()coerced JSONB arrays to their text representation. Net effect on b59: typing into theSchlagwortfield offered zero suggestions even when matching keywords existed.keys()/__iter__now use the sameUNION ALLexpansion asuniqueValuesfor KEYWORD, andget()usesidx->key @> to_jsonb(value::text)so membership checks against a keyword actually match. Follow-up to #143.
1.0.0b59¶
Fixed¶
PGIndex.uniqueValues()now branches on the wrapped index type. ForIndexType.KEYWORDthe JSONB value is a list of tags, so the SQL usesjsonb_array_elements_textto expand it into individual entries. Previously all index types went throughidx->>key, which coerces a JSONB array to its JSON text representation — producing entries like'["Werkvortrag", "Tirol"]'instead of'Werkvortrag'/'Tirol'. Callers (the querystring composer vocabulary,plone.app.vocabularies.Keywords,collective.collectionfiltertag clouds, etc.) now see the distinct set of elements as expected.A defensive
UNION ALLbranch treats a scalar row under the same keyword key (corrupt/legacy data) as a single-value keyword so the query does not raisecannot extract elements from a scalar._maybe_wrap_indexpasses the registeredIndexTypethrough toPGIndexso callers that build the wrapper directly keep getting the (correct) scalar path by default.Closes #143.
1.0.0b58¶
Fixed¶
Register
SanitizeRowsModifieras anIQueryModifierutility so that malformed Collection-query rows (missing or non-stringifield) are dropped beforeplone.app.querystring.parseFormqueryprocesses them. Upstreamqueryparser.py:73otherwise setsquery[None] = ...and the subsequentcatalog(**parsedquery)inquerybuilder._makequeryfails at the Python level withTypeError: keywords must be strings— which the Collection edit widget cannot recover from. With the sanitizer in place the preview renders again, so editors can open their Collections and repair corruptedSubject/ tag data through the UI.This is a defensive workaround for an upstream gotcha, not a fix for the underlying
plone.app.querystringbehavior. Closes #142.
1.0.0b57¶
Fixed¶
PGCatalogBrain.getObject()now mirrors upstreamProducts.ZCatalog.CatalogBrains.AbstractCatalogBrain.getObject: the parent path is traversed unrestricted and only the final object isrestrictedTraverse-checked. Previously the full path went throughrestrictedTraverse, so any intermediate container with stricter permissions than the leaf raisedAccessControl.unauthorized.Unauthorized— even though the catalog filter had already authorized access to the target. Sites with a private parent folder publishing individual public items (the common “kalender/event-xyz” pattern) hit this on every anonymous render. Closes #141.
1.0.0b56¶
Fixed¶
The v1->v2 upgrade step was silently no-op on production sites. GenericSetup invokes upgrade handlers with the
portal_setuptool as the context, but_resolve_compatonly understood theImportContextshape (getSite()), so the setup-tool call path hit thereturn None, Nonebranch and loggedmigrate_catalog_indexes: no _CatalogCompat found; skipping— while GenericSetup happily bumped the profile version to 2. Net effect: the persisted_CatalogCompatkept its legacyindexesattribute and the newindexesproperty then raisedAttributeErrorfor_raw_indexes, which Acquisition swallowed and replaced with the tool’sindexes()method — surfacing as'function' object has no attribute 'keys'fromcatalog.indexes.keys()and'method' object is not subscriptablefromcatalog.indexes[name]._resolve_compatnow also walksaq_parent(context)to reach the Plone site when the context is the setup tool, so the normal ZMImanage_upgradespath migrates the state as intended.Made
_CatalogCompat.indexesself-healing: if the legacyindexesattribute is still in__dict__(unmigrated or fresh-install site that skipped v1), the property moves it to_raw_indexeson first access and marks the instance dirty. This avoids the Acquisition-swallowedAttributeErrorfailure mode even when the upgrade step never ran.Closes #139.
1.0.0b55¶
Fixed¶
Plone and addon code commonly reaches into the catalog via the non-API- conform pattern
catalog._catalog.indexes[name]/.get(name)/.items(). Previously this returned the raw ZCatalog index objects with empty BTrees, so queries against them silently returned no results._CatalogCompat.indexesis now a property returning a transient view that wraps each index withPGIndex(same behavior ascatalog.Indexes[name]). CustomPATH-type indexes and other special indexes (idx_key=None) continue to be returned raw, since they have dedicated typed columns and don’t need PG-backed wrapping.Migration: GenericSetup profile bumped from v1 to v2. The upgrade step renames the persisted
indexesattribute to_raw_indexesand sets__parent__on the compat soaq_parentcan reach the catalog tool through bare attribute access. Run Plone Site Setup → Add-ons → plone.pgcatalog → Upgrade on existing sites, or let the nextrunAllImportStepson the default profile pick it up.Likely-affected callers include
plone.base.utils.check_id(reserved-name check),plone.restapi.search.query.Query.get_index,plone.app.discussion,plone.app.referenceablebehavior,plone.volto,collective.collectionfilter, andcollective.exportimport— per-package verification is recommended after upgrade.Based on prior prototyping by @thet on
thet/indexes-wrapper. Closes #137.
1.0.0b54¶
Changed¶
Stop duplicating
path,path_parent, andpath_depthbetween the typed columns onobject_stateand theidxJSONB. These three fields now live exclusively in their typed columns (path,parent_path,path_depth) — previously identical values were stored in both places, wasting ~10 % of JSONB storage and (more importantly) blocking the planner from collecting selectivity statistics on path-subtree filters. Indexes and extended statistics on these fields have been migrated to reference the typed columns directly. CustomPATH-type indexes (e.g.tgpath) are unaffected and continue to store their data inidx.Migration: Schema and writer changes are picked up automatically on startup (the eight affected indexes and three extended-statistics objects are reissued with idempotent
DROP … IF EXISTS/CREATE … IF NOT EXISTSpairs). To strip the obsolete keys from existing JSONB on large catalogs, run:from plone.pgcatalog.migrations.strip_path_keys import run run(conn, batch_size=5000)
Safe to run online, idempotent, batched. Issue #132.
1.0.0b53¶
Fixed¶
Migration install handler silently dropped every
DateRecurringIndex(DRI) — e.g.plone.app.event’s /bda.aaf.site’sgeneral_start/general_end— when replacing a foreignportal_catalogwithPlonePGCatalogTool._snapshot_catalogcorrectly captured the storedattr_recurdef/attr_untilattributes, but_build_extrahad no DRI branch, so the restoredextranamespace lacked therecurdef/untilkeys thatDateRecurringIndex.__init__reads. The constructor raisedAttributeError, the outertry/exceptin_restore_from_snapshotswallowed it as a warning, and the index was never created — which meantextract_idxnever indexed those fields, the IndexRegistry had no entry for them, and every site-wide Collection filtering ongeneral_endreturned zero results.Added the DRI translation in
_build_extra, plus a roundtrip test that actually instantiatesDateRecurringIndexwith the built extra — the kind of assertion that would have caught this before it ever shipped. Issue #126.Existing deployments that migrated on an affected build have to re-add the missing indexes manually (the upgrade can’t recover them without the original catalog snapshot). Run:
catalog = portal.portal_catalog class _Extra: pass extra = _Extra() extra.recurdef = "recurrence" extra.until = "" for name in ("general_start", "general_end"): if name not in catalog._catalog.indexes: catalog.addIndex(name, "DateRecurringIndex", extra) catalog.reindexIndex("general_start") catalog.reindexIndex("general_end")
Added¶
Slow-query suggestions now produce covering composite indexes for the common
portal_type + effectiveRange + sort_on=effectivepattern (issue #122). The suggestion engine splits the legacy_NON_IDX_FIELDSinto purpose-specific constants, expandseffectiveRangeto itseffectivedate contributor, and appends the query’ssort_onfield as a trailing btree composite column so the planner can skip the ORDER BY sort step.
1.0.0b52¶
Fixed¶
CatalogStateProcessor._enqueue_tika_jobsindexed result rows by integer position (row[0],row[1]), but the request-scoped connection pool uses adict_rowfactory, so every content save that produced an unresolved blob ref raisedKeyError: 0duringtpc_vote(e.g. uploading a Dexterity Image). Switched to column-name access.Existing tests didn’t catch this because the integration tests opened their cursor with
tuple_rowand the unit tests mockedfetchall()with tuple rows — both diverged from production. Tests updated to usedict_rowto match the real pool.
1.0.0b51¶
Added¶
Extended PostgreSQL statistics for every default composite catalog index on
object_state, so the planner has accurate joint-selectivity estimates for the expression pairs we actually index.mcv + dependenciesfor low-cardinality pairs (type + state,parent + type,type + effective,type + expires),dependenciesonly for path-pairs (high-cardinality paths makemcvwasteful; CMS content structure is typically wide-shallow, so dependency signal is what matters).Without these, PG’s per-column histograms treat the expressions as independent and underestimate joint selectivity, so the planner picks a composite-index scan and heap-filters thousands of tuples instead of doing a Bitmap-AND with the available GIN indexes. On a published-Event navigation query observed in production, this dropped query time from 911 ms to sub-100 ms.
On existing installations a one-shot
ANALYZE object_stateruns on the first write transaction after upgrade so the new statistics take effect immediately rather than waiting for autovacuum. Idempotent viapg_stats_extskip check.Issue #122 (PR 1 of 3 — engine refactor and EXPLAIN-driven coverage to follow).
1.0.0b50¶
Fixed¶
release_request_connectionnow issues an explicitconn.rollback()before returning the connection to the pool. Otherwise an implicit transaction opened by a priorSELECTon the pool fallback path stays alive, holding avirtualxidthat blocksCREATE INDEX CONCURRENTLY. Companion fix to bluedynamics/zodb-pgjsonb#58 (the storage-conn path). Closes #118.Suggested Indexes UI: detect already-applied suggestions with mixed-case field names (e.g.
Language) by matching index names case-insensitively — PostgreSQL folds unquoted identifiers to lowercase. Also strengthen expression normalization (whitespace around->>, iterative paren collapse, WHERE-anchored extraction) so generated and PG-storedindexdefforms compare equal.apply_indexis now idempotent when a valid index with the same name already exists — returns success no-op instead of propagating theDuplicateTableerror. Closes #119.Tika enqueue: resolve Dexterity
NamedBlobFile/NamedBlobImagewrapper OIDs via a second-hop lookup throughobject_state, so the queue receives jobs for modern Dexterity File/Image content. Previously_enqueue_tika_jobs()only looked up the OIDs it found in the content’s state — which are the wrapper OIDs, not the innerZODB.blob.BlobOIDs. The direct lookup returned zero rows and the enqueue silently skipped. Flat-state content (legacy/Archetypes- style, where the content state carries a directZODB.blob.Blob@ref) is unchanged. Closes #115._handle_uuidnow accepts list/tuple queries (uses= ANY(...)), matching_handle_fieldsemantics. Previously a list query such ascatalog.searchResults(UID=['f852...'])was stringified asstr(['f852...'])→"['f852...']"and the JSONB->>comparison never matched, so@@getVocabulary?name=plone.app.vocabularies.Catalogwith aplone.app.querystring.operation.list.containscriterion on UID returned an empty vocabulary.catalog._catalog.getIndex(name)now returns aPGIndexwrapper with PG-backed_indexanduniqueValues(), same ascatalog.Indexes[name]. Previously it returned the raw ZCatalog index with empty BTrees, which broke:plone.app.vocabularies.KeywordsVocabulary(empty Subject/Tags dropdowns).Products.CMFPlone.browser.search.Search.types_list()(empty “Item type” filter in@@search).plone.app.event.setuphandlers(DateIndex detection).Other Plone code paths that bypass
catalog.Indexes[name].
Special indexes registered with
idx_key=None(SearchableText, path, effectiveRange) are returned unwrapped so dedicated columns are used for them.
1.0.0b49¶
Added¶
Log all catalog queries for debugging via
PGCATALOG_LOG_ALL_QUERIES=1(also acceptstrue/yes). Enabled queries are logged at INFO level with duration, SQL, params, and query keys. Params are truncated at 2000 chars to bound log size. The env var is re-checked on every query, so the setting can be toggled at runtime without a restart. Seedocs/how-to/debug-queries.mdfor details and a production-safety warning about logging user-supplied query values.Slow-query log format changed slightly: the prefix is now
Slow SQL catalog query (%.2f ms)instead ofSlow catalog query (%.1f ms)— log-aggregation grep patterns may need an update.
Fixed¶
clearFindAndRebuild()now works on fresh installs and after refactorings that leaveobject_state.pathempty. Previously the rebuild relied on a PG snapshot ofWHERE path IS NOT NULL, so when the column was not yet populated only the Plone root was re-indexed. The rebuild now walks theISiteRootbreadth-first regardless of PG state.The new traversal is still memory-flat: the BFS queue holds only path strings (not objects), and objects are ghosted by
cacheMinimize()after every 500 commits. Also yields discussion items via theIConversationadapter whenplone.app.discussionis installed, so comments on content are included in the rebuild.Stringify Boolean query values to JSON notation (
'true'/'false') so queries against JSONB->>comparisons match. Previouslystr(True)produced'True'which never matched JSONB’s lowercase form, causing queries to return no results. Fix applied inquery.py(all field handlers),pgindex.py(ZCatalog_index.get()compat),addons_compat/eeafacetednavigation.py(faceted search dispatch), andbackends.py(text search backends). Helper renamed from_to_json_stringto_bool_to_lower_strto match what it actually does.Gracefully handle missing
metacolumn in_load_idx_batch()(#105). Falls back toSELECT zoid, idxif the column does not exist yet, preventingUndefinedColumncrash on first read after upgrade. Root cause fix is in zodb-pgjsonb 1.10.4 (poll_invalidationsnow applies deferred DDL before the read snapshot).Show index creation errors in red instead of green in ZMI (#104).
manage_apply_index/manage_drop_indexnow redirect withindex_errorparam on failure, rendered as Bootstrapalert-danger.
1.0.0b48¶
Fixed¶
Fix startup warnings “security declaration for nonexistent method” for unsupported ZCatalog stubs (
getAllBrains,searchAll, etc.).ObjectManager.__class_init__callsInitializeClassat class creation time, so stub methods must be defined in the class body, not via post-hocsetattr.
Changed¶
Extract
@meta,object_provides, andallowedRolesAndUsersfromidxJSONB into dedicated columns via genericExtraIdxColumnmechanism. Reducesidxsize by ~85% (from ~3.2 KB to ~400 B avg, below TOAST threshold). Runclear_and_rebuildafter upgrading. (#98)object_providesqueries now use a dedicatedTEXT[]column with GIN index instead of JSONB containment.Removed
_backfill_allowed_rolesstartup function (superseded by generic extraction mechanism).
1.0.0b47¶
Fixed¶
Fix
ValueError: Invalid path: ''when path query receives empty string. Empty/blank paths are now silently filtered, matching ZCatalog behavior.
1.0.0b46¶
Fixed¶
Query cache: use catalog-specific change counter instead of
MAX(tid)(#94). The cache was invalidated on every ZODB write (~2500/hour from ScalesDict alone), making it nearly useless (~28% hit rate). Now usespgcatalog_change_seqwhich only increments on actual catalog writes (catalog_object, uncatalog, reindex, move). Expected hit rate 90%+ on typical sites.
1.0.0b45¶
Fixed¶
Fix Tika queue never populated:
content_typealways None (#90). Removed brokenextract_content_type()—IPrimaryFieldInfocan’t adapt the indexer wrapper and Dexterity items have no top-levelcontent_typeattribute. MIME type is now read directly fromidx["mime_type"](the standard Plone catalog index), which is reliably extracted by the IndexRegistry.Fix suggestion index existence check + dedicated KEYWORD fields (#92).
_check_covered()now compares by index name (reliable) with normalized expression fallback.object_providesandSubjectadded to_DEDICATED_FIELDS— their existing GIN indexes make new suggestions useless.
1.0.0b44¶
Added¶
Smart index suggestions in ZMI Slow Queries tab (#86). Replaces the naive
_suggest_index()with field-type-aware suggestions using the IndexRegistry. Generates correct DDL per IndexType (btree expression, GIN, tsvector, composites). Detects already-covered fields (dedicated columns, existing indexes). Manual “Apply” button creates indexes viaCREATE INDEX CONCURRENTLY. “Drop” button for removing suggestion indexes (idx_os_sug_*). On-demand EXPLAIN plans for slow queries. Newsuggestions.pymodule with pure suggestion engine + DB helpers.
1.0.0b43¶
Fixed¶
Fix Tika enqueue:
_collect_ref_oids()and theANNOTATION_KEYfallback inCatalogStateProcessor.process()now handle JSON string state (fromdecode_zodb_record_for_pg_json). Previouslystatewas assumed to be a dict, but the fast codec path returns a JSON string — so@refmarkers were never found, no extraction jobs were enqueued, and Tika sat idle.
1.0.0b42¶
Fixed¶
Handle Unix epoch floats/ints in
ensure_date_param(). Callers likeplone.app.textfieldpasstime.time()values as date query params. Now converts todatetime.fromtimestamp(value, tz=UTC). Fixes #82.Skip missing attributes instead of storing null in idx JSONB. Matches ZCatalog semantics: missing attribute = not indexed (key omitted), not “indexed as null”. Fixes #81.
Use
@>containment for single-value KeywordIndex queries instead of?|overlap. The GIN index handles@>much better.object_providesqueries: 2.4s to 650ms. Fixes #80.
1.0.0b41¶
Fixed¶
Register refs prefetch expression for cataloged content objects. Uses
CASE WHEN idx IS NOT NULL THEN refs ENDso only content objects (with catalog data) trigger prefetch. Requires zodb-pgjsonb >= 1.9.2.
1.0.0b40¶
Added¶
Brain object prefetch via
storage.load_multiple(). When iterating search results and callinggetObject(), the first call in each batch prefetches up to 100 objects in a single SQL query, warming the storage cache for subsequent calls. Configurable viaPGCATALOG_PREFETCH_BATCHenvironment variable (default 100, set to 0 to disable).
1.0.0b39¶
Fixed¶
Log query cache TID lookup failures instead of silently swallowing them. Diagnoses why the query cache may not be populating.
1.0.0b38¶
Fixed¶
Extract
allowed_rolesbackfill from schema DDL into batched startup step (#65 Phase 2). Previously the backfill ran as a single UPDATE on 4.4M rows inside ACCESS EXCLUSIVE, blocking the entire database. Now processes 5000 rows per batch with autocommit andlock_timeout. Safe to re-run, idempotent, logs progress.
1.0.0b37¶
Fixed¶
Prevent database lockup during rolling deployments (#65).
_ensure_text_indexes()and_ensure_field_indexes()now setlock_timeout = '5s'to prevent indefinite blocking on ACCESS EXCLUSIVE locks from concurrent REPEATABLE READ sessions. Log level changed fromerrortowarningsince indexes are retried on next startup.
1.0.0b36¶
Added¶
Process-wide query result cache with TID-based invalidation. Caches catalog query results in memory. Invalidated when
MAX(tid)changes (any ZODB commit). Cost-based eviction keeps expensive queries in cache. Configurable viaPGCATALOG_QUERY_CACHE_SIZE(default 200) andPGCATALOG_QUERY_CACHE_TTR(default 60s). Fixes #74.ZMI: Cache Status section on the Slow Queries tab showing hit/miss rate, entries, invalidations, TTR, and top cached queries by cost.
Fixed¶
getCounter()fix:SELECT MAX(tid)returns columnmax, nottid. AddedAS tidalias. Also fixes the test.
1.0.0b35¶
Performance¶
Add partial index
idx_os_cat_nav_visiblefor navigation listings (exclude_from_nav=falseis only ~1.6% of rows). Verified on production: 261ms → 20ms (13×).Add partial index
idx_os_cat_events_upcomingfor calendar/event queries (portal_type=Event+show_in_sidecalendar=true+ end date). Verified on production: 728ms → 33ms (22×).Mark
pgcatalog_to_timestamptz()asPARALLEL SAFEto allow parallel query execution.
Fixed¶
getCounter()now returnsMAX(tid)from PostgreSQL instead of a persistent counter that was never incremented (always returned 0). This enables Plone’s cache invalidation (plone.memoize) for catalog-dependent caches like navigation trees. ~0.2ms via Index Only Scan, no ZODB write overhead.
1.0.0b34¶
Fixed¶
DateRecurringIndex fields (e.g.
start,end) now get auto-created btree expression indexes and appear in the ZMI Indexes tab. AddedDateRecurringIndextoMETA_TYPE_MAP. Query builder now checksIPGIndexTranslatorbefore IndexRegistry, so rrule query logic takes priority. Fixes #71.
1.0.0b33¶
Fixed¶
Revert path_parent IN subquery for bounded-depth queries (#68). The subquery caused Nested Loop plans where PG repeated the
allowed_rolesGIN scan per parent path (615ms). Reverts toLIKE + path_depthwhich uses thepath_depth_typecomposite index (85-300ms depending on cache state).
1.0.0b32¶
Fixed¶
Optimize bounded-depth path queries: rewrite
path LIKE + path_depthtopath_parent IN (subquery)so PG can use the composite(path_parent, portal_type)index. Navigation tree queries drop from 630ms to ~77ms. Fixes #66.
1.0.0b31¶
Added¶
Denormalize
allowedRolesAndUsersinto dedicatedallowed_roles TEXT[]column with GIN index. Security filter queries now useallowed_roles && ARRAY[...]instead of JSONB decompression. Includes automatic backfill migration for existing databases. Navigation queries 85ms to 5-15ms, all queries benefit. Fixes #63.
1.0.0b30¶
Fixed¶
Add dedicated GIN indexes for
allowedRolesAndUsers,object_provides, andSubjectkeyword fields. The full-idx GIN index is too broad for?|queries on individual keyword arrays. Dedicated indexes are much smaller and faster.object_providesqueries drop from 850ms to sub-millisecond.
1.0.0b29¶
Fixed¶
Use btree-friendly expressions instead of GIN containment for FieldIndex single-value, BooleanIndex, and UUIDIndex queries. Root cause of 3-4 second navigation queries on large sites. Navigation queries drop from 3900ms to <1ms.
Fix Slow Queries ZMI tab crash with KeyError when
PGCATALOG_SLOW_QUERY_MSis not set. Fixes #58.Python 3.14 CI compatibility. Fixes #57.
1.0.0b28¶
Added¶
Auto-create btree expression indexes for custom CatalogIndex fields at startup. Only standard Plone fields have hardcoded indexes; custom fields (like
general_end) now get indexes automatically based on the IndexRegistry. Date fields usepgcatalog_to_timestamptz()wrapper. Fixes #49.
1.0.0b27¶
Fixed¶
Add composite indexes for common catalog query patterns. Without these, PG picks a single-column index and sequentially filters all indexed rows (3+ seconds per query, 30+ second page loads). With composite indexes: sub-millisecond. Fixes #50.
New indexes:
(path_parent, portal_type)— folder listings, navigation(path pattern, portal_type)— collections, search(path pattern, path_depth, portal_type)— navigation tree(portal_type, review_state)— workflow-filtered listings
Indexes are created automatically on startup (idempotent DDL).
Added¶
Slow catalog query logging: queries exceeding
PGCATALOG_SLOW_QUERY_MS(default: 10ms) are logged as warnings and recorded in thepgcatalog_slow_queriestable. Fixes #52.ZMI “Slow Queries” tab on portal_catalog: shows aggregated slow query patterns (count, avg/max duration, last seen) with suggested composite index DDL for frequent patterns. Includes a “Clear Stats” button.
1.0.0b26¶
Added¶
ZMI: Tika status card on the Advanced tab showing URL, worker mode, configured content types, extraction queue stats (pending/processing/ done/failed), and IFile transform override status. Fixes #47.
1.0.0b25¶
Fixed¶
reindexIndex(name)now re-extracts index values from ZODB objects instead of reshuffling existing JSONB values. Iterates all cataloged paths, loads viaunrestrictedTraverse, extracts the requested index, and writes a JSONB merge update. Batched commits for memory. Fixes #43.
Added¶
ZMI [reindex] button per index on the Indexes & Metadata tab with confirmation dialog. Calls new
manage_reindexIndexendpoint.ZMI: confirmation dialogs on Advanced tab for “Update Catalog” and “Clear and Rebuild” buttons. Warns that operations may take a while and that Clear and Rebuild destroys catalog data temporarily. Fixes #44.
1.0.0b24¶
Changed¶
clearFindAndRebuildnow uses PG-driven iteration instead ofZopeFindAndApply. Queriesobject_statedirectly, filtering out known non-content classes (~96% of rows). No acquisition parent chains on the call stack meanscacheMinimize()can ghost all objects — flat memory on large sites. Fixes #39.
Added¶
Skip
portal_transformstext extraction forIFilewhenPGCATALOG_TIKA_URLis set. The async Tika worker handles blob text extraction — no more synchronous pdftotext/wv calls or BFS graph traversal of the transform registry during indexing. Custom types with blob fields need their own override (see docs). Fixes #41.
1.0.0b23¶
Fixed¶
Fix Tika worker queue never being populated. Content objects (File/Image) and their Blob sub-objects have different ZODB oids. The enqueue logic now extracts
@refoids from the content state to resolve the actual blob zoid inblob_state. The queue stores bothzoid(content, for searchable_text update) andblob_zoid(for blob data fetch). Fixes #37.
1.0.0b22¶
Fixed¶
Fix high memory usage during catalog rebuild.
clearFindAndRebuildandrefreshCatalognow commit every 500 objects, flushing dirty ZODB objects and pending catalog data socacheMinimize()can actually reclaim memory. Previously,_p_changed = Trueon every indexed object prevented deactivation until the end of the (single) transaction.
1.0.0b21¶
Fixed¶
Reduce memory usage during catalog rebuild.
clearFindAndRebuildandrefreshCatalognow deactivate ZODB objects after indexing and periodically callcacheMinimize()to keep RAM usage flat on large sites. Folderish objects are kept active during tree traversal to avoid redundant reloads.
1.0.0b20¶
Fixed¶
Fix UID expression index using wrong case (
idx->>'uid'instead ofidx->>'UID'). JSONB keys are case-sensitive, so the old index was never used. Also addCREATE STATISTICSfor UID selectivity so the query planner picks the correct index on large tables. Existing databases are migrated automatically on next startup. Fixes #28.
1.0.0b19¶
Removed¶
Remove “Blob Storage” ZMI tab from portal_catalog. Blob storage statistics are now provided by zodb-pgjsonb >= 1.5.2 in the Zope Control Panel under Database management.
1.0.0b18¶
Fixed¶
Fix computed index extraction (
is_folderish,is_default_page,sortable_title, etc.) always returningnull.IPGCatalogToolextended bothICatalogToolandIPloneCatalogTool, causingICatalogToolto come first in the interface resolution order. CMFCore’sIndexableObjectWrapper(which does not resolve plone.indexer adapters) won over the plone.indexer wrapper. Fixed by extendingIPloneCatalogToolonly —ICatalogToolis already provided viaIZCatalog.
1.0.0b17¶
Security¶
CAT-Q1: Validate unknown query keys before SQL interpolation in
_process_index()fallback path. Unregistered index names are now checked withvalidate_identifier()before being interpolated into JSONB field query expressions, preventing potential SQL injection via crafted query dict keys.CAT-S1: Replace f-string DDL in
_ensure_text_indexes()withpsycopg.sql.SQL/Identifier/Literalcomposition for defense-in-depth.
Changed¶
CAT-P1:
reindex_index()now uses a server-side cursor with batched fetches instead of loading all rows into memory at once. Progress is logged after each batch.
Fixed¶
CAT-O1: Index/metadata extraction failures in
extraction.pynow emitlog.debug()messages with field name and exception info instead of silently passing. Translator extraction failures are also logged.CAT-O2: Startup degradation (failed registry sync, failed text index creation) now logs at
ERRORlevel with actionable context messages instead ofWARNING/DEBUG.CAT-L1: Fallback connection pool (
_fallback_poolfromPGCATALOG_DSNenv var) now registers anatexitclose hook for clean shutdown.Install step now runs
clearFindAndRebuild()after catalog replacement to index all existing content into PostgreSQL. Previously, content created before pgcatalog was installed (e.g. during Plone site creation) had nopath/idxdata, causing empty navigation and search results.
1.0.0b16¶
Added¶
Add “Blob Storage” ZMI tab to portal_catalog showing blob statistics (total count, size, per-tier breakdown for PG/S3), a logarithmic size distribution histogram, and S3 tiering threshold visualization.
1.0.0b15¶
Fixed¶
Protect PlonePGCatalogTool from being replaced during GenericSetup profile imports. CMFPlone’s baseline
toolset.xmldeclaresportal_catalogwithCatalogTool; sincePlonePGCatalogToolis a different class, the defaultimportToolsetdeletes it, triggering anIObjectModifiedEventcascade that raisesKeyError: 'portal_catalog'. AddedimportToolsetwrapper inoverrides.zcmlthat skipsportal_catalogwhen it is already aPlonePGCatalogTool.
1.0.0b14¶
Fixed¶
Fix new objects not being indexed in PostgreSQL. ZODB assigns object IDs (
_p_oid) duringConnection.commit(), which runs afterbefore_commithooks (where the IndexQueue flushes). All new objects therefore have_p_oid=Noneatcatalog_object()call time, causing the catalog to silently skip them. The fix stores pending catalog data directly inobj.__dict__under the_pgcatalog_pendingkey when no OID is available yet;CatalogStateProcessor.process()pops and uses it duringstore()so the annotation is never persisted to the database. Fixes #27.
1.0.0b13¶
Fixed¶
Preserve original Python types for metadata columns (e.g.
brain.effectivenow returns a ZopeDateTimeobject instead of an ISO string). Non-JSON-native metadata values (DateTime, datetime, date, etc.) are encoded via the Rust codec intoidx["@meta"]at write time and restored on brain attribute access with per-brain caching. JSON-native values (str, int, float, bool, None) remain in top-levelidxunchanged. Backward compatible — old data without@metastill works. Fixes #23.
1.0.0b12¶
Fixed¶
Fix
clearFindAndRebuildproducing wrong paths (missing portal id prefix, e.g./newsinstead of/Plone/news), indexingportal_catalogitself, and not re-indexing the portal root object. Now usesgetPhysicalPath()for authoritative paths,aq_base()for identity comparison through Acquisition wrappers, and explicitly indexes the portal root before traversal (matching Plone’sCatalogTool). Fixes #21.
1.0.0b11¶
Fixed¶
Fix example
requirements.txt: use local editable path forpgcatalog-exampleinstead of bare package name (not on PyPI). Fixes #18.Fix ZMI “Update Catalog” and “Clear and Rebuild” buttons returning 404. Added missing
manage_catalogReindexandmanage_catalogRebuildmethods. Fixes #19.Fix
clearFindAndRebuildindexing non-content objects (e.g.acl_users). Now filters for contentish objects only (those with areindexObjectmethod), matching Plone’sCatalogToolbehavior. Fixes #20.
Changed¶
uniqueValuesFor(name)is now a supported API (no longer deprecated). It delegates tocatalog.Indexes[name].uniqueValues().
1.0.0b10¶
Changed¶
Clean break from ZCatalog:
PlonePGCatalogToolno longer inherits fromProducts.CMFPlone.CatalogTool(and transitivelyZCatalog,ObjectManager, etc.). The new base classes areUniqueObject + Folder, providing a minimal OFS container for index objects and lexicons while eliminating the deep inheritance chain.This improves query performance by ~2x across most scenarios (reduced Python-side overhead from attribute lookups, security checks, and Acquisition wrapping) and write performance by ~5% (lighter commit path).
A
_CatalogCompatpersistent object provides_catalog.indexesand_catalog.schemafor backward compatibility with code that accesses ZCatalog internal data structures. Existing ZODB instances with the old_catalog(fullCatalogobject) continue to work without migration.ZCML override for eea.facetednavigation: Moved from
<includeOverrides>insideconfigure.zcmlto a properoverrides.zcmlat the package root, loaded by Zope’sfive:loadProductsOverrides. Fixes ZCML conflict errors when both eea.facetednavigation and plone.pgcatalog are installed.
Added¶
eea.facetednavigation adapter:
PGFacetedCataloginaddons_compat/eeafacetednavigation.py– PG-backedIFacetedCatalogthat queriesidxJSONB directly for faceted counting. Dispatches byIndexType(FIELD, KEYWORD, BOOLEAN, UUID, DATE) withIPGIndexTranslatorfallback. Falls back to the default BTree-based implementation when the catalog is notIPGCatalogTool. Conditionally loaded only wheneea.facetednavigationis installed.Deprecated proxy methods:
search()proxies tosearchResults()anduniqueValuesFor()proxies toIndexes[name].uniqueValues(), both emittingDeprecationWarning.Blocked methods:
getAllBrains,searchAll,getobject,getMetadataForUID,getMetadataForRID,getIndexDataForUID,index_objectsraiseNotImplementedErrorwith descriptive messages.AccessControl security declarations: Comprehensive Zope security matching ZCatalog’s permission model.
Search ZCatalogon read methods (searchResults,__call__,getpath,getrid, etc.),Manage ZCatalog Entrieson write methods (catalog_object,uncatalog_object,refreshCatalog, etc.),Manage ZCatalogIndex Entrieson index management (addIndex,delIndex,addColumn,delColumn,getIndexObjects).setPermissionDefaultassigns default roles (Anonymousfor search,Managerfor management). Private helpers (indexObject,reindexObject, etc.) declared private.DateRangeInRangeIndex support: Native
IPGIndexTranslatorforProducts.DateRangeInRangeIndexoverlap queries. Translatescatalog({'my_idx': {'start': dt1, 'end': dt2}})into a single SQL overlap clause (obj_start <= q_end AND obj_end >= q_start). Supports recurring events: when the underlying start index is a DateRecurringIndex with RRULE, usesrrule."between"()with duration offset for occurrence-level overlap detection. Auto-discovered at startup — no configuration needed. Allows dropping theProducts.DateRangeInRangeIndexaddon while keeping the same query API.
Fixed¶
Addon index preservation: Installing plone.pgcatalog on a site with addon-provided catalog indexes (e.g. from
collective.taxonomy,plone.app.multilingual, etc.) no longer silently drops those index definitions. The install step now snapshots all existing index definitions and metadata columns before replacingportal_catalog, then restores addon indexes after re-applying core Plone profiles. Removedtoolset.xmlin favour of a setuphandler-controlled replacement for correct timing.
1.0.0b9¶
Changed¶
ZMI polish: All ZMI tabs now use Bootstrap 4 cards/tables matching Zope 5’s modern look (was old-style
<table>layout withsection-bar).Catalog tab (
manage_catalogView): Replaced inherited ZCatalog BTree-based view with PG-backed version. Shows catalog summary (object count, index/metadata count, search backend with BM25/Tsvector status), path filter, and server-side paginated object table (20/page) with Previous/Next navigation. Object detail shows full idx JSONB and searchable text preview.Advanced tab (
manage_catalogAdvanced): Simplified to only show Update Catalog and Clear and Rebuild actions. Removed ZCatalog-specific features (subtransactions, progress logging, standalone Clear Catalog) that don’t apply to PostgreSQL.Indexes & Metadata tab (
manage_catalogIndexesAndMetadata): Merged the separate Indexes and Metadata tabs into one read-only view showing all registered indexes (name, type, PG storage location, source attrs) and metadata columns. Reflects the IndexRegistry rather than BTree counts (which were always 0).Removed tabs: Query Report, Query Plan (BTree timing), and the separate Indexes / Metadata tabs are hidden — replaced by PG-aware equivalents.
Lexicon cleanup:
setuphandlers.install()now removes orphaned ZCTextIndex lexicons (htmltext_lexicon,plaintext_lexicon,plone_lexicon) created by Plone’scatalog.xml— unused with PG-backed text search.
1.0.0b8¶
Changed¶
Module split:
config.pyhas been split into four focused modules:pending.py(thread-local pending store + savepoint support),pool.py(connection pool discovery + request-scoped connections),processor.py(CatalogStateProcessor),startup.py(IDatabaseOpenedWithRootsubscriber + registry sync).config.pyis now a deprecation stub.Shared
ensure_date_param(): Deduplicated date coercion utility fromquery.pyanddri.pyintocolumns.ensure_date_param().__all__exports: Added explicit__all__topending.py,pool.py,processor.py,startup.py,columns.py,backends.py,interfaces.py.Top-level imports: Removed unnecessary deferred imports across
catalog.py,processor.py,startup.py.
Added¶
verifyClass/verifyObjecttests forIPGIndexTranslatorimplementations.Shared
query_zoids()test helper inconftest.py.
Security¶
Security review fixes (addresses #11):
CAT-C1: Replace f-string DDL in
BM25Backend.install_schema()withpsycopg.sql.SQL/Identifier/Literalcomposition. Validate language codes againstLANG_TOKENIZER_MAPallowlist +validate_identifier()on all generated column/index/tokenizer names.CAT-H1: Clamp
sort_limit/b_sizeto_MAX_LIMIT(10,000) andb_startto_MAX_OFFSET(1,000,000) to prevent resource exhaustion.CAT-H2: Validate RRULE strings in
DateRecurringIndexTranslator.extract()against RFC 5545 pattern and_MAX_RRULE_LENGTH(1,000) before storing.CAT-H3: Truncate full-text search queries to
_MAX_SEARCH_LENGTH(1,000) to prevent excessive tsvector parsing.CAT-M1: Replace f-string SQL in
clear_catalog_data()withpsycopg.sql.Identifierfor extra column names.CAT-M2: Add
conn.closedguard inrelease_request_connection()to handle already-closed connections; document pool leak recovery in docstring.CAT-M3: Add defensive
validate_identifier(index_name)inDateRecurringIndexTranslator.query().CAT-L1: Simplify error messages to not expose internal limit values.
CAT-L2: Add rate limiting guidance note in
searchResults()docstring.CAT-L3: Normalize double slashes in
_validate_path().
1.0.0b7¶
Fixed¶
sort_onnow accepts a list of index names for multi-column sorting, matching ZCatalog’s API.sort_ordercan also be a list (one direction per sort key) or a single string applied to all keys.PGCatalogBrain.__getattr__now distinguishes known catalog fields from unknown attributes. Known indexes and metadata columns returnNonewhen absent from idx (matching ZCatalog’s Missing Value behavior), while unknown attributes raiseAttributeError. This enablesCatalogContentListingObject.__getattr__to fall back togetObject()for non-catalog attributes (e.g.content_type), and fixes PAM’sget_alternate_languages()viewlet crash onbrain.Language.reindexIndexnow acceptspghandlerkeyword argument for compatibility with ZCatalog’smanage_reindexIndexand plone.distribution. The argument is accepted but ignored (PG-based reindexing doesn’t need progress reporting). [#9]clearFindAndRebuildnow properly rebuilds the catalog by traversing all content objects after clearing PG data. Previously only cleared without rebuilding.refreshCatalognow properly re-catalogs objects by resolving them from ZODB and re-extracting index values. Added missingpghandlerparameter for ZCatalog API compatibility.Fixed
ConnectionStateErroron Zope restart when a Plone site already exists in the database._sync_registry_from_dband_detect_languages_from_dbnow abort the transaction before closing their temporary ZODB connections._ensure_catalog_indexesnow checks for essential Plone indexes (UID, portal_type) instead of any indexes, preventing addon indexes from blocking re-application of Plone defaults.ZCatalog internal API compatibility:
getpath(rid),getrid(path),Indexes["UID"]._index.get(uuid), anduniqueValues(withLengths=True)now work with PG-backed data. Uses ZOID as the record ID. This fixesplone.api.content.get(UID=...),plone.app.vocabulariescontent validation, and dexterity type counting in the control panel.
1.0.0b6¶
Added¶
Relevance-ranked search results: SearchableText queries now automatically return results ordered by relevance when no explicit
sort_onis specified. Title matches rank highest (weight A), followed by Description (weight B), then body text (weight D). Uses PostgreSQL’s built-ints_rank_cd()with cover density ranking. No extensions required. Note: Requires a full catalog reindex after upgrade.Optional BM25 ranking via VectorChord-BM25 extension. When
vchord_bm25andpg_tokenizerextensions are detected at startup, search results are automatically ranked using BM25 (IDF, term saturation, length normalization) instead ofts_rank_cd. Title matches are boosted via combined text. Vanilla PostgreSQL installations continue using weighted tsvector ranking with no changes needed. Requires:vchord_bm25+pg_tokenizerPostgreSQL extensions. Note: Full catalog reindex required after enabling.Per-language BM25 columns: each configured language gets its own
bm25vectorcolumn with a language-specific tokenizer. Supports 30 Snowball stemmers (Arabic to Yiddish), jieba (Chinese), and lindera (Japanese/Korean). Configure viaPGCATALOG_BM25_LANGUAGESenvironment variable (comma-separated codes, orautoto detect from portal_languages). Fallback column for unconfigured languages ensures BM25 ranking benefits for all content. Note: Changing languages requires full catalog reindex.SearchBackendabstraction: thin interface for swappable search/ranking backends.TsvectorBackend(always available) andBM25Backend(optional). Backend auto-detected at Zope startup.LANG_TOKENIZER_MAPinbackends.pymaps ISO 639-1 codes to pg_tokenizer configurations. Regional variants (pt-br, zh-CN) are normalized to base codes automatically.Estonian (
et) added to language-to-regconfig mapping (supported by PG 17).Multilingual example:
create_site.pyzconsole script creates a Plone site withplone.app.multilingual(EN, DE, ZH), installs plone.pgcatalog, and imports ~800+ Wikipedia geography articles across all three languages with PAM translation linking.fetch_wikipedia.pyfetches articles from en/de/zh Wikipedia with cross-language links. Seeexample/README.md.
Fixed¶
reindexObjectSecuritynow works for newly created objects.unrestrictedSearchResultsextends PG results with objects from the thread-local pending store (not yet committed to PG) for path queries. Previously, newly created objects were invisible to the path search inCMFCatalogAware.reindexObjectSecurity, so their security indexes (e.g.allowedRolesAndUsers) were never updated during workflow transitions in the same transaction.CatalogSearchResultsnow implementsIFiniteSequence, enablingIContentListingadaptation in Plone’s search view.PGCatalogBrainnow providesgetId(property) andpretty_title_or_id()for compatibility with Plone’s Classic UI navigation and search templates.getIdis a property (not a method) sobrain.getIdreturns a string, matching standard ZCatalog brain behavior.PGCatalogBrain.__getattr__returnsNonefor missing idx keys instead of raisingAttributeError, matching ZCatalog’s Missing Value behavior. Fixes PAM’sget_alternate_languages()viewlet crash onbrain.Language.Unknown catalog indexes (e.g.
Language,TranslationGroupfrom plone.app.multilingual) now fall back to JSONB field queries instead of being silently skipped. This enables PAM’s translation registration and lookup queries to work correctly.CJK tokenizer TOML format fixed: jieba (Chinese) and lindera (Japanese/Korean) now use the correct table syntax for pg_tokenizer’s
pre_tokenizerconfiguration.
1.0.0b5¶
Added¶
Add partial idx JSONB updates for lightweight reindex. [#6]
When
reindexObject(idxs=[...])is called with specific index names (e.g. duringreindexObjectSecurity), extract only the requested values and register a JSONB merge patch (idx || patch) instead of full ZODB serialization + full idx column replacementAvoids
_p_changed = Trueand the associated pickle-JSON round-trip for every object in a subtreeUses the new
finalize(cursor)hook from zodb-pgjsonb to apply partial JSONB merges atomically in the same PG transaction
1.0.0b4¶
Added¶
Language-aware full-text search: SearchableText now uses per-object language for stemming. The
pgcatalog_lang_to_regconfig()PL/pgSQL function maps Plone language codes (ISO 639-1, 30 languages) to PostgreSQL text search configurations (e.g."de"→german). Falls back to'simple'for unmapped or missing languages. Non-multilingual sites are unaffected.Python mirror:
columns.language_to_regconfig()for testing/validation.Title/Description text search: Title and Description queries now use tsvector word-level matching instead of exact JSONB containment.
catalog(Title="Hello")now correctly matches"Hello World". Backed by GIN expression indexes with'simple'config (no stemming).Automatic addon ZCTextIndex support: Addon-registered ZCTextIndex fields are automatically discovered at startup. GIN expression indexes are created dynamically by
_ensure_text_indexes(), and queries use tsvector matching – zero addon code needed.
Fixed¶
Title/Description query broken: Previously, querying Title or Description as ZCTextIndex used JSONB exact containment (
idx @> '{"Title":"Hello"}'), which only matched exact values, not words within text. Now usesto_tsvector/plainto_tsqueryfor proper word-level matching.
1.0.0b3¶
Fixed¶
Snapshot consistency: Catalog read queries now route through the ZODB storage instance’s PG connection, sharing the same REPEATABLE READ snapshot as
load()calls. Previously, catalog queries used a separate autocommit connection that could see a different database state than ZODB object loads within the same request.New internal API:
pool.get_storage_connection(context)— retrieves the PG connection fromcontext._p_jar._storage.pg_connection.PlonePGCatalogTool._get_pg_read_connection()— prefers storage connection, falls back to pool for non-ZODB contexts (tests, scripts).
CatalogSearchResultsnow accepts aconnparameter (waspool) for lazy idx batch loading, using the same connection directly.
1.0.0b2¶
Security¶
SQL identifier validation: Added
validate_identifier()incolumns.pyto reject unsafe SQL identifiers. Allidx_keyvalues inIndexRegistryanddate_attrinDateRecurringIndexTranslatorare now validated.Access control declarations: Added
declareProtectedfor management methods (refreshCatalog,reindexIndex,clearFindAndRebuild) anddeclarePrivateforunrestrictedSearchResultsonPlonePGCatalogTool.API safety: Renamed
execute_query()to_execute_query()to mark as internal API. Capped path query list size to 100 (DoS prevention). Documented security contract forIPGIndexTranslatorimplementations.
Fixed¶
Savepoint-aware pending store: The thread-local pending catalog data now participates in ZODB’s transaction lifecycle via
ISavepointDataManager. Fixes two bugs: pending data not reverting on savepoint rollback, and stale pending data leaking across transactions after abort.
1.0.0b1 Initial release (2026-02-10)¶
Changed¶
ZCatalog BTree write elimination: Removed
super()delegation inindexObject(),reindexObject(),catalog_object(), anduncatalog_object(). All catalog data now flows exclusively to PostgreSQL viaCatalogStateProcessor— no BTree/Bucket objects are written to ZODB. Content creation dropped from 175 ms/doc to 68.5 ms/doc (2.5x faster), making PGCatalog 1.13x faster than RelStorage+ZCatalog for writes.
Added¶
Dynamic IndexRegistry: Replaced static
KNOWN_INDEXESdict with a dynamicIndexRegistrythat discovers indexes from ZCatalog at startup viasync_from_catalog(). Addons that add indexes viacatalog.xmlprofiles are now automatically supported without code changes.META_TYPE_MAPmaps ZCatalog meta_types (FieldIndex, KeywordIndex, DateIndex, etc.) toIndexTypeenum values.SPECIAL_INDEXES(SearchableText,effectiveRange,path) have dedicated PG columns and are excluded from idx JSONB extraction.Registry entries are 3-tuples:
(IndexType, idx_key, source_attrs), wheresource_attrssupportsindexed_attrdiffering from index name.Startup sync via
_sync_registry_from_db()populates the registry from each Plone site’sportal_catalogbefore the first request.
IPGIndexTranslator utility: Named utility interface for custom index types not covered by
META_TYPE_MAP. Wired intoquery.py(query + sort fallback) andcatalog.py(extraction fallback).DateRecurringIndex support: Built-in translator for
Products.DateRecurringIndex(Plone’sstart/endevent indexes). Stores base date + RFC 5545 RRULE string in idx JSONB; queries use rrule_plpgsql (pure PL/pgSQL, no C extensions) for recurrence expansion at query time. Translators are auto-discovered from ZCatalog at startup – no manual configuration needed. Container-friendly: works on standardpostgres:17images without additional extensions.DDL via
get_schema_sql():CatalogStateProcessornow provides DDL through theget_schema_sql()method, applied byPGJsonbStorageusing its own connection — no REPEATABLE READ lock conflicts during startup.Transactional catalog writes:
catalog_object()sets a_pgcatalog_pendingannotation on persistent objects. TheCatalogStateProcessorextracts this annotation during ZODB commit and writes catalog columns (path,parent_path,path_depth,idx,searchable_text) atomically alongside the object state.PlonePGCatalogTool: PostgreSQL-backed
portal_catalogreplacement for Plone, inheriting fromProducts.CMFPlone.CatalogTool. Registered via GenericSetuptoolset.xml.plone.restapi compatibility:
CatalogSearchResultsinheritsZTUtils.Lazy.Lazyfor serialization;PGCatalogBrainimplementsICatalogBrainforIContentListingObjectadaptation.