Write a custom index translator¶
When you need this¶
Use IPGIndexTranslator when your ZCatalog index type is not in the standard META_TYPE_MAP:
Custom composite indexes
Third-party addon indexes
Non-standard query formats
Standard types (FieldIndex, KeywordIndex, DateIndex, BooleanIndex, DateRangeIndex, UUIDIndex, ZCTextIndex, ExtendedPathIndex, GopipIndex) are handled automatically.
The IPGIndexTranslator interface¶
Three methods:
def extract(obj, index_name) -> dict:
"""Return key-value pairs to merge into idx JSONB."""
def query(index_name, raw, spec) -> tuple[str, dict]:
"""Return (sql_fragment, params_dict) for WHERE clause."""
def sort(index_name) -> str | None:
"""Return SQL expression for ORDER BY, or None."""
Example: A priority score index¶
Suppose you have a custom PriorityIndex that stores a numeric score:
from plone.pgcatalog.columns import validate_identifier
from plone.pgcatalog.interfaces import IPGIndexTranslator
from zope.interface import implementer
@implementer(IPGIndexTranslator)
class PriorityIndexTranslator:
def extract(self, obj, index_name):
score = getattr(obj, "priority_score", None)
if score is not None:
return {index_name: int(score)}
return {}
def query(self, index_name, raw, spec):
validate_identifier(index_name)
query_val = spec.get("query")
range_spec = spec.get("range")
if query_val is None:
return ("TRUE", {})
p = f"priority_{index_name}"
if range_spec == "min":
sql = f"(idx->>'{index_name}')::int >= %({p})s"
return (sql, {p: int(query_val)})
elif range_spec == "max":
sql = f"(idx->>'{index_name}')::int <= %({p})s"
return (sql, {p: int(query_val)})
else:
sql = f"(idx->>'{index_name}')::int = %({p})s"
return (sql, {p: int(query_val)})
def sort(self, index_name):
return f"(idx->>'{index_name}')::int"
Register via ZCML¶
<utility
provides="plone.pgcatalog.interfaces.IPGIndexTranslator"
factory=".translators.PriorityIndexTranslator"
name="priority_score"
/>
The utility name must match the ZCatalog index name.
Register programmatically (for autodiscovery)¶
from zope.component import provideUtility
from plone.pgcatalog.interfaces import IPGIndexTranslator
translator = PriorityIndexTranslator()
provideUtility(translator, IPGIndexTranslator, name="priority_score")
Security requirements¶
Always use
%(name)sparameter placeholders for user-supplied values.Never interpolate query values into SQL strings.
Always call
validate_identifier()onindex_nameinquery()andsort().Index column references (like
idx->>'{name}') are safe becauseindex_namecomes from the component architecture (validated at registration time) – but the belt-and-suspendersvalidate_identifier()check prevents any edge cases.
Testing¶
Unit test the translator without PostgreSQL:
def test_query_exact():
t = PriorityIndexTranslator()
sql, params = t.query("priority_score", {"query": 5}, {"query": 5})
assert "priority_score" in sql
assert params["priority_priority_score"] == 5
Integration test with PostgreSQL (see tests/test_dri.py for patterns).
Built-in examples¶
Study these for more complex patterns:
dri.py(DateRecurringIndexTranslator) – RRULE expansion, multiple query strategies (min, max, min:max, exact), rrule_plpgsql functions for recurrence at query time.addons_compat/driri.py(DateRangeInRangeIndexTranslator) – proxy over two indexes, overlap queries, no-opextract()(underlying indexes handle storage).