@@ -882,6 +882,94 @@ def test_dynamic_field_multi_value(self, populated_registry):
882882 assert "= ANY(" in qr ["where" ]
883883
884884
885+ class TestFieldRangeNumeric :
886+ """Regression for #150 — FieldIndex range on numeric fields.
887+
888+ Two stacked bugs the old ``_field_range`` had:
889+
890+ 1. No min/max normalization: caller-supplied ``[max, min]`` produced
891+ always-false SQL. ``plone.app.querystring`` / ``collective.collectionfilter``
892+ pass values in URL order and do not sort.
893+ 2. Lexicographic string comparison on ``idx->>'key'`` — wrong for
894+ numeric fields (``'46.1' <= '5.0' <= '49.0'`` returns true).
895+
896+ Both shipped together — the map-widget bbox filter on aaf-6 silently
897+ returned zero rows.
898+ """
899+
900+ def _register (self , name ):
901+ from plone .pgcatalog .columns import get_registry
902+ from plone .pgcatalog .columns import IndexType
903+
904+ get_registry ().register (name , IndexType .FIELD , name )
905+
906+ def test_numeric_field_range_casts_to_numeric (self , populated_registry ):
907+ self ._register ("latitude" )
908+ qr = build_query ({"latitude" : {"query" : [46.1 , 49.0 ], "range" : "minmax" }})
909+ assert "(idx->>'latitude')::numeric" in qr ["where" ]
910+ # min param gets the lower value, max param the upper — regardless
911+ # of the caller-supplied order.
912+ params = qr ["params" ]
913+ min_key = next (k for k in params if k .endswith ("_min_1" ))
914+ max_key = next (k for k in params if k .endswith ("_max_2" ))
915+ assert params [min_key ] == 46.1
916+ assert params [max_key ] == 49.0
917+
918+ def test_numeric_field_range_normalizes_reversed_order (self , populated_registry ):
919+ self ._register ("latitude" )
920+ # Caller sends [max, min] — collective.collectionfilter's map
921+ # widget does this on every pan/zoom.
922+ qr = build_query ({"latitude" : {"query" : [49.0 , 46.1 ], "range" : "minmax" }})
923+ params = qr ["params" ]
924+ min_key = next (k for k in params if k .endswith ("_min_1" ))
925+ max_key = next (k for k in params if k .endswith ("_max_2" ))
926+ assert params [min_key ] == 46.1
927+ assert params [max_key ] == 49.0
928+
929+ def test_numeric_field_range_int (self , populated_registry ):
930+ self ._register ("priority" )
931+ qr = build_query ({"priority" : {"query" : [5 , 1 ], "range" : "minmax" }})
932+ assert "(idx->>'priority')::numeric" in qr ["where" ]
933+ params = qr ["params" ]
934+ min_key = next (k for k in params if k .endswith ("_min_1" ))
935+ max_key = next (k for k in params if k .endswith ("_max_2" ))
936+ assert params [min_key ] == 1
937+ assert params [max_key ] == 5
938+
939+ def test_numeric_field_range_min_only (self , populated_registry ):
940+ self ._register ("latitude" )
941+ qr = build_query ({"latitude" : {"query" : 46.1 , "range" : "min" }})
942+ assert "(idx->>'latitude')::numeric >=" in qr ["where" ]
943+
944+ def test_numeric_field_range_max_only (self , populated_registry ):
945+ self ._register ("latitude" )
946+ qr = build_query ({"latitude" : {"query" : 49.0 , "range" : "max" }})
947+ assert "(idx->>'latitude')::numeric <=" in qr ["where" ]
948+
949+ def test_string_field_range_keeps_text_comparison (self , populated_registry ):
950+ """Non-numeric values continue to use plain ``idx->>'key'``
951+ comparison (correct for ISO-format dates and similar
952+ lexicographically-orderable strings).
953+ """
954+ self ._register ("getId" )
955+ qr = build_query ({"getId" : {"query" : ["a" , "m" ], "range" : "minmax" }})
956+ assert "::numeric" not in qr ["where" ]
957+ assert "idx->>'getId' >=" in qr ["where" ]
958+ assert "idx->>'getId' <=" in qr ["where" ]
959+
960+ def test_string_field_range_normalizes_order (self , populated_registry ):
961+ """Normalization applies regardless of type — ``_field_range`` sorts
962+ caller-supplied values even for strings.
963+ """
964+ self ._register ("getId" )
965+ qr = build_query ({"getId" : {"query" : ["m" , "a" ], "range" : "minmax" }})
966+ params = qr ["params" ]
967+ min_key = next (k for k in params if k .endswith ("_min_1" ))
968+ max_key = next (k for k in params if k .endswith ("_max_2" ))
969+ assert params [min_key ] == "a"
970+ assert params [max_key ] == "m"
971+
972+
885973class TestDynamicKeywordIndex :
886974 """KeywordIndex dynamically registered via registry."""
887975
0 commit comments