1# Copyright 2021 Google LLC
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7# https://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import copy
16import typing
17from typing import Any, Dict, Iterable, List, Optional
18
19from google.cloud.bigquery.enums import StandardSqlTypeNames
20
21
22class StandardSqlDataType:
23 """The type of a variable, e.g., a function argument.
24
25 See:
26 https://cloud.google.com/bigquery/docs/reference/rest/v2/StandardSqlDataType
27
28 Examples:
29
30 .. code-block:: text
31
32 INT64: {type_kind="INT64"}
33 ARRAY: {type_kind="ARRAY", array_element_type="STRING"}
34 STRUCT<x STRING, y ARRAY>: {
35 type_kind="STRUCT",
36 struct_type={
37 fields=[
38 {name="x", type={type_kind="STRING"}},
39 {
40 name="y",
41 type={type_kind="ARRAY", array_element_type="DATE"}
42 }
43 ]
44 }
45 }
46 RANGE: {type_kind="RANGE", range_element_type="DATETIME"}
47
48 Args:
49 type_kind:
50 The top level type of this field. Can be any standard SQL data type,
51 e.g. INT64, DATE, ARRAY.
52 array_element_type:
53 The type of the array's elements, if type_kind is ARRAY.
54 struct_type:
55 The fields of this struct, in order, if type_kind is STRUCT.
56 range_element_type:
57 The type of the range's elements, if type_kind is RANGE.
58 """
59
60 def __init__(
61 self,
62 type_kind: Optional[
63 StandardSqlTypeNames
64 ] = StandardSqlTypeNames.TYPE_KIND_UNSPECIFIED,
65 array_element_type: Optional["StandardSqlDataType"] = None,
66 struct_type: Optional["StandardSqlStructType"] = None,
67 range_element_type: Optional["StandardSqlDataType"] = None,
68 ):
69 self._properties: Dict[str, Any] = {}
70
71 self.type_kind = type_kind
72 self.array_element_type = array_element_type
73 self.struct_type = struct_type
74 self.range_element_type = range_element_type
75
76 @property
77 def type_kind(self) -> Optional[StandardSqlTypeNames]:
78 """The top level type of this field.
79
80 Can be any standard SQL data type, e.g. INT64, DATE, ARRAY.
81 """
82 kind = self._properties["typeKind"]
83 return StandardSqlTypeNames[kind] # pytype: disable=missing-parameter
84
85 @type_kind.setter
86 def type_kind(self, value: Optional[StandardSqlTypeNames]):
87 if not value:
88 kind = StandardSqlTypeNames.TYPE_KIND_UNSPECIFIED.value
89 else:
90 kind = value.value
91 self._properties["typeKind"] = kind
92
93 @property
94 def array_element_type(self) -> Optional["StandardSqlDataType"]:
95 """The type of the array's elements, if type_kind is ARRAY."""
96 element_type = self._properties.get("arrayElementType")
97
98 if element_type is None:
99 return None
100
101 result = StandardSqlDataType()
102 result._properties = element_type # We do not use a copy on purpose.
103 return result
104
105 @array_element_type.setter
106 def array_element_type(self, value: Optional["StandardSqlDataType"]):
107 element_type = None if value is None else value.to_api_repr()
108
109 if element_type is None:
110 self._properties.pop("arrayElementType", None)
111 else:
112 self._properties["arrayElementType"] = element_type
113
114 @property
115 def struct_type(self) -> Optional["StandardSqlStructType"]:
116 """The fields of this struct, in order, if type_kind is STRUCT."""
117 struct_info = self._properties.get("structType")
118
119 if struct_info is None:
120 return None
121
122 result = StandardSqlStructType()
123 result._properties = struct_info # We do not use a copy on purpose.
124 return result
125
126 @struct_type.setter
127 def struct_type(self, value: Optional["StandardSqlStructType"]):
128 struct_type = None if value is None else value.to_api_repr()
129
130 if struct_type is None:
131 self._properties.pop("structType", None)
132 else:
133 self._properties["structType"] = struct_type
134
135 @property
136 def range_element_type(self) -> Optional["StandardSqlDataType"]:
137 """The type of the range's elements, if type_kind = "RANGE". Must be
138 one of DATETIME, DATE, or TIMESTAMP."""
139 range_element_info = self._properties.get("rangeElementType")
140
141 if range_element_info is None:
142 return None
143
144 result = StandardSqlDataType()
145 result._properties = range_element_info # We do not use a copy on purpose.
146 return result
147
148 @range_element_type.setter
149 def range_element_type(self, value: Optional["StandardSqlDataType"]):
150 range_element_type = None if value is None else value.to_api_repr()
151
152 if range_element_type is None:
153 self._properties.pop("rangeElementType", None)
154 else:
155 self._properties["rangeElementType"] = range_element_type
156
157 def to_api_repr(self) -> Dict[str, Any]:
158 """Construct the API resource representation of this SQL data type."""
159 return copy.deepcopy(self._properties)
160
161 @classmethod
162 def from_api_repr(cls, resource: Dict[str, Any]):
163 """Construct an SQL data type instance given its API representation."""
164 type_kind = resource.get("typeKind")
165 if type_kind not in StandardSqlTypeNames.__members__:
166 type_kind = StandardSqlTypeNames.TYPE_KIND_UNSPECIFIED
167 else:
168 # Convert string to an enum member.
169 type_kind = StandardSqlTypeNames[ # pytype: disable=missing-parameter
170 typing.cast(str, type_kind)
171 ]
172
173 array_element_type = None
174 if type_kind == StandardSqlTypeNames.ARRAY:
175 element_type = resource.get("arrayElementType")
176 if element_type:
177 array_element_type = cls.from_api_repr(element_type)
178
179 struct_type = None
180 if type_kind == StandardSqlTypeNames.STRUCT:
181 struct_info = resource.get("structType")
182 if struct_info:
183 struct_type = StandardSqlStructType.from_api_repr(struct_info)
184
185 range_element_type = None
186 if type_kind == StandardSqlTypeNames.RANGE:
187 range_element_info = resource.get("rangeElementType")
188 if range_element_info:
189 range_element_type = cls.from_api_repr(range_element_info)
190
191 return cls(type_kind, array_element_type, struct_type, range_element_type)
192
193 def __eq__(self, other):
194 if not isinstance(other, StandardSqlDataType):
195 return NotImplemented
196 else:
197 return (
198 self.type_kind == other.type_kind
199 and self.array_element_type == other.array_element_type
200 and self.struct_type == other.struct_type
201 and self.range_element_type == other.range_element_type
202 )
203
204 def __str__(self):
205 result = f"{self.__class__.__name__}(type_kind={self.type_kind!r}, ...)"
206 return result
207
208
209class StandardSqlField:
210 """A field or a column.
211
212 See:
213 https://cloud.google.com/bigquery/docs/reference/rest/v2/StandardSqlField
214
215 Args:
216 name:
217 The name of this field. Can be absent for struct fields.
218 type:
219 The type of this parameter. Absent if not explicitly specified.
220
221 For example, CREATE FUNCTION statement can omit the return type; in this
222 case the output parameter does not have this "type" field).
223 """
224
225 def __init__(
226 self, name: Optional[str] = None, type: Optional[StandardSqlDataType] = None
227 ):
228 type_repr = None if type is None else type.to_api_repr()
229 self._properties = {"name": name, "type": type_repr}
230
231 @property
232 def name(self) -> Optional[str]:
233 """The name of this field. Can be absent for struct fields."""
234 return typing.cast(Optional[str], self._properties["name"])
235
236 @name.setter
237 def name(self, value: Optional[str]):
238 self._properties["name"] = value
239
240 @property
241 def type(self) -> Optional[StandardSqlDataType]:
242 """The type of this parameter. Absent if not explicitly specified.
243
244 For example, CREATE FUNCTION statement can omit the return type; in this
245 case the output parameter does not have this "type" field).
246 """
247 type_info = self._properties["type"]
248
249 if type_info is None:
250 return None
251
252 result = StandardSqlDataType()
253 # We do not use a properties copy on purpose.
254 result._properties = typing.cast(Dict[str, Any], type_info)
255
256 return result
257
258 @type.setter
259 def type(self, value: Optional[StandardSqlDataType]):
260 value_repr = None if value is None else value.to_api_repr()
261 self._properties["type"] = value_repr
262
263 def to_api_repr(self) -> Dict[str, Any]:
264 """Construct the API resource representation of this SQL field."""
265 return copy.deepcopy(self._properties)
266
267 @classmethod
268 def from_api_repr(cls, resource: Dict[str, Any]):
269 """Construct an SQL field instance given its API representation."""
270 result = cls(
271 name=resource.get("name"),
272 type=StandardSqlDataType.from_api_repr(resource.get("type", {})),
273 )
274 return result
275
276 def __eq__(self, other):
277 if not isinstance(other, StandardSqlField):
278 return NotImplemented
279 else:
280 return self.name == other.name and self.type == other.type
281
282
283class StandardSqlStructType:
284 """Type of a struct field.
285
286 See:
287 https://cloud.google.com/bigquery/docs/reference/rest/v2/StandardSqlDataType#StandardSqlStructType
288
289 Args:
290 fields: The fields in this struct.
291 """
292
293 def __init__(self, fields: Optional[Iterable[StandardSqlField]] = None):
294 if fields is None:
295 fields = []
296 self._properties = {"fields": [field.to_api_repr() for field in fields]}
297
298 @property
299 def fields(self) -> List[StandardSqlField]:
300 """The fields in this struct."""
301 result = []
302
303 for field_resource in self._properties.get("fields", []):
304 field = StandardSqlField()
305 field._properties = field_resource # We do not use a copy on purpose.
306 result.append(field)
307
308 return result
309
310 @fields.setter
311 def fields(self, value: Iterable[StandardSqlField]):
312 self._properties["fields"] = [field.to_api_repr() for field in value]
313
314 def to_api_repr(self) -> Dict[str, Any]:
315 """Construct the API resource representation of this SQL struct type."""
316 return copy.deepcopy(self._properties)
317
318 @classmethod
319 def from_api_repr(cls, resource: Dict[str, Any]) -> "StandardSqlStructType":
320 """Construct an SQL struct type instance given its API representation."""
321 fields = (
322 StandardSqlField.from_api_repr(field_resource)
323 for field_resource in resource.get("fields", [])
324 )
325 return cls(fields=fields)
326
327 def __eq__(self, other):
328 if not isinstance(other, StandardSqlStructType):
329 return NotImplemented
330 else:
331 return self.fields == other.fields
332
333
334class StandardSqlTableType:
335 """A table type.
336
337 See:
338 https://cloud.google.com/workflows/docs/reference/googleapis/bigquery/v2/Overview#StandardSqlTableType
339
340 Args:
341 columns: The columns in this table type.
342 """
343
344 def __init__(self, columns: Iterable[StandardSqlField]):
345 self._properties = {"columns": [col.to_api_repr() for col in columns]}
346
347 @property
348 def columns(self) -> List[StandardSqlField]:
349 """The columns in this table type."""
350 result = []
351
352 for column_resource in self._properties.get("columns", []):
353 column = StandardSqlField()
354 column._properties = column_resource # We do not use a copy on purpose.
355 result.append(column)
356
357 return result
358
359 @columns.setter
360 def columns(self, value: Iterable[StandardSqlField]):
361 self._properties["columns"] = [col.to_api_repr() for col in value]
362
363 def to_api_repr(self) -> Dict[str, Any]:
364 """Construct the API resource representation of this SQL table type."""
365 return copy.deepcopy(self._properties)
366
367 @classmethod
368 def from_api_repr(cls, resource: Dict[str, Any]) -> "StandardSqlTableType":
369 """Construct an SQL table type instance given its API representation."""
370 columns = []
371
372 for column_resource in resource.get("columns", []):
373 type_ = column_resource.get("type")
374 if type_ is None:
375 type_ = {}
376
377 column = StandardSqlField(
378 name=column_resource.get("name"),
379 type=StandardSqlDataType.from_api_repr(type_),
380 )
381 columns.append(column)
382
383 return cls(columns=columns)
384
385 def __eq__(self, other):
386 if not isinstance(other, StandardSqlTableType):
387 return NotImplemented
388 else:
389 return self.columns == other.columns