Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lmfdb/number_fields/number_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,7 +1326,8 @@ def __init__(self):
label="Intermediate field",
knowl="nf.intermediate_fields",
example_span="2.2.5.1 or x^2-5 or a "
+ display_knowl("nf.nickname", "field nickname"),
+ display_knowl("nf.nickname", "field nickname")
+ ", or a comma-separated list, e.g. x^2-2,x^2-3",
example="x^2-5")
completion = TextBox(
name="completions",
Expand Down
60 changes: 60 additions & 0 deletions lmfdb/number_fields/test_numberfield.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from psycodict.utils import SearchParsingError

from lmfdb.tests import LmfdbTest
from lmfdb.utils.search_parsing import parse_subfield

class NumberFieldTest(LmfdbTest):
# All tests should pass
Expand Down Expand Up @@ -46,6 +49,63 @@ def test_search_multiple_fields(self):
self.check_args('/NumberField/?jump=Qsqrt5%2c+x%5E2-3&search=Go', '2.2.5.1')
self.check_args('/NumberField/?jump=Qsqrt5%2c+x%5E2-3&search=Go', '2.2.12.1')

def parse_subfield_query(self, inp):
"""The query produced by the "Intermediate field" search box."""
query = {}
parse_subfield({'subfield': inp}, query, field='subfield', qfield='subfields')
return query

def test_parse_subfield(self):
# A single intermediate field gives a scalar $contains, as it did before
# lists were allowed
self.assertEqual(self.parse_subfield_query('x^2-2'),
{'subfields': {'$contains': '-2.0.1'}})
# A comma-separated list gives a single $contains holding every entry, in
# the order given. On the text[] subfields column that compiles to the
# Postgres containment subfields @> ARRAY[...], i.e. the AND of the
# containment conditions -- not the first entry alone, and not an OR.
self.assertEqual(self.parse_subfield_query('x^2-2,x^2-3'),
{'subfields': {'$contains': ['-2.0.1', '-3.0.1']}})
self.assertEqual(self.parse_subfield_query('x^2-3,x^2-2'),
{'subfields': {'$contains': ['-3.0.1', '-2.0.1']}})
# Labels, polynomials and nicknames may be mixed freely
self.assertEqual(self.parse_subfield_query('2.2.8.1,Qsqrt3'),
{'subfields': {'$contains': ['-2.0.1', '-3.0.1']}})
# Whitespace is stripped by the search parser
self.assertEqual(self.parse_subfield_query('x^2-2, x^2-3'),
{'subfields': {'$contains': ['-2.0.1', '-3.0.1']}})

def test_parse_subfield_errors(self):
# An empty entry, from a leading, trailing or repeated comma, is an error:
# dropping it would silently weaken the search, and dropping the only
# entries of "," would drop the constraint altogether. An entry that is
# empty only after unsupported characters are removed ('é' below) is
# an error too, and a malformed entry is an error as before.
for bad in [',', 'x^2-2,', ',x^2-2', 'x^2-2,,x^2-3', 'x^2-2,é', 'x^2-2,notafield']:
query = {}
# The parser flashes the error before re-raising, so it needs a request
with self.app.test_request_context():
with self.assertRaises(SearchParsingError):
parse_subfield({'subfield': bad}, query, field='subfield', qfield='subfields')
self.assertEqual(query, {}, "%s did not leave the query untouched" % bad)

def test_search_subfield(self):
# An end to end check that a list search really is an AND. Among the
# quartic fields of discriminant at most 3000, both Q(sqrt2,sqrt3) =
# 4.4.2304.1 and Q(zeta_8) = 4.0.256.1 contain Q(sqrt2), but only the
# first also contains Q(sqrt3), so it alone answers the list search.
# (The degree and discriminant bounds keep the search fast; the query
# itself is checked without a database search in test_parse_subfield.)
self.check_args('/NumberField/?degree=4&discriminant=1-3000&subfield=x%5E2-2',
['4.4.2304.1', '4.0.256.1'])
self.check_args('/NumberField/?degree=4&discriminant=1-3000&subfield=x%5E2-2%2Cx%5E2-3',
'4.4.2304.1')
self.not_check_args('/NumberField/?degree=4&discriminant=1-3000&subfield=x%5E2-2%2Cx%5E2-3',
'4.0.256.1')
# A malformed or empty entry gives a clean error page, not a 500
self.check_args('/NumberField/?subfield=x%5E2-2%2Cnotafield', 'not a valid field nickname or label')
self.check_args('/NumberField/?subfield=x%5E2-2%2C', 'Entries in the comma-separated list must be nonempty')

def test_search_disc(self):
self.check_args('/NumberField/?discriminant=1988-2014', '401') # factor of one of the discriminants

Expand Down
23 changes: 20 additions & 3 deletions lmfdb/utils/search_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1615,9 +1615,26 @@ def notq():

@search_parser # see SearchParser.__call__ for actual arguments when calling
def parse_subfield(inp, query, qfield):
sf = input_to_subfield(inp)
if sf: # Might return none
query[qfield] = {"$contains": sf}
if "," in inp:
# A comma-separated list of subfields means AND of the containment
# conditions, i.e. fields containing every listed subfield (equivalently,
# their compositum). An empty entry (from a leading, trailing or
# repeated comma) is an error rather than something to drop, since
# dropping it would silently broaden the search.
empty_entry = "Entries in the comma-separated list must be nonempty."
parts = inp.split(",")
if not all(parts):
raise SearchParsingError(empty_entry)
sfs = [input_to_subfield(part) for part in parts]
# input_to_subfield returns None for an entry that is empty once
# unsupported characters have been removed.
if not all(sfs):
raise SearchParsingError(empty_entry)
query[qfield] = {"$contains": sfs}
else:
sf = input_to_subfield(inp)
if sf: # Might return none
query[qfield] = {"$contains": sf}

@search_parser # see SearchParser.__call__ for actual arguments when calling
def parse_nf_string(inp, query, qfield):
Expand Down
Loading