Repository: Kinto/kinto

Issue:
Regression/Bug when filtering with integer values in PostgreSQL storage
The bug was probably introduced in #761 

Reproduce steps:
- Run Kinto with PostgreSQL storage
- Create a record `{"age": 12}`
- Create a record `{"name": "captain hook"}`
- Fetch the list with `records?min_age=10`

```
BackendError: DataError: (psycopg2.DataError) invalid input syntax for type numeric: ""
 [SQL: "
         WITH total_filtered AS (
             SELECT COUNT(id) AS count
               FROM records
              WHERE parent_id = %(parent_id)s
                AND collection_id = %(collection_id)s
                AND coalesce(data->>%(filters_field_0_0)s, '')::numeric >= %(filters_value_0)s
         ),
         collection_filtered AS (
             SELECT id, last_modified, data
               FROM records
              WHERE parent_id = %(parent_id)s
                AND collection_id = %(collection_id)s
                AND coalesce(data->>%(filters_field_0_0)s, '')::numeric >= %(filters_value_0)s
              LIMIT 10000
         ),
         fake_deleted AS (
             SELECT (%(deleted_field)s)::JSONB AS data
         ),
         filtered_deleted AS (
             SELECT id, last_modified, fake_deleted.data AS data
               FROM deleted, fake_deleted
              WHERE parent_id = %(parent_id)s
                AND collection_id = %(collection_id)s
                AND coalesce(data->>%(filters_field_0_0)s, '')::numeric >= %(filters_value_0)s
                LIMIT 0
         ),
         all_records AS (
             SELECT * FROM filtered_deleted
              UNION ALL
             SELECT * FROM collection_filtered
         ),
         paginated_records AS (
             SELECT DISTINCT id
               FROM all_records

         )
         SELECT total_filtered.count AS count_total,
                a.id, as_epoch(a.last_modified) AS last_modified, a.data
           FROM paginated_records AS p JOIN all_records AS a ON (a.id = p.id),
                total_filtered
           ORDER BY last_modified DESC
           ;
         "] [parameters: {'filters_field_0_0': u'readposition', 'collection_id': 'record', 'deleted_field': '{"deleted":true}', 'filters_value_0': '100', 'parent_id': u'/buckets/1301df10-6ba4-a783-7ca4-f66851a1ebf5/collections/tata'}] lang=None uid=05fb445063da0e1a188d1e8f03009ae08f461d24ce08c6880b2ef569bdb6a2ed
```

What happens is that the `coalesce(data->>%(filters_field_0_0)s, '')::numeric` is resolved to `''::numeric` which fails.



Discussion:

Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -935,6 +935,7 @@ def _extract_filters(self, queryparams=None):
                 raise_invalid(self.request, **error_details)
 
             value = native_value(paramvalue)
+
             if operator in (COMPARISON.IN, COMPARISON.EXCLUDE):
                 value = set([native_value(v) for v in paramvalue.split(',')])
 
diff --git a/kinto/core/storage/postgresql/__init__.py b/kinto/core/storage/postgresql/__init__.py
--- a/kinto/core/storage/postgresql/__init__.py
+++ b/kinto/core/storage/postgresql/__init__.py
@@ -626,7 +626,7 @@ def _format_conditions(self, filters, id_field, modified_field,
             elif filtr.field == modified_field:
                 sql_field = 'as_epoch(last_modified)'
             else:
-                sql_field = "data"
+                column_name = "data"
                 # Subfields: ``person.name`` becomes ``data->person->>name``
                 subfields = filtr.field.split('.')
                 for j, subfield in enumerate(subfields):
@@ -634,15 +634,15 @@ def _format_conditions(self, filters, id_field, modified_field,
                     field_holder = '%s_field_%s_%s' % (prefix, i, j)
                     holders[field_holder] = subfield
                     # Use ->> to convert the last level to text.
-                    sql_field += "->>" if j == len(subfields) - 1 else "->"
-                    sql_field += ":%s" % field_holder
+                    column_name += "->>" if j == len(subfields) - 1 else "->"
+                    column_name += ":%s" % field_holder
 
                 # If field is missing, we default to ''.
-                sql_field = "coalesce(%s, '')" % sql_field
+                sql_field = "coalesce(%s, '')" % column_name
                 # Cast when comparing to number (eg. '4' < '12')
                 if isinstance(value, (int, float)) and \
                    value not in (True, False):
-                    sql_field += "::numeric"
+                    sql_field = "(%s)::numeric" % column_name
 
             if filtr.operator not in (COMPARISON.IN, COMPARISON.EXCLUDE):
                 # For the IN operator, let psycopg escape the values list.


Test patch:
diff --git a/tests/core/test_storage.py b/tests/core/test_storage.py
--- a/tests/core/test_storage.py
+++ b/tests/core/test_storage.py
@@ -392,10 +392,13 @@ def test_get_all_can_filter_with_list_of_values(self):
         self.assertEqual(len(records), 2)
 
     def test_get_all_can_filter_with_numeric_values(self):
+        self.create_record({'missing': 'code'})
         for l in [1, 10, 6, 46]:
             self.create_record({'code': l})
+
         sorting = [Sort('code', 1)]
-        filters = [Filter('code', 10, utils.COMPARISON.MAX)]
+        filters = [Filter('code', 0, utils.COMPARISON.MIN),
+                   Filter('code', 10, utils.COMPARISON.MAX)]
         records, _ = self.storage.get_all(sorting=sorting, filters=filters,
                                           **self.storage_kw)
         self.assertEqual(records[0]['code'], 1)