New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add multivalue genre tag support #2503
Changes from all commits
c1636ff
3c2c716
28a508d
1ce4c16
091e126
645565c
35c8fa1
6822c52
efe7e31
aa8a33a
55a9e3d
57d682d
f9de5ec
e41a56b
1ac4014
5504f45
1465181
63138e0
61411ac
53617dc
9748df2
2f8e854
05f3476
63d7780
31b08c5
818a60d
f0f3943
e5c831c
8228067
06f7c04
bbd8809
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,11 +66,12 @@ class Query(object): | |
def clause(self): | ||
"""Generate an SQLite expression implementing the query. | ||
|
||
Return (clause, subvals) where clause is a valid sqlite | ||
WHERE clause implementing the query and subvals is a list of | ||
items to be substituted for ?s in the clause. | ||
Return (clause, subvals, tables) where clause is a valid sqlite | ||
WHERE clause implementing the query, subvals is a list of | ||
items to be substituted for ?s in the clause and tables is a | ||
list of FROM table names to be added to the sql statement. | ||
""" | ||
return None, () | ||
return None, (), () | ||
|
||
def match(self, item): | ||
"""Check whether this query matches a given Item. Can be used to | ||
|
@@ -101,14 +102,14 @@ def __init__(self, field, pattern, fast=True): | |
self.fast = fast | ||
|
||
def col_clause(self): | ||
return None, () | ||
return None, (), () | ||
|
||
def clause(self): | ||
if self.fast: | ||
return self.col_clause() | ||
else: | ||
# Matching a flexattr. This is a slow query. | ||
return None, () | ||
return None, (), () | ||
|
||
@classmethod | ||
def value_match(cls, pattern, value): | ||
|
@@ -135,7 +136,7 @@ def __hash__(self): | |
class MatchQuery(FieldQuery): | ||
"""A query that looks for exact matches in an item field.""" | ||
def col_clause(self): | ||
return self.field + " = ?", [self.pattern] | ||
return self.field + " = ?", [self.pattern], () | ||
|
||
@classmethod | ||
def value_match(cls, pattern, value): | ||
|
@@ -148,7 +149,7 @@ def __init__(self, field, fast=True): | |
super(NoneQuery, self).__init__(field, None, fast) | ||
|
||
def col_clause(self): | ||
return self.field + " IS NULL", () | ||
return self.field + " IS NULL", (), () | ||
|
||
@classmethod | ||
def match(cls, item): | ||
|
@@ -165,6 +166,7 @@ class StringFieldQuery(FieldQuery): | |
"""A FieldQuery that converts values to strings before matching | ||
them. | ||
""" | ||
|
||
@classmethod | ||
def value_match(cls, pattern, value): | ||
"""Determine whether the value matches the pattern. The value | ||
|
@@ -190,7 +192,7 @@ def col_clause(self): | |
search = '%' + pattern + '%' | ||
clause = self.field + " like ? escape '\\'" | ||
subvals = [search] | ||
return clause, subvals | ||
return clause, subvals, () | ||
|
||
@classmethod | ||
def string_match(cls, pattern, value): | ||
|
@@ -227,6 +229,64 @@ def string_match(cls, pattern, value): | |
return pattern.search(cls._normalize(value)) is not None | ||
|
||
|
||
class JSonSubstringListQuery(SubstringQuery): | ||
"""A query that matches a regular expression in a specific item | ||
field which contains a json dump of a string array. | ||
""" | ||
_unique_id = 1 | ||
|
||
def __init__(self, field, pattern, fast, model_cls): | ||
super(JSonSubstringListQuery, self).__init__( | ||
field, pattern, fast and util.SQLITE_HAS_JSON_EXTENSION) | ||
self.model_cls = model_cls | ||
|
||
def get_unique_alias(self): | ||
alias = 'json_' + self.field + str(JSonSubstringListQuery._unique_id) | ||
JSonSubstringListQuery._unique_id += 1 | ||
return alias | ||
|
||
def col_clause(self): | ||
pattern = (self.pattern | ||
.replace('\\', '\\\\') | ||
.replace('%', '\\%') | ||
.replace('_', '\\_')) | ||
search = '%' + pattern + '%' | ||
alias = self.get_unique_alias() | ||
clause = alias + ".value like ? escape '\\'" | ||
subvals = [search] | ||
table = 'json_each({0}) as {1}'.format(self.field, alias) | ||
return clause, subvals, (table,) | ||
|
||
def negated_clause(self): | ||
clause, subvals, table = self.col_clause() | ||
_table = self.model_cls._table | ||
primary_key = '%s.id' % (_table) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sampsyo Do you think it's worth it to add a get_primary_key_field to Model so instead of hardcoding |
||
neg_clause = '{0} not in (select {1} from {2}, {3} where {4})'.format( | ||
primary_key, primary_key, _table, ', '.join(table), clause) | ||
return neg_clause, subvals, () | ||
|
||
@classmethod | ||
def value_match(cls, pattern, _value): | ||
return any([pattern.lower() in value.lower() for value in _value]) | ||
|
||
|
||
class JSonRegexpListQuery(RegexpQuery): | ||
"""A query that matches a regular expression in a specific item | ||
field which is a json value. | ||
|
||
Raises InvalidQueryError when the pattern is not a valid regular | ||
expression. | ||
""" | ||
|
||
@classmethod | ||
def value_match(cls, pattern, _value): | ||
"""Determine whether the value matches the pattern. The value | ||
may have any type. | ||
""" | ||
return any([pattern.search(cls._normalize(value)) is not None | ||
for value in _value]) | ||
|
||
|
||
class BooleanQuery(MatchQuery): | ||
"""Matches a boolean field. Pattern should either be a boolean or a | ||
string reflecting a boolean. | ||
|
@@ -259,7 +319,7 @@ def __init__(self, field, pattern): | |
self.pattern = bytes(self.pattern) | ||
|
||
def col_clause(self): | ||
return self.field + " = ?", [self.buf_pattern] | ||
return self.field + " = ?", [self.buf_pattern], () | ||
|
||
|
||
class NumericQuery(FieldQuery): | ||
|
@@ -320,17 +380,17 @@ def match(self, item): | |
|
||
def col_clause(self): | ||
if self.point is not None: | ||
return self.field + '=?', (self.point,) | ||
return self.field + '=?', (self.point,), () | ||
else: | ||
if self.rangemin is not None and self.rangemax is not None: | ||
return (u'{0} >= ? AND {0} <= ?'.format(self.field), | ||
(self.rangemin, self.rangemax)) | ||
(self.rangemin, self.rangemax), ()) | ||
elif self.rangemin is not None: | ||
return u'{0} >= ?'.format(self.field), (self.rangemin,) | ||
return u'{0} >= ?'.format(self.field), (self.rangemin,), () | ||
elif self.rangemax is not None: | ||
return u'{0} <= ?'.format(self.field), (self.rangemax,) | ||
return u'{0} <= ?'.format(self.field), (self.rangemax,), () | ||
else: | ||
return u'1', () | ||
return u'1', (), () | ||
|
||
|
||
class CollectionQuery(Query): | ||
|
@@ -360,15 +420,18 @@ def clause_with_joiner(self, joiner): | |
""" | ||
clause_parts = [] | ||
subvals = [] | ||
tables = [] | ||
for subq in self.subqueries: | ||
subq_clause, subq_subvals = subq.clause() | ||
subq_clause, subq_subvals, _tables = subq.clause() | ||
tables.extend(_tables) | ||
|
||
if not subq_clause: | ||
# Fall back to slow query. | ||
return None, () | ||
return None, (), () | ||
clause_parts.append('(' + subq_clause + ')') | ||
subvals += subq_subvals | ||
clause = (' ' + joiner + ' ').join(clause_parts) | ||
return clause, subvals | ||
return clause, subvals, tables | ||
|
||
def __repr__(self): | ||
return "{0.__class__.__name__}({0.subqueries!r})".format(self) | ||
|
@@ -452,18 +515,25 @@ def match(self, item): | |
class NotQuery(Query): | ||
"""A query that matches the negation of its `subquery`, as a shorcut for | ||
performing `not(subquery)` without using regular expressions. | ||
|
||
Sometimes, negating a subquery is not as simple as prefixing it with 'NOT' | ||
in the WHERE clause, so for those cases, query objects may implement a | ||
negated_clause function that returns a negated clause expression. | ||
""" | ||
def __init__(self, subquery): | ||
self.subquery = subquery | ||
|
||
def clause(self): | ||
clause, subvals = self.subquery.clause() | ||
if hasattr(self.subquery, 'negated_clause'): | ||
return self.subquery.negated_clause() | ||
|
||
clause, subvals, tables = self.subquery.clause() | ||
if clause: | ||
return 'not ({0})'.format(clause), subvals | ||
return 'not ({0})'.format(clause), subvals, tables | ||
else: | ||
# If there is no clause, there is nothing to negate. All the logic | ||
# is handled by match() for slow queries. | ||
return clause, subvals | ||
return clause, subvals, tables | ||
|
||
def match(self, item): | ||
return not self.subquery.match(item) | ||
|
@@ -482,7 +552,7 @@ def __hash__(self): | |
class TrueQuery(Query): | ||
"""A query that always matches.""" | ||
def clause(self): | ||
return '1', () | ||
return '1', (), () | ||
|
||
def match(self, item): | ||
return True | ||
|
@@ -491,7 +561,7 @@ def match(self, item): | |
class FalseQuery(Query): | ||
"""A query that never matches.""" | ||
def clause(self): | ||
return '0', () | ||
return '0', (), () | ||
|
||
def match(self, item): | ||
return False | ||
|
@@ -660,7 +730,7 @@ def col_clause(self): | |
else: | ||
# Match any date. | ||
clause = '1' | ||
return clause, subvals | ||
return clause, subvals, () | ||
|
||
|
||
class DurationQuery(NumericQuery): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sampsyo should I still allow Query objects to maybe return 2-tuples just in case any external plugin still uses the old API? It's easy to check for unpack errors but it's a bit ugly, so I preferred to ask your opinion.