Skip to content

Commit 4812857

Browse files
committed
support query()
1 parent d786b3f commit 4812857

File tree

5 files changed

+124
-1
lines changed

5 files changed

+124
-1
lines changed

google/cloud/bigquery/_job_helpers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import google.api_core.exceptions as core_exceptions
5050
from google.api_core import retry as retries
5151

52+
from google.cloud.bigquery import enums
5253
from google.cloud.bigquery import job
5354
import google.cloud.bigquery.job.query
5455
import google.cloud.bigquery.query
@@ -265,6 +266,7 @@ def _to_query_request(
265266
query: str,
266267
location: Optional[str] = None,
267268
timeout: Optional[float] = None,
269+
timestamp_precision: Optional[enums.TimestampPrecision] = None,
268270
) -> Dict[str, Any]:
269271
"""Transform from Job resource to QueryRequest resource.
270272
@@ -290,6 +292,12 @@ def _to_query_request(
290292
request_body.setdefault("formatOptions", {})
291293
request_body["formatOptions"]["useInt64Timestamp"] = True # type: ignore
292294

295+
if timestamp_precision == enums.TimestampPrecision.PICOSECOND:
296+
# Cannot specify both use_int64_timestamp and timestamp_output_format.
297+
del request_body["formatOptions"]["useInt64Timestamp"]
298+
299+
request_body["formatOptions"]["timestampOutputFormat"] = "ISO8601_STRING"
300+
293301
if timeout is not None:
294302
# Subtract a buffer for context switching, network latency, etc.
295303
request_body["timeoutMs"] = max(0, int(1000 * timeout) - _TIMEOUT_BUFFER_MILLIS)
@@ -370,14 +378,19 @@ def query_jobs_query(
370378
retry: retries.Retry,
371379
timeout: Optional[float],
372380
job_retry: Optional[retries.Retry],
381+
timestamp_precision: Optional[enums.TimestampPrecision] = None,
373382
) -> job.QueryJob:
374383
"""Initiate a query using jobs.query with jobCreationMode=JOB_CREATION_REQUIRED.
375384
376385
See: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query
377386
"""
378387
path = _to_query_path(project)
379388
request_body = _to_query_request(
380-
query=query, job_config=job_config, location=location, timeout=timeout
389+
query=query,
390+
job_config=job_config,
391+
location=location,
392+
timeout=timeout,
393+
timestamp_precision=timestamp_precision,
381394
)
382395

383396
def do_query():

google/cloud/bigquery/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3469,6 +3469,8 @@ def query(
34693469
timeout: TimeoutType = DEFAULT_TIMEOUT,
34703470
job_retry: Optional[retries.Retry] = DEFAULT_JOB_RETRY,
34713471
api_method: Union[str, enums.QueryApiMethod] = enums.QueryApiMethod.INSERT,
3472+
*,
3473+
timestamp_precision: Optional[enums.TimestampPrecision] = None,
34723474
) -> job.QueryJob:
34733475
"""Run a SQL query.
34743476
@@ -3524,6 +3526,11 @@ def query(
35243526
35253527
See :class:`google.cloud.bigquery.enums.QueryApiMethod` for
35263528
details on the difference between the query start methods.
3529+
timestamp_precision (Optional[enums.TimestampPrecision]):
3530+
[Private Preview] If set to `enums.TimestampPrecision.PICOSECOND`,
3531+
timestamp columns of picosecond precision will be returned with
3532+
full precision. Otherwise, will truncate to microsecond
3533+
precision. Only applies when api_method == `enums.QueryApiMethod.QUERY`.
35273534
35283535
Returns:
35293536
google.cloud.bigquery.job.QueryJob: A new query job instance.
@@ -3543,6 +3550,15 @@ def query(
35433550
"`job_id` was provided, but the 'QUERY' `api_method` was requested."
35443551
)
35453552

3553+
if (
3554+
timestamp_precision == enums.TimestampPrecision.PICOSECOND
3555+
and api_method == enums.QueryApiMethod.INSERT
3556+
):
3557+
raise ValueError(
3558+
"Picosecond Timestamp is only supported when `api_method "
3559+
"== enums.QueryApiMethod.QUERY`."
3560+
)
3561+
35463562
if project is None:
35473563
project = self.project
35483564

@@ -3568,6 +3584,7 @@ def query(
35683584
retry,
35693585
timeout,
35703586
job_retry,
3587+
timestamp_precision=timestamp_precision,
35713588
)
35723589
elif api_method == enums.QueryApiMethod.INSERT:
35733590
return _job_helpers.query_jobs_insert(

tests/system/test_query.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pytest
2222

2323
from google.cloud import bigquery
24+
from google.cloud.bigquery import enums
2425
from google.cloud.bigquery.query import ArrayQueryParameter
2526
from google.cloud.bigquery.query import ScalarQueryParameter
2627
from google.cloud.bigquery.query import ScalarQueryParameterType
@@ -546,3 +547,15 @@ def test_session(bigquery_client: bigquery.Client, query_api_method: str):
546547

547548
assert len(rows) == 1
548549
assert rows[0][0] == 5
550+
551+
552+
def test_query_picosecond(bigquery_client: bigquery.Client):
553+
job = bigquery_client.query(
554+
"SELECT CAST('2025-10-20' AS TIMESTAMP(12));",
555+
api_method="QUERY",
556+
timestamp_precision=enums.TimestampPrecision.PICOSECOND,
557+
)
558+
559+
result = job.result()
560+
rows = list(result)
561+
assert rows[0][0] == "2025-10-20T00:00:00.000000000000Z"

tests/unit/test__job_helpers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ def test_query_jobs_query_defaults():
335335
assert request["location"] == "asia-northeast1"
336336
assert request["formatOptions"]["useInt64Timestamp"] is True
337337
assert "timeoutMs" not in request
338+
assert "timestampOutputFormat" not in request["formatOptions"]
338339

339340

340341
def test_query_jobs_query_sets_format_options():
@@ -400,6 +401,35 @@ def test_query_jobs_query_sets_timeout(timeout, expected_timeout):
400401
assert request["timeoutMs"] == expected_timeout
401402

402403

404+
def test_query_jobs_query_picosecond():
405+
mock_client = mock.create_autospec(Client)
406+
mock_retry = mock.create_autospec(retries.Retry)
407+
mock_job_retry = mock.create_autospec(retries.Retry)
408+
mock_client._call_api.return_value = {
409+
"jobReference": {
410+
"projectId": "test-project",
411+
"jobId": "abc",
412+
"location": "asia-northeast1",
413+
}
414+
}
415+
_job_helpers.query_jobs_query(
416+
mock_client,
417+
"SELECT * FROM test",
418+
None,
419+
"asia-northeast1",
420+
"test-project",
421+
mock_retry,
422+
None,
423+
mock_job_retry,
424+
enums.TimestampPrecision.PICOSECOND,
425+
)
426+
427+
_, call_kwargs = mock_client._call_api.call_args
428+
request = call_kwargs["data"]
429+
assert "useInt64Timestamp" not in request["formatOptions"]
430+
assert request["formatOptions"]["timestampOutputFormat"] == "ISO8601_STRING"
431+
432+
403433
def test_query_and_wait_uses_jobs_insert():
404434
"""With unsupported features, call jobs.insert instead of jobs.query."""
405435
client = mock.create_autospec(Client)

tests/unit/test_client.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5214,6 +5214,56 @@ def test_query_w_query_parameters(self):
52145214
},
52155215
)
52165216

5217+
def test_query_pico_timestamp(self):
5218+
query = "select *;"
5219+
response = {
5220+
"jobReference": {
5221+
"projectId": self.PROJECT,
5222+
"location": "EU",
5223+
"jobId": "abcd",
5224+
},
5225+
}
5226+
creds = _make_credentials()
5227+
http = object()
5228+
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
5229+
conn = client._connection = make_connection(response)
5230+
5231+
client.query(
5232+
query,
5233+
location="EU",
5234+
api_method="QUERY",
5235+
timestamp_precision=TimestampPrecision.PICOSECOND,
5236+
)
5237+
5238+
# Check that query actually starts the job.
5239+
expected_resource = {
5240+
"query": query,
5241+
"useLegacySql": False,
5242+
"location": "EU",
5243+
"formatOptions": {"timestampOutputFormat": "ISO8601_STRING"},
5244+
"requestId": mock.ANY,
5245+
}
5246+
conn.api_request.assert_called_once_with(
5247+
method="POST",
5248+
path=f"/projects/{self.PROJECT}/queries",
5249+
data=expected_resource,
5250+
timeout=None,
5251+
)
5252+
5253+
def test_query_pico_timestamp_insert_error(self):
5254+
query = "select *;"
5255+
creds = _make_credentials()
5256+
http = object()
5257+
client = self._make_one(project=self.PROJECT, credentials=creds, _http=http)
5258+
5259+
with pytest.raises(ValueError):
5260+
client.query(
5261+
query,
5262+
location="EU",
5263+
api_method="INSERT",
5264+
timestamp_precision=TimestampPrecision.PICOSECOND,
5265+
)
5266+
52175267
def test_query_job_rpc_fail_w_random_error(self):
52185268
from google.api_core.exceptions import Unknown
52195269
from google.cloud.bigquery.job import QueryJob

0 commit comments

Comments
 (0)