88import decimal
99import numpy
1010import pandas
11+ import json
1112from .errors import DataJointError
1213
14+ JSON_PATTERN = re .compile (
15+ r"^(?P<attr>\w+)(\.(?P<path>[\w.*\[\]]+))?(:(?P<type>[\w(,\s)]+))?$"
16+ )
17+
18+
19+ def translate_attribute (key ):
20+ match = JSON_PATTERN .match (key )
21+ if match is None :
22+ return match , key
23+ match = match .groupdict ()
24+ if match ["path" ] is None :
25+ return match , match ["attr" ]
26+ else :
27+ return match , "json_value(`{}`, _utf8mb4'$.{}'{})" .format (
28+ * [
29+ ((f" returning { v } " if k == "type" else v ) if v else "" )
30+ for k , v in match .items ()
31+ ]
32+ )
33+
1334
1435class PromiscuousOperand :
1536 """
@@ -94,35 +115,56 @@ def make_condition(query_expression, condition, columns):
94115 from .expression import QueryExpression , Aggregation , U
95116
96117 def prep_value (k , v ):
97- """prepare value v for inclusion as a string in an SQL condition"""
98- if query_expression .heading [k ].uuid :
118+ """prepare SQL condition"""
119+ key_match , k = translate_attribute (k )
120+ if key_match ["path" ] is None :
121+ k = f"`{ k } `"
122+ if (
123+ query_expression .heading [key_match ["attr" ]].json
124+ and key_match ["path" ] is not None
125+ and isinstance (v , dict )
126+ ):
127+ return f"{ k } ='{ json .dumps (v )} '"
128+ if v is None :
129+ return f"{ k } IS NULL"
130+ if query_expression .heading [key_match ["attr" ]].uuid :
99131 if not isinstance (v , uuid .UUID ):
100132 try :
101133 v = uuid .UUID (v )
102134 except (AttributeError , ValueError ):
103135 raise DataJointError (
104136 "Badly formed UUID {v} in restriction by `{k}`" .format (k = k , v = v )
105137 )
106- return "X'%s'" % v .bytes .hex ()
138+ return f" { k } =X' { v .bytes .hex ()} '"
107139 if isinstance (
108- v , (datetime .date , datetime .datetime , datetime .time , decimal .Decimal )
140+ v ,
141+ (
142+ datetime .date ,
143+ datetime .datetime ,
144+ datetime .time ,
145+ decimal .Decimal ,
146+ list ,
147+ ),
109148 ):
110- return '"%s"' % v
149+ return f' { k } =" { v } "'
111150 if isinstance (v , str ):
112- return '"%s"' % v .replace ("%" , "%%" ).replace ("\\ " , "\\ \\ " )
113- return "%r" % v
151+ v = v .replace ("%" , "%%" ).replace ("\\ " , "\\ \\ " )
152+ return f'{ k } ="{ v } "'
153+ return f"{ k } ={ v } "
154+
155+ def combine_conditions (negate , conditions ):
156+ return f"{ 'NOT ' if negate else '' } ({ ')AND(' .join (conditions )} )"
114157
115158 negate = False
116159 while isinstance (condition , Not ):
117160 negate = not negate
118161 condition = condition .restriction
119- template = "NOT (%s)" if negate else "%s"
120162
121163 # restrict by string
122164 if isinstance (condition , str ):
123165 columns .update (extract_column_names (condition ))
124- return template % condition . strip (). replace (
125- "%" , "%%"
166+ return combine_conditions (
167+ negate , conditions = [ condition . strip (). replace ( "%" , "%%" )]
126168 ) # escape %, see issue #376
127169
128170 # restrict by AndList
@@ -139,7 +181,7 @@ def prep_value(k, v):
139181 return negate # if any item is False, the whole thing is False
140182 if not items :
141183 return not negate # and empty AndList is True
142- return template % ( "(" + ") AND (" . join ( items ) + ")" )
184+ return combine_conditions ( negate , conditions = items )
143185
144186 # restriction by dj.U evaluates to True
145187 if isinstance (condition , U ):
@@ -151,23 +193,19 @@ def prep_value(k, v):
151193
152194 # restrict by a mapping/dict -- convert to an AndList of string equality conditions
153195 if isinstance (condition , collections .abc .Mapping ):
154- common_attributes = set (condition ).intersection (query_expression .heading .names )
196+ common_attributes = set (c .split ("." , 1 )[0 ] for c in condition ).intersection (
197+ query_expression .heading .names
198+ )
155199 if not common_attributes :
156200 return not negate # no matching attributes -> evaluates to True
157201 columns .update (common_attributes )
158- return template % (
159- "("
160- + ") AND (" .join (
161- "`%s`%s"
162- % (
163- k ,
164- " IS NULL"
165- if condition [k ] is None
166- else f"={ prep_value (k , condition [k ])} " ,
167- )
168- for k in common_attributes
169- )
170- + ")"
202+ return combine_conditions (
203+ negate ,
204+ conditions = [
205+ prep_value (k , v )
206+ for k , v in condition .items ()
207+ if k .split ("." , 1 )[0 ] in common_attributes # handle json indexing
208+ ],
171209 )
172210
173211 # restrict by a numpy record -- convert to an AndList of string equality conditions
@@ -178,12 +216,9 @@ def prep_value(k, v):
178216 if not common_attributes :
179217 return not negate # no matching attributes -> evaluate to True
180218 columns .update (common_attributes )
181- return template % (
182- "("
183- + ") AND (" .join (
184- "`%s`=%s" % (k , prep_value (k , condition [k ])) for k in common_attributes
185- )
186- + ")"
219+ return combine_conditions (
220+ negate ,
221+ conditions = [prep_value (k , condition [k ]) for k in common_attributes ],
187222 )
188223
189224 # restrict by a QueryExpression subclass -- trigger instantiation and move on
@@ -231,7 +266,11 @@ def prep_value(k, v):
231266 ] # ignore False conditions
232267 if any (item is True for item in or_list ): # if any item is True, entirely True
233268 return not negate
234- return template % ("(%s)" % " OR " .join (or_list )) if or_list else negate
269+ return (
270+ f"{ 'NOT ' if negate else '' } ({ ' OR ' .join (or_list )} )"
271+ if or_list
272+ else negate
273+ )
235274
236275
237276def extract_column_names (sql_expression ):
0 commit comments