Skip to content

Commit 7a23a02

Browse files
authored
Merge pull request #195 from open-data/fix/ds-text-int-filter
Backport DataStore Text/Int Filter Fix
2 parents 5901280 + 4cc9be8 commit 7a23a02

File tree

4 files changed

+106
-20
lines changed

4 files changed

+106
-20
lines changed

changes/195.backport.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
You can now use non-string values in `datastore_search` and `datastore_delete` filters for text datatype fields.

ckanext/datastore/backend/postgres.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,18 @@ def _where_clauses(
595595
sa.column(field),
596596
','.join(f":{p}" for p in placeholders)
597597
))
598+
if fields_types[field] == 'text':
599+
# pSQL can do int_field = "10"
600+
# but cannot do text_field = 10
601+
# this fixes parity there.
602+
value = (str(v) for v in value)
598603
clause = (clause_str, dict(zip(placeholders, value)))
599604
else:
605+
if fields_types[field] == 'text':
606+
# pSQL can do int_field = "10"
607+
# but cannot do text_field = 10
608+
# this fixes parity there.
609+
value = str(value)
600610
placeholder = f"value_{next(idx_gen)}"
601611
clause: tuple[Any, ...] = (
602612
f'{sa.column(field)} = :{placeholder}',
@@ -1942,7 +1952,10 @@ def delete_data(context: Context, data_dict: dict[str, Any]):
19421952
where_clause
19431953
)
19441954

1945-
_execute_single_statement(context, sql_string, where_values)
1955+
try:
1956+
_execute_single_statement(context, sql_string, where_values)
1957+
except ProgrammingError as pe:
1958+
raise ValidationError({'filters': [_programming_error_summary(pe)]})
19461959

19471960

19481961
def _create_triggers(connection: Any, resource_id: str,

ckanext/datastore/tests/test_delete.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,41 @@ def test_delete_records_required_filters(self):
205205
err = ve.value.error_dict
206206
assert err == expected
207207

208+
@pytest.mark.ckan_config("ckan.plugins", "datastore")
209+
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
210+
def test_delete_records_text_int_filter(self):
211+
resource = factories.Resource()
212+
data = {
213+
"resource_id": resource["id"],
214+
"force": True,
215+
"fields": [
216+
{"id": "text_field", "type": "text"},
217+
],
218+
"records": [
219+
{"text_field": 25},
220+
{"text_field": 37},
221+
],
222+
}
223+
helpers.call_action("datastore_create", **data)
224+
225+
# can delete by int
226+
data = {"resource_id": resource["id"], "force": True,
227+
"filters": {"text_field": 25}}
228+
helpers.call_action("datastore_records_delete", **data)
229+
result = helpers.call_action("datastore_search",
230+
resource_id=resource["id"],
231+
include_total=True)
232+
assert result["total"] == 1
233+
234+
# can delete by text
235+
data = {"resource_id": resource["id"], "force": True,
236+
"filters": {"text_field": "37"}}
237+
helpers.call_action("datastore_records_delete", **data)
238+
result = helpers.call_action("datastore_search",
239+
resource_id=resource["id"],
240+
include_total=True)
241+
assert result["total"] == 0
242+
208243

209244
@pytest.mark.usefixtures("with_request_context")
210245
class TestDatastoreDeleteLegacy(object):

ckanext/datastore/tests/test_search.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,62 @@ def test_search_filter_with_percent_in_column_name(self):
453453
result = helpers.call_action("datastore_search", **search_data)
454454
assert result["total"] == 1
455455

456+
@pytest.mark.ckan_config("ckan.plugins", "datastore")
457+
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
458+
def test_search_sort_nulls_first_last(self):
459+
resource = factories.Resource()
460+
data = {
461+
"resource_id": resource["id"],
462+
"force": True,
463+
"records": [{"a": 1, "b": "Y"}, {"b": "Z"}],
464+
}
465+
helpers.call_action("datastore_create", **data)
466+
467+
search_data = {
468+
"resource_id": data["resource_id"],
469+
"sort": [u"a desc nulls last"],
470+
}
471+
result = helpers.call_action("datastore_search", **search_data)
472+
assert result["records"][0]['b'] == 'Y'
473+
474+
search_data = {
475+
"resource_id": data["resource_id"],
476+
"sort": [u"a desc nulls first"],
477+
}
478+
result = helpers.call_action("datastore_search", **search_data)
479+
assert result["records"][0]['b'] == 'Z'
480+
481+
@pytest.mark.ckan_config("ckan.plugins", "datastore")
482+
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
483+
def test_search_records_text_int_filter(self):
484+
resource = factories.Resource()
485+
data = {
486+
"resource_id": resource["id"],
487+
"force": True,
488+
"fields": [
489+
{"id": "text_field", "type": "text"},
490+
],
491+
"records": [
492+
{"text_field": 25},
493+
{"text_field": 37},
494+
],
495+
}
496+
helpers.call_action("datastore_create", **data)
497+
498+
# can search by int
499+
data = {"resource_id": resource["id"],
500+
"include_total": True,
501+
"filters": {"text_field": 25}}
502+
result = helpers.call_action("datastore_search", **data)
503+
assert len(result["records"]) == 1
504+
505+
# can search by text
506+
data = {"resource_id": resource["id"],
507+
"include_total": True,
508+
"filters": {"text_field": "37"}}
509+
result = helpers.call_action("datastore_search", **data)
510+
assert len(result["records"]) == 1
511+
456512

457513
@pytest.mark.usefixtures("with_request_context")
458514
class TestDatastoreSearchLegacyTests(object):
@@ -770,25 +826,6 @@ def test_search_filters_get(self, app):
770826
assert result["total"] == 1
771827
assert result["records"] == [self.expected_records[0]]
772828

773-
@pytest.mark.ckan_config("ckan.plugins", "datastore")
774-
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
775-
def test_search_invalid_filter(self, app):
776-
data = {
777-
"resource_id": self.data["resource_id"],
778-
# invalid because author is not a numeric field
779-
"filters": {u"author": 42},
780-
}
781-
782-
auth = {"Authorization": self.sysadmin_token}
783-
res = app.post(
784-
"/api/action/datastore_search",
785-
json=data,
786-
extra_environ=auth,
787-
status=409,
788-
)
789-
res_dict = json.loads(res.data)
790-
assert res_dict["success"] is False
791-
792829
@pytest.mark.ckan_config("ckan.plugins", "datastore")
793830
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
794831
def test_search_sort(self, app):

0 commit comments

Comments
 (0)