3434from google .cloud .bigquery .table import TableReference
3535from google .api_core .exceptions import NotFound
3636
37+ import sqlalchemy
3738import sqlalchemy .sql .sqltypes
3839import sqlalchemy .sql .type_api
3940from sqlalchemy .exc import NoSuchTableError
5758FIELD_ILLEGAL_CHARACTERS = re .compile (r"[^\w]+" )
5859
5960
61+ def assert_ (cond , message = "Assertion failed" ): # pragma: NO COVER
62+ if not cond :
63+ raise AssertionError (message )
64+
65+
6066class BigQueryIdentifierPreparer (IdentifierPreparer ):
6167 """
6268 Set containing everything
@@ -152,15 +158,25 @@ def get_insert_default(self, column): # pragma: NO COVER
152158 elif isinstance (column .type , String ):
153159 return str (uuid .uuid4 ())
154160
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 ):
164180 # If we have an in parameter, it sometimes gets expaned to 0 or more
165181 # parameters and we need to move the type marker to each
166182 # parameter.
@@ -171,29 +187,29 @@ def pre_exec(
171187 # suffixes refect that when an array parameter is expanded,
172188 # numeric suffixes are added. For example, a placeholder like
173189 # `%(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 } ])"
174196
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+ )
184201
185202
186203class BigQueryCompiler (SQLCompiler ):
187204
188205 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"
190208
191- def __init__ (self , dialect , statement , column_keys = None , inline = False , ** kwargs ):
209+ def __init__ (self , dialect , statement , * args , ** kwargs ):
192210 if isinstance (statement , Column ):
193211 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 )
197213
198214 def visit_insert (self , insert_stmt , asfrom = False , ** kw ):
199215 # The (internal) documentation for `inline` is confusing, but
@@ -260,24 +276,37 @@ def group_by_clause(self, select, **kw):
260276 # no way to tell sqlalchemy that, so it works harder than
261277 # necessary and makes us do the same.
262278
263- _in_expanding_bind = re . compile ( r" IN \((\[EXPANDING_\w+\](:[A-Z0-9]+)?)\)$" )
279+ __sqlalchemy_version_info = tuple ( map ( int , sqlalchemy . __version__ . split ( "." )) )
264280
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+ )
267290
268291 def visit_in_op_binary (self , binary , operator_ , ** kw ):
269- return self ._unnestify_in_expanding_bind (
292+ return self .__in_expanding_bind (
270293 self ._generate_generic_binary (binary , " IN " , ** kw )
271294 )
272295
273296 def visit_empty_set_expr (self , element_types ):
274297 return ""
275298
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+ + ")"
279306 )
280307
308+ visit_notin_op_binary = visit_not_in_op_binary # before 1.4
309+
281310 ############################################################################
282311
283312 ############################################################################
@@ -327,6 +356,10 @@ def visit_notendswith_op_binary(self, binary, operator, **kw):
327356
328357 ############################################################################
329358
359+ __placeholder = re .compile (r"%\(([^\]:]+)(:[^\]:]+)?\)s$" ).match
360+
361+ __expanded_param = re .compile (fr"\(\[" fr"{ __expandng_text } " fr"_[^\]]+\]\)$" ).match
362+
330363 def visit_bindparam (
331364 self ,
332365 bindparam ,
@@ -365,8 +398,20 @@ def visit_bindparam(
365398 # Values get arrayified at a lower level.
366399 bq_type = bq_type [6 :- 1 ]
367400
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
370415
371416
372417class BigQueryTypeCompiler (GenericTypeCompiler ):
@@ -541,7 +586,6 @@ class BigQueryDialect(DefaultDialect):
541586 supports_unicode_statements = True
542587 supports_unicode_binds = True
543588 supports_native_decimal = True
544- returns_unicode_strings = True
545589 description_encoding = None
546590 supports_native_boolean = True
547591 supports_simple_order_by_label = True
0 commit comments