8
8
import decimal
9
9
import numpy
10
10
import pandas
11
+ import json
11
12
from .errors import DataJointError
12
13
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
+
13
34
14
35
class PromiscuousOperand :
15
36
"""
@@ -94,35 +115,56 @@ def make_condition(query_expression, condition, columns):
94
115
from .expression import QueryExpression , Aggregation , U
95
116
96
117
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 :
99
131
if not isinstance (v , uuid .UUID ):
100
132
try :
101
133
v = uuid .UUID (v )
102
134
except (AttributeError , ValueError ):
103
135
raise DataJointError (
104
136
"Badly formed UUID {v} in restriction by `{k}`" .format (k = k , v = v )
105
137
)
106
- return "X'%s'" % v .bytes .hex ()
138
+ return f" { k } =X' { v .bytes .hex ()} '"
107
139
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
+ ),
109
148
):
110
- return '"%s"' % v
149
+ return f' { k } =" { v } "'
111
150
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 )} )"
114
157
115
158
negate = False
116
159
while isinstance (condition , Not ):
117
160
negate = not negate
118
161
condition = condition .restriction
119
- template = "NOT (%s)" if negate else "%s"
120
162
121
163
# restrict by string
122
164
if isinstance (condition , str ):
123
165
columns .update (extract_column_names (condition ))
124
- return template % condition . strip (). replace (
125
- "%" , "%%"
166
+ return combine_conditions (
167
+ negate , conditions = [ condition . strip (). replace ( "%" , "%%" )]
126
168
) # escape %, see issue #376
127
169
128
170
# restrict by AndList
@@ -139,7 +181,7 @@ def prep_value(k, v):
139
181
return negate # if any item is False, the whole thing is False
140
182
if not items :
141
183
return not negate # and empty AndList is True
142
- return template % ( "(" + ") AND (" . join ( items ) + ")" )
184
+ return combine_conditions ( negate , conditions = items )
143
185
144
186
# restriction by dj.U evaluates to True
145
187
if isinstance (condition , U ):
@@ -151,23 +193,19 @@ def prep_value(k, v):
151
193
152
194
# restrict by a mapping/dict -- convert to an AndList of string equality conditions
153
195
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
+ )
155
199
if not common_attributes :
156
200
return not negate # no matching attributes -> evaluates to True
157
201
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
+ ],
171
209
)
172
210
173
211
# restrict by a numpy record -- convert to an AndList of string equality conditions
@@ -178,12 +216,9 @@ def prep_value(k, v):
178
216
if not common_attributes :
179
217
return not negate # no matching attributes -> evaluate to True
180
218
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 ],
187
222
)
188
223
189
224
# restrict by a QueryExpression subclass -- trigger instantiation and move on
@@ -231,7 +266,11 @@ def prep_value(k, v):
231
266
] # ignore False conditions
232
267
if any (item is True for item in or_list ): # if any item is True, entirely True
233
268
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
+ )
235
274
236
275
237
276
def extract_column_names (sql_expression ):
0 commit comments