34
34
from google .cloud .bigquery .table import TableReference
35
35
from google .api_core .exceptions import NotFound
36
36
37
+ import sqlalchemy
37
38
import sqlalchemy .sql .sqltypes
38
39
import sqlalchemy .sql .type_api
39
40
from sqlalchemy .exc import NoSuchTableError
57
58
FIELD_ILLEGAL_CHARACTERS = re .compile (r"[^\w]+" )
58
59
59
60
61
+ def assert_ (cond , message = "Assertion failed" ): # pragma: NO COVER
62
+ if not cond :
63
+ raise AssertionError (message )
64
+
65
+
60
66
class BigQueryIdentifierPreparer (IdentifierPreparer ):
61
67
"""
62
68
Set containing everything
@@ -152,15 +158,25 @@ def get_insert_default(self, column): # pragma: NO COVER
152
158
elif isinstance (column .type , String ):
153
159
return str (uuid .uuid4 ())
154
160
155
- def pre_exec (
156
- self ,
157
- in_sub = re .compile (
158
- r" IN UNNEST\(\[ "
159
- r"(%\([^)]+_\d+\)s(?:, %\([^)]+_\d+\)s)*)?" # Placeholders. See below.
160
- r":([A-Z0-9]+)" # Type
161
- r" \]\)"
162
- ).sub ,
163
- ):
161
+ __remove_type_from_empty_in = _helpers .substitute_re_method (
162
+ r" IN UNNEST\(\[ ("
163
+ r"(?:NULL|\(NULL(?:, NULL)+\))\)"
164
+ r" (?:AND|OR) \(1 !?= 1"
165
+ r")"
166
+ r"(?:[:][A-Z0-9]+)?"
167
+ r" \]\)" ,
168
+ re .IGNORECASE ,
169
+ r" IN(\1)" ,
170
+ )
171
+
172
+ @_helpers .substitute_re_method (
173
+ r" IN UNNEST\(\[ "
174
+ r"(%\([^)]+_\d+\)s(?:, %\([^)]+_\d+\)s)*)?" # Placeholders. See below.
175
+ r":([A-Z0-9]+)" # Type
176
+ r" \]\)" ,
177
+ re .IGNORECASE ,
178
+ )
179
+ def __distribute_types_to_expanded_placeholders (self , m ):
164
180
# If we have an in parameter, it sometimes gets expaned to 0 or more
165
181
# parameters and we need to move the type marker to each
166
182
# parameter.
@@ -171,29 +187,29 @@ def pre_exec(
171
187
# suffixes refect that when an array parameter is expanded,
172
188
# numeric suffixes are added. For example, a placeholder like
173
189
# `%(foo)s` gets expaneded to `%(foo_0)s, `%(foo_1)s, ...`.
190
+ placeholders , type_ = m .groups ()
191
+ if placeholders :
192
+ placeholders = placeholders .replace (")" , f":{ type_ } )" )
193
+ else :
194
+ placeholders = ""
195
+ return f" IN UNNEST([ { placeholders } ])"
174
196
175
- def repl (m ):
176
- placeholders , type_ = m .groups ()
177
- if placeholders :
178
- placeholders = placeholders .replace (")" , f":{ type_ } )" )
179
- else :
180
- placeholders = ""
181
- return f" IN UNNEST([ { placeholders } ])"
182
-
183
- self .statement = in_sub (repl , self .statement )
197
+ def pre_exec (self ):
198
+ self .statement = self .__distribute_types_to_expanded_placeholders (
199
+ self .__remove_type_from_empty_in (self .statement )
200
+ )
184
201
185
202
186
203
class BigQueryCompiler (SQLCompiler ):
187
204
188
205
compound_keywords = SQLCompiler .compound_keywords .copy ()
189
- compound_keywords [selectable .CompoundSelect .UNION ] = "UNION ALL"
206
+ compound_keywords [selectable .CompoundSelect .UNION ] = "UNION DISTINCT"
207
+ compound_keywords [selectable .CompoundSelect .UNION_ALL ] = "UNION ALL"
190
208
191
- def __init__ (self , dialect , statement , column_keys = None , inline = False , ** kwargs ):
209
+ def __init__ (self , dialect , statement , * args , ** kwargs ):
192
210
if isinstance (statement , Column ):
193
211
kwargs ["compile_kwargs" ] = util .immutabledict ({"include_table" : False })
194
- super (BigQueryCompiler , self ).__init__ (
195
- dialect , statement , column_keys , inline , ** kwargs
196
- )
212
+ super (BigQueryCompiler , self ).__init__ (dialect , statement , * args , ** kwargs )
197
213
198
214
def visit_insert (self , insert_stmt , asfrom = False , ** kw ):
199
215
# The (internal) documentation for `inline` is confusing, but
@@ -260,24 +276,37 @@ def group_by_clause(self, select, **kw):
260
276
# no way to tell sqlalchemy that, so it works harder than
261
277
# necessary and makes us do the same.
262
278
263
- _in_expanding_bind = re . compile ( r" IN \((\[EXPANDING_\w+\](:[A-Z0-9]+)?)\)$" )
279
+ __sqlalchemy_version_info = tuple ( map ( int , sqlalchemy . __version__ . split ( "." )) )
264
280
265
- def _unnestify_in_expanding_bind (self , in_text ):
266
- return self ._in_expanding_bind .sub (r" IN UNNEST([ \1 ])" , in_text )
281
+ __expandng_text = (
282
+ "EXPANDING" if __sqlalchemy_version_info < (1 , 4 ) else "POSTCOMPILE"
283
+ )
284
+
285
+ __in_expanding_bind = _helpers .substitute_re_method (
286
+ fr" IN \((\[" fr"{ __expandng_text } " fr"_[^\]]+\](:[A-Z0-9]+)?)\)$" ,
287
+ re .IGNORECASE ,
288
+ r" IN UNNEST([ \1 ])" ,
289
+ )
267
290
268
291
def visit_in_op_binary (self , binary , operator_ , ** kw ):
269
- return self ._unnestify_in_expanding_bind (
292
+ return self .__in_expanding_bind (
270
293
self ._generate_generic_binary (binary , " IN " , ** kw )
271
294
)
272
295
273
296
def visit_empty_set_expr (self , element_types ):
274
297
return ""
275
298
276
- def visit_notin_op_binary (self , binary , operator , ** kw ):
277
- return self ._unnestify_in_expanding_bind (
278
- self ._generate_generic_binary (binary , " NOT IN " , ** kw )
299
+ def visit_not_in_op_binary (self , binary , operator , ** kw ):
300
+ return (
301
+ "("
302
+ + self .__in_expanding_bind (
303
+ self ._generate_generic_binary (binary , " NOT IN " , ** kw )
304
+ )
305
+ + ")"
279
306
)
280
307
308
+ visit_notin_op_binary = visit_not_in_op_binary # before 1.4
309
+
281
310
############################################################################
282
311
283
312
############################################################################
@@ -327,6 +356,10 @@ def visit_notendswith_op_binary(self, binary, operator, **kw):
327
356
328
357
############################################################################
329
358
359
+ __placeholder = re .compile (r"%\(([^\]:]+)(:[^\]:]+)?\)s$" ).match
360
+
361
+ __expanded_param = re .compile (fr"\(\[" fr"{ __expandng_text } " fr"_[^\]]+\]\)$" ).match
362
+
330
363
def visit_bindparam (
331
364
self ,
332
365
bindparam ,
@@ -365,8 +398,20 @@ def visit_bindparam(
365
398
# Values get arrayified at a lower level.
366
399
bq_type = bq_type [6 :- 1 ]
367
400
368
- assert param != "%s"
369
- return param .replace (")" , f":{ bq_type } )" )
401
+ assert_ (param != "%s" , f"Unexpected param: { param } " )
402
+
403
+ if bindparam .expanding :
404
+ assert_ (self .__expanded_param (param ), f"Unexpected param: { param } " )
405
+ param = param .replace (")" , f":{ bq_type } )" )
406
+
407
+ else :
408
+ m = self .__placeholder (param )
409
+ if m :
410
+ name , type_ = m .groups ()
411
+ assert_ (type_ is None )
412
+ param = f"%({ name } :{ bq_type } )s"
413
+
414
+ return param
370
415
371
416
372
417
class BigQueryTypeCompiler (GenericTypeCompiler ):
@@ -541,7 +586,6 @@ class BigQueryDialect(DefaultDialect):
541
586
supports_unicode_statements = True
542
587
supports_unicode_binds = True
543
588
supports_native_decimal = True
544
- returns_unicode_strings = True
545
589
description_encoding = None
546
590
supports_native_boolean = True
547
591
supports_simple_order_by_label = True
0 commit comments