Skip to content

Commit e4c94f4

Browse files
authored
feat: add session and connection properties to QueryJobConfig (#1024)
* feat: add session and connection properties to QueryJobConfig * add unit tests * adjust types and add versionadded * add missing url * link to ConnectionProperty docs * add resource classes to root module
1 parent e37380a commit e4c94f4

File tree

8 files changed

+220
-8
lines changed

8 files changed

+220
-8
lines changed

google/cloud/bigquery/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from google.cloud.bigquery.external_config import ExternalSourceFormat
5353
from google.cloud.bigquery.format_options import AvroOptions
5454
from google.cloud.bigquery.format_options import ParquetOptions
55+
from google.cloud.bigquery.job.base import SessionInfo
5556
from google.cloud.bigquery.job import Compression
5657
from google.cloud.bigquery.job import CopyJob
5758
from google.cloud.bigquery.job import CopyJobConfig
@@ -77,6 +78,7 @@
7778
from google.cloud.bigquery.model import ModelReference
7879
from google.cloud.bigquery.query import ArrayQueryParameter
7980
from google.cloud.bigquery.query import ArrayQueryParameterType
81+
from google.cloud.bigquery.query import ConnectionProperty
8082
from google.cloud.bigquery.query import ScalarQueryParameter
8183
from google.cloud.bigquery.query import ScalarQueryParameterType
8284
from google.cloud.bigquery.query import StructQueryParameter
@@ -104,6 +106,7 @@
104106
"__version__",
105107
"Client",
106108
# Queries
109+
"ConnectionProperty",
107110
"QueryJob",
108111
"QueryJobConfig",
109112
"ArrayQueryParameter",
@@ -132,6 +135,7 @@
132135
"ExtractJobConfig",
133136
"LoadJob",
134137
"LoadJobConfig",
138+
"SessionInfo",
135139
"UnknownJob",
136140
# Models
137141
"Model",

google/cloud/bigquery/job/base.py

+31
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,19 @@ def script_statistics(self) -> Optional["ScriptStatistics"]:
202202
return None
203203
return ScriptStatistics(resource)
204204

205+
@property
206+
def session_info(self) -> Optional["SessionInfo"]:
207+
"""[Preview] Information of the session if this job is part of one.
208+
209+
.. versionadded:: 2.29.0
210+
"""
211+
resource = _helpers._get_sub_prop(
212+
self._properties, ["statistics", "sessionInfo"]
213+
)
214+
if resource is None:
215+
return None
216+
return SessionInfo(resource)
217+
205218
@property
206219
def num_child_jobs(self):
207220
"""The number of child jobs executed.
@@ -990,6 +1003,24 @@ def evaluation_kind(self) -> Optional[str]:
9901003
return self._properties.get("evaluationKind")
9911004

9921005

1006+
class SessionInfo:
1007+
"""[Preview] Information of the session if this job is part of one.
1008+
1009+
.. versionadded:: 2.29.0
1010+
1011+
Args:
1012+
resource (Map[str, Any]): JSON representation of object.
1013+
"""
1014+
1015+
def __init__(self, resource):
1016+
self._properties = resource
1017+
1018+
@property
1019+
def session_id(self) -> Optional[str]:
1020+
"""The ID of the session."""
1021+
return self._properties.get("sessionId")
1022+
1023+
9931024
class UnknownJob(_AsyncJob):
9941025
"""A job whose type cannot be determined."""
9951026

google/cloud/bigquery/job/query.py

+67-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import copy
1919
import re
2020
import typing
21-
from typing import Any, Dict, List, Optional, Union
21+
from typing import Any, Dict, Iterable, List, Optional, Union
2222

2323
from google.api_core import exceptions
2424
from google.api_core.future import polling as polling_future
@@ -31,11 +31,14 @@
3131
from google.cloud.bigquery.enums import KeyResultStatementKind
3232
from google.cloud.bigquery.external_config import ExternalConfig
3333
from google.cloud.bigquery import _helpers
34-
from google.cloud.bigquery.query import _query_param_from_api_repr
35-
from google.cloud.bigquery.query import ArrayQueryParameter
36-
from google.cloud.bigquery.query import ScalarQueryParameter
37-
from google.cloud.bigquery.query import StructQueryParameter
38-
from google.cloud.bigquery.query import UDFResource
34+
from google.cloud.bigquery.query import (
35+
_query_param_from_api_repr,
36+
ArrayQueryParameter,
37+
ConnectionProperty,
38+
ScalarQueryParameter,
39+
StructQueryParameter,
40+
UDFResource,
41+
)
3942
from google.cloud.bigquery.retry import DEFAULT_RETRY, DEFAULT_JOB_RETRY
4043
from google.cloud.bigquery.routine import RoutineReference
4144
from google.cloud.bigquery.schema import SchemaField
@@ -269,6 +272,24 @@ def allow_large_results(self):
269272
def allow_large_results(self, value):
270273
self._set_sub_prop("allowLargeResults", value)
271274

275+
@property
276+
def connection_properties(self) -> List[ConnectionProperty]:
277+
"""Connection properties.
278+
279+
See
280+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationQuery.FIELDS.connection_properties
281+
282+
.. versionadded:: 2.29.0
283+
"""
284+
resource = self._get_sub_prop("connectionProperties", [])
285+
return [ConnectionProperty.from_api_repr(prop) for prop in resource]
286+
287+
@connection_properties.setter
288+
def connection_properties(self, value: Iterable[ConnectionProperty]):
289+
self._set_sub_prop(
290+
"connectionProperties", [prop.to_api_repr() for prop in value],
291+
)
292+
272293
@property
273294
def create_disposition(self):
274295
"""google.cloud.bigquery.job.CreateDisposition: Specifies behavior
@@ -283,6 +304,27 @@ def create_disposition(self):
283304
def create_disposition(self, value):
284305
self._set_sub_prop("createDisposition", value)
285306

307+
@property
308+
def create_session(self) -> Optional[bool]:
309+
"""[Preview] If :data:`True`, creates a new session, where
310+
:attr:`~google.cloud.bigquery.job.QueryJob.session_info` will contain a
311+
random server generated session id.
312+
313+
If :data:`False`, runs query with an existing ``session_id`` passed in
314+
:attr:`~google.cloud.bigquery.job.QueryJobConfig.connection_properties`,
315+
otherwise runs query in non-session mode.
316+
317+
See
318+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#JobConfigurationQuery.FIELDS.create_session
319+
320+
.. versionadded:: 2.29.0
321+
"""
322+
return self._get_sub_prop("createSession")
323+
324+
@create_session.setter
325+
def create_session(self, value: Optional[bool]):
326+
self._set_sub_prop("createSession", value)
327+
286328
@property
287329
def default_dataset(self):
288330
"""google.cloud.bigquery.dataset.DatasetReference: the default dataset
@@ -613,7 +655,7 @@ def schema_update_options(self, values):
613655

614656
@property
615657
def script_options(self) -> ScriptOptions:
616-
"""Connection properties which can modify the query behavior.
658+
"""Options controlling the execution of scripts.
617659
618660
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#scriptoptions
619661
"""
@@ -694,13 +736,31 @@ def allow_large_results(self):
694736
"""
695737
return self._configuration.allow_large_results
696738

739+
@property
740+
def connection_properties(self) -> List[ConnectionProperty]:
741+
"""See
742+
:attr:`google.cloud.bigquery.job.QueryJobConfig.connection_properties`.
743+
744+
.. versionadded:: 2.29.0
745+
"""
746+
return self._configuration.connection_properties
747+
697748
@property
698749
def create_disposition(self):
699750
"""See
700751
:attr:`google.cloud.bigquery.job.QueryJobConfig.create_disposition`.
701752
"""
702753
return self._configuration.create_disposition
703754

755+
@property
756+
def create_session(self) -> Optional[bool]:
757+
"""See
758+
:attr:`google.cloud.bigquery.job.QueryJobConfig.create_session`.
759+
760+
.. versionadded:: 2.29.0
761+
"""
762+
return self._configuration.create_session
763+
704764
@property
705765
def default_dataset(self):
706766
"""See

google/cloud/bigquery/query.py

+60-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import copy
1919
import datetime
2020
import decimal
21-
from typing import Optional, Union
21+
from typing import Any, Optional, Dict, Union
2222

2323
from google.cloud.bigquery.table import _parse_schema_resource
2424
from google.cloud.bigquery._helpers import _rows_from_json
@@ -31,6 +31,65 @@
3131
]
3232

3333

34+
class ConnectionProperty:
35+
"""A connection-level property to customize query behavior.
36+
37+
See
38+
https://cloud.google.com/bigquery/docs/reference/rest/v2/ConnectionProperty
39+
40+
Args:
41+
key:
42+
The key of the property to set, for example, ``'time_zone'`` or
43+
``'session_id'``.
44+
value: The value of the property to set.
45+
"""
46+
47+
def __init__(self, key: str = "", value: str = ""):
48+
self._properties = {
49+
"key": key,
50+
"value": value,
51+
}
52+
53+
@property
54+
def key(self) -> str:
55+
"""Name of the property.
56+
57+
For example:
58+
59+
* ``time_zone``
60+
* ``session_id``
61+
"""
62+
return self._properties["key"]
63+
64+
@property
65+
def value(self) -> str:
66+
"""Value of the property."""
67+
return self._properties["value"]
68+
69+
@classmethod
70+
def from_api_repr(cls, resource) -> "ConnectionProperty":
71+
"""Construct :class:`~google.cloud.bigquery.query.ConnectionProperty`
72+
from JSON resource.
73+
74+
Args:
75+
resource: JSON representation.
76+
77+
Returns:
78+
A connection property.
79+
"""
80+
value = cls()
81+
value._properties = resource
82+
return value
83+
84+
def to_api_repr(self) -> Dict[str, Any]:
85+
"""Construct JSON API representation for the connection property.
86+
87+
Returns:
88+
JSON mapping
89+
"""
90+
return self._properties
91+
92+
3493
class UDFResource(object):
3594
"""Describe a single user-defined function (UDF) resource.
3695

tests/system/test_query.py

+26
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,29 @@ def test_dry_run(bigquery_client: bigquery.Client, scalars_table: str):
2727
assert query_job.dry_run is True
2828
assert query_job.total_bytes_processed > 0
2929
assert len(query_job.schema) > 0
30+
31+
32+
def test_session(bigquery_client: bigquery.Client):
33+
initial_config = bigquery.QueryJobConfig()
34+
initial_config.create_session = True
35+
initial_query = """
36+
CREATE TEMPORARY TABLE numbers(id INT64)
37+
AS
38+
SELECT * FROM UNNEST([1, 2, 3, 4, 5]) AS id;
39+
"""
40+
initial_job = bigquery_client.query(initial_query, job_config=initial_config)
41+
initial_job.result()
42+
session_id = initial_job.session_info.session_id
43+
assert session_id is not None
44+
45+
second_config = bigquery.QueryJobConfig()
46+
second_config.connection_properties = [
47+
bigquery.ConnectionProperty("session_id", session_id),
48+
]
49+
second_job = bigquery_client.query(
50+
"SELECT COUNT(*) FROM numbers;", job_config=second_config
51+
)
52+
rows = list(second_job.result())
53+
54+
assert len(rows) == 1
55+
assert rows[0][0] == 5

tests/unit/job/test_base.py

+9
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ def test_script_statistics(self):
228228
self.assertEqual(stack_frame.end_column, 14)
229229
self.assertEqual(stack_frame.text, "QUERY TEXT")
230230

231+
def test_session_info(self):
232+
client = _make_client(project=self.PROJECT)
233+
job = self._make_one(self.JOB_ID, client)
234+
235+
self.assertIsNone(job.session_info)
236+
job._properties["statistics"] = {"sessionInfo": {"sessionId": "abcdefg"}}
237+
self.assertIsNotNone(job.session_info)
238+
self.assertEqual(job.session_info.session_id, "abcdefg")
239+
231240
def test_transaction_info(self):
232241
from google.cloud.bigquery.job.base import TransactionInfo
233242

tests/unit/job/test_query.py

+2
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def test_from_api_repr_bare(self):
281281
job = klass.from_api_repr(RESOURCE, client=client)
282282
self.assertIs(job._client, client)
283283
self._verifyResourceProperties(job, RESOURCE)
284+
self.assertEqual(len(job.connection_properties), 0)
285+
self.assertIsNone(job.create_session)
284286

285287
def test_from_api_repr_with_encryption(self):
286288
self._setUpConstants()

tests/unit/job/test_query_config.py

+21
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,27 @@ def test_clustering_fields(self):
152152
config.clustering_fields = None
153153
self.assertIsNone(config.clustering_fields)
154154

155+
def test_connection_properties(self):
156+
from google.cloud.bigquery.job.query import ConnectionProperty
157+
158+
config = self._get_target_class()()
159+
self.assertEqual(len(config.connection_properties), 0)
160+
161+
session_id = ConnectionProperty("session_id", "abcd")
162+
time_zone = ConnectionProperty("time_zone", "America/Chicago")
163+
config.connection_properties = [session_id, time_zone]
164+
self.assertEqual(len(config.connection_properties), 2)
165+
self.assertEqual(config.connection_properties[0].key, "session_id")
166+
self.assertEqual(config.connection_properties[0].value, "abcd")
167+
self.assertEqual(config.connection_properties[1].key, "time_zone")
168+
self.assertEqual(config.connection_properties[1].value, "America/Chicago")
169+
170+
def test_create_session(self):
171+
config = self._get_target_class()()
172+
self.assertIsNone(config.create_session)
173+
config.create_session = True
174+
self.assertTrue(config.create_session)
175+
155176
def test_from_api_repr_empty(self):
156177
klass = self._get_target_class()
157178
config = klass.from_api_repr({})

0 commit comments

Comments
 (0)