@@ -86,6 +86,44 @@ def test_default_operator_is_or(self):
8686 qr = build_query ({"Subject" : {"query" : ["Python" , "Zope" ]}})
8787 assert "?|" in qr ["where" ]
8888
89+ def test_non_iterable_non_str_coerced_to_single_value (self ):
90+ """Regression for #152: a caller accidentally passing a non-string,
91+ non-iterable value (e.g. a Zope ``DateTime``, a number, etc.) must
92+ not crash the query builder with ``TypeError: object is not
93+ iterable``. Observed on aaf-6 prod where an addon passed a
94+ ``DateTime`` through a keyword criterion by mistake.
95+ """
96+
97+ class _NotIterable :
98+ """Mimics Zope ``DateTime`` — has ``__str__`` but no
99+ ``__iter__`` and isn't a ``str``."""
100+
101+ def __str__ (self ):
102+ return "2026-04-20"
103+
104+ qr = build_query ({"Subject" : _NotIterable ()})
105+ # Falls into the single-value branch — containment (`@>`).
106+ assert "idx @>" in qr ["where" ]
107+ # Value was coerced to its str form.
108+ param = _find_json_param (qr ["params" ])
109+ assert param .obj == {"Subject" : ["2026-04-20" ]}
110+
111+ def test_zope_datetime_as_keyword_does_not_crash (self ):
112+ """The specific shape that surfaced in production: a Zope
113+ ``DateTime`` passed as a keyword-index query value. It should
114+ be coerced via ``str()`` into a single-element list, not crash.
115+ """
116+ from datetime import datetime
117+ from datetime import UTC
118+
119+ # Python ``datetime`` is also not iterable — exactly the same
120+ # bug class.
121+ dt = datetime (2026 , 4 , 20 , tzinfo = UTC )
122+ qr = build_query ({"Subject" : dt })
123+ assert "idx @>" in qr ["where" ]
124+ param = _find_json_param (qr ["params" ])
125+ assert len (param .obj ["Subject" ]) == 1
126+
89127
90128# ---------------------------------------------------------------------------
91129# BooleanIndex
0 commit comments