IPGIndexTranslator interface reference

This page documents the IPGIndexTranslator interface for extending plone.pgcatalog with custom index types, its methods, security contract, registration, wiring points, and built-in implementations.

Interface definition

Defined in plone.pgcatalog.interfaces:

class IPGIndexTranslator(Interface):
    """Named utility that translates a custom index's data for PG storage + querying."""

    def extract(obj, index_name):
        """Extract value(s) from obj for this index.
        Returns dict to merge into idx JSONB."""

    def query(index_name, query_value, query_options):
        """Translate ZCatalog query to SQL fragment + params.
        Returns (sql_fragment, params_dict)."""

    def sort(index_name):
        """Return SQL expression for ORDER BY, or None."""

Methods

extract(obj, index_name) -> dict

Called during indexing (from catalog.py’s _extract_from_translators()). The obj argument is the IIndexableObject-wrapped Plone content object. Returns a dict of key-value pairs to merge into the idx JSONB column. Return {} if this translator stores no data (for example, DateRangeInRangeIndex delegates to underlying indexes).

query(index_name, raw, spec) -> tuple[str, dict]

Called during query execution (from query.py’s _process_index()). raw is the original query value as passed to the catalog. spec is the normalized query dict (simple values are wrapped as {"query:" value}). Returns a tuple of (sql_fragment, params_dict) where the SQL fragment is inserted into the WHERE clause and params are bound via psycopg parameterized queries using %(name)s placeholders.

sort(index_name) -> str | None

Called during sort processing (from query.py’s _process_sort()). Returns a SQL expression for ORDER BY, or None if the index does not support sorting.

Security contract

All IPGIndexTranslator implementations must follow these rules:

  • All user-supplied values must use %(name)s parameter placeholders in the SQL fragment. Never string-format values into SQL.

  • Index/column identifiers in the SQL fragment should be hardcoded constants or validated via validate_identifier().

  • validate_identifier(name) (from plone.pgcatalog.columns) rejects any name not matching ^[a-zA-Z_][a-zA-Z0-9_]*$.

  • The query() return value is appended directly to the WHERE clause. SQL injection through the fragment would bypass all parameterization.

Registration

Via Python (used at startup for autodiscovered indexes)

from zope.component import provideUtility
from plone.pgcatalog.interfaces import IPGIndexTranslator

provideUtility(translator_instance, IPGIndexTranslator, name="my_index")

The utility name must match the ZCatalog index name.

Wiring points

Module

Function

When Called

catalog.py

_extract_from_translators()

During indexing – calls extract() for all registered translators

query.py

_process_index()

During query – falls back to translator query() when index not in registry

query.py

_process_sort()

During sort – falls back to translator sort() when index not in registry

The fallback path in _process_index(): if an index name is not found in the IndexRegistry, query.py calls queryUtility(IPGIndexTranslator, name=index_name). If a translator is found, its query() method is called. If no translator is found either, the index is treated as a simple JSONB field query.

Built-in implementations

DateRecurringIndexTranslator

Defined in plone.pgcatalog.dri. Handles Products.DateRecurringIndex instances.

Constructor attributes:

Attribute

Description

date_attr

Object attribute for the base date (equals the index name)

recurdef_attr

Object attribute for the RRULE string

until_attr

Object attribute for the until date (rarely used)

Storage:

Two keys in the idx JSONB column:

  • {index_name}: ISO 8601 base date string

  • {index_name}_recurrence: RFC 5545 RRULE string (if recurring)

RRULE strings are validated against the pattern ^(RRULE:)?FREQ=(YEARLY|MONTHLY|...) and capped at 1,000 characters.

Query strategies:

Range

Non-recurring

Recurring

min:max

date BETWEEN min AND max

EXISTS (rrule."between"(rrule, date, min, max))

min

date >= min

EXISTS (rrule."after"(rrule, date, min, 1))

max

date <= max

date <= max (base date check sufficient)

exact

date = value

EXISTS (rrule."between"(rrule, date, value, value))

Sort: pgcatalog_to_timestamptz(idx->>'{index_name}') (base date).

DateRangeInRangeIndexTranslator

Defined in plone.pgcatalog.addons_compat.driri. Handles Products.DateRangeInRangeIndex instances.

Constructor attributes:

Attribute

Description

startindex

Name of the underlying start date index

endindex

Name of the underlying end date index

Storage:

No-op. extract() returns {}. The underlying DateIndex or DateRecurringIndex translators handle extraction into idx JSONB.

Query:

The query dict uses start and end keys (not query/range):

catalog(event_dates={"start": DateTime("2025-03-01"), "end": DateTime("2025-03-31")})

Case

SQL Logic

Non-recurring

obj_start <= q_end AND obj_end >= q_start (overlap test)

Recurring

EXISTS (rrule."between"(rrule, base_start, q_start - duration, q_end)) where duration = base_end - base_start

Sort: pgcatalog_to_timestamptz(idx->>'{startindex}') (start date of the underlying start index).

Autodiscovery

Both translators are autodiscovered and registered at startup by _register_dri_translators() and _register_driri_translators() in startup.py. These functions iterate the ZCatalog indexes, identify DateRecurringIndex and DateRangeInRangeIndex instances by meta_type, and register the corresponding translator utility with the index name.