| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -32,6 +32,8 @@ | |||
| 32 | 32 | from google.cloud._helpers import _RFC3339_MICROS | |
| 33 | 33 | from google.cloud._helpers import _RFC3339_NO_FRACTION | |
| 34 | 34 | from google.cloud._helpers import _to_bytes | |
| 35 | + from google.cloud.bigquery import enums | ||
| 36 | + | ||
| 35 | 37 | from google.auth import credentials as ga_credentials # type: ignore | |
| 36 | 38 | from google.api_core import client_options as client_options_lib | |
| 37 | 39 | ||
@@ -252,11 +254,15 @@ def bytes_to_py(self, value, field): | |||
| 252 | 254 | if _not_null(value, field): | |
| 253 | 255 | return base64.standard_b64decode(_to_bytes(value)) | |
| 254 | 256 | ||
| 255 | - def timestamp_to_py(self, value, field): | ||
| 256 | - """Coerce 'value' to a datetime, if set or not nullable.""" | ||
| 257 | + def timestamp_to_py(self, value, field) -> Union[datetime.datetime, str, None]: | ||
| 258 | + """Coerce 'value' to a datetime, if set or not nullable. If timestamp | ||
| 259 | + is of picosecond precision, preserve the string format.""" | ||
| 260 | + if field.timestamp_precision == enums.TimestampPrecision.PICOSECOND: | ||
| 261 | + return value | ||
| 257 | 262 | if _not_null(value, field): | |
| 258 | 263 | # value will be a integer in seconds, to microsecond precision, in UTC. | |
| 259 | 264 | return _datetime_from_microseconds(int(value)) | |
| 265 | + return None | ||
| 260 | 266 | ||
| 261 | 267 | def datetime_to_py(self, value, field): | |
| 262 | 268 | """Coerce 'value' to a datetime, if set or not nullable. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -49,6 +49,7 @@ | |||
| 49 | 49 | import google.api_core.exceptions as core_exceptions | |
| 50 | 50 | from google.api_core import retry as retries | |
| 51 | 51 | ||
| 52 | + from google.cloud.bigquery import enums | ||
| 52 | 53 | from google.cloud.bigquery import job | |
| 53 | 54 | import google.cloud.bigquery.job.query | |
| 54 | 55 | import google.cloud.bigquery.query | |
@@ -265,6 +266,7 @@ def _to_query_request( | |||
| 265 | 266 | query: str, | |
| 266 | 267 | location: Optional[str] = None, | |
| 267 | 268 | timeout: Optional[float] = None, | |
| 269 | + timestamp_precision: Optional[enums.TimestampPrecision] = None, | ||
| 268 | 270 | ) -> Dict[str, Any]: | |
| 269 | 271 | """Transform from Job resource to QueryRequest resource. | |
| 270 | 272 | ||
@@ -285,10 +287,15 @@ def _to_query_request( | |||
| 285 | 287 | # Default to standard SQL. | |
| 286 | 288 | request_body.setdefault("useLegacySql", False) | |
| 287 | 289 | ||
| 288 | - # Since jobs.query can return results, ensure we use the lossless timestamp | ||
| 289 | - # format. See: https://github.com/googleapis/python-bigquery/issues/395 | ||
| 290 | 290 | request_body.setdefault("formatOptions", {}) | |
| 291 | - request_body["formatOptions"]["useInt64Timestamp"] = True # type: ignore | ||
| 291 | + | ||
| 292 | + # Cannot specify both use_int64_timestamp and timestamp_output_format. | ||
| 293 | + if timestamp_precision == enums.TimestampPrecision.PICOSECOND: | ||
| 294 | + request_body["formatOptions"]["timestampOutputFormat"] = "ISO8601_STRING" # type: ignore | ||
| 295 | + else: | ||
| 296 | + # Since jobs.query can return results, ensure we use the lossless | ||
| 297 | + # timestamp format. See: https://github.com/googleapis/python-bigquery/issues/395 | ||
| 298 | + request_body["formatOptions"]["useInt64Timestamp"] = True # type: ignore | ||
| 292 | 299 | ||
| 293 | 300 | if timeout is not None: | |
| 294 | 301 | # Subtract a buffer for context switching, network latency, etc. | |
@@ -370,14 +377,19 @@ def query_jobs_query( | |||
| 370 | 377 | retry: retries.Retry, | |
| 371 | 378 | timeout: Optional[float], | |
| 372 | 379 | job_retry: Optional[retries.Retry], | |
| 380 | + timestamp_precision: Optional[enums.TimestampPrecision] = None, | ||
| 373 | 381 | ) -> job.QueryJob: | |
| 374 | 382 | """Initiate a query using jobs.query with jobCreationMode=JOB_CREATION_REQUIRED. | |
| 375 | 383 | ||
| 376 | 384 | See: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query | |
| 377 | 385 | """ | |
| 378 | 386 | path = _to_query_path(project) | |
| 379 | 387 | request_body = _to_query_request( | |
| 380 | - query=query, job_config=job_config, location=location, timeout=timeout | ||
| 388 | + query=query, | ||
| 389 | + job_config=job_config, | ||
| 390 | + location=location, | ||
| 391 | + timeout=timeout, | ||
| 392 | + timestamp_precision=timestamp_precision, | ||
| 381 | 393 | ) | |
| 382 | 394 | ||
| 383 | 395 | def do_query(): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3469,6 +3469,8 @@ def query( | |||
| 3469 | 3469 | timeout: TimeoutType = DEFAULT_TIMEOUT, | |
| 3470 | 3470 | job_retry: Optional[retries.Retry] = DEFAULT_JOB_RETRY, | |
| 3471 | 3471 | api_method: Union[str, enums.QueryApiMethod] = enums.QueryApiMethod.INSERT, | |
| 3472 | + *, | ||
| 3473 | + timestamp_precision: Optional[enums.TimestampPrecision] = None, | ||
| 3472 | 3474 | ) -> job.QueryJob: | |
| 3473 | 3475 | """Run a SQL query. | |
| 3474 | 3476 | ||
@@ -3524,6 +3526,11 @@ def query( | |||
| 3524 | 3526 | ||
| 3525 | 3527 | See :class:`google.cloud.bigquery.enums.QueryApiMethod` for | |
| 3526 | 3528 | 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`. | ||
| 3527 | 3534 | ||
| 3528 | 3535 | Returns: | |
| 3529 | 3536 | google.cloud.bigquery.job.QueryJob: A new query job instance. | |
@@ -3543,6 +3550,15 @@ def query( | |||
| 3543 | 3550 | "`job_id` was provided, but the 'QUERY' `api_method` was requested." | |
| 3544 | 3551 | ) | |
| 3545 | 3552 | ||
| 3553 | + if ( | ||
| 3554 | + timestamp_precision == enums.TimestampPrecision.PICOSECOND | ||
| 3555 | + and api_method != enums.QueryApiMethod.QUERY | ||
| 3556 | + ): | ||
| 3557 | + raise ValueError( | ||
| 3558 | + "Picosecond Timestamp is only supported when `api_method " | ||
| 3559 | + "== enums.QueryApiMethod.QUERY`." | ||
| 3560 | + ) | ||
| 3561 | + | ||
| 3546 | 3562 | if project is None: | |
| 3547 | 3563 | project = self.project | |
| 3548 | 3564 | ||
@@ -3568,6 +3584,7 @@ def query( | |||
| 3568 | 3584 | retry, | |
| 3569 | 3585 | timeout, | |
| 3570 | 3586 | job_retry, | |
| 3587 | + timestamp_precision=timestamp_precision, | ||
| 3571 | 3588 | ) | |
| 3572 | 3589 | elif api_method == enums.QueryApiMethod.INSERT: | |
| 3573 | 3590 | return _job_helpers.query_jobs_insert( | |
@@ -4062,6 +4079,8 @@ def list_rows( | |||
| 4062 | 4079 | page_size: Optional[int] = None, | |
| 4063 | 4080 | retry: retries.Retry = DEFAULT_RETRY, | |
| 4064 | 4081 | timeout: TimeoutType = DEFAULT_TIMEOUT, | |
| 4082 | + *, | ||
| 4083 | + timestamp_precision: Optional[enums.TimestampPrecision] = None, | ||
| 4065 | 4084 | ) -> RowIterator: | |
| 4066 | 4085 | """List the rows of the table. | |
| 4067 | 4086 | ||
@@ -4110,6 +4129,11 @@ def list_rows( | |||
| 4110 | 4129 | before using ``retry``. | |
| 4111 | 4130 | If multiple requests are made under the hood, ``timeout`` | |
| 4112 | 4131 | applies to each individual request. | |
| 4132 | + timestamp_precision (Optional[enums.TimestampPrecision]): | ||
| 4133 | + [Private Preview] If set to `enums.TimestampPrecision.PICOSECOND`, | ||
| 4134 | + timestamp columns of picosecond precision will be returned with | ||
| 4135 | + full precision. Otherwise, will truncate to microsecond | ||
| 4136 | + precision. | ||
| 4113 | 4137 | ||
| 4114 | 4138 | Returns: | |
| 4115 | 4139 | google.cloud.bigquery.table.RowIterator: | |
@@ -4143,7 +4167,12 @@ def list_rows( | |||
| 4143 | 4167 | if start_index is not None: | |
| 4144 | 4168 | params["startIndex"] = start_index | |
| 4145 | 4169 | ||
| 4146 | - params["formatOptions.useInt64Timestamp"] = True | ||
| 4170 | + # Cannot specify both use_int64_timestamp and timestamp_output_format. | ||
| 4171 | + if timestamp_precision == enums.TimestampPrecision.PICOSECOND: | ||
| 4172 | + params["formatOptions.timestampOutputFormat"] = "ISO8601_STRING" | ||
| 4173 | + else: | ||
| 4174 | + params["formatOptions.useInt64Timestamp"] = True | ||
| 4175 | + | ||
| 4147 | 4176 | row_iterator = RowIterator( | |
| 4148 | 4177 | client=self, | |
| 4149 | 4178 | api_request=functools.partial(self._call_api, retry, timeout=timeout), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -759,6 +759,36 @@ def column_name_character_map(self, value: Optional[str]): | |||
| 759 | 759 | value = ColumnNameCharacterMap.COLUMN_NAME_CHARACTER_MAP_UNSPECIFIED | |
| 760 | 760 | self._set_sub_prop("columnNameCharacterMap", value) | |
| 761 | 761 | ||
| 762 | + @property | ||
| 763 | + def timestamp_target_precision(self) -> Optional[List[int]]: | ||
| 764 | + """Optional[list[int]]: [Private Preview] Precisions (maximum number of | ||
| 765 | + total digits in base 10) for seconds of TIMESTAMP types that are | ||
| 766 | + allowed to the destination table for autodetection mode. | ||
| 767 | + | ||
| 768 | + Available for the formats: CSV. | ||
| 769 | + | ||
| 770 | + For the CSV Format, Possible values include: | ||
| 771 | + None, [], or [6]: timestamp(6) for all auto detected TIMESTAMP | ||
| 772 | + columns. | ||
| 773 | + [6, 12]: timestamp(6) for all auto detected TIMESTAMP columns that | ||
| 774 | + have less than 6 digits of subseconds. timestamp(12) for all auto | ||
| 775 | + detected TIMESTAMP columns that have more than 6 digits of | ||
| 776 | + subseconds. | ||
| 777 | + [12]: timestamp(12) for all auto detected TIMESTAMP columns. | ||
| 778 | + | ||
| 779 | + The order of the elements in this array is ignored. Inputs that have | ||
| 780 | + higher precision than the highest target precision in this array will | ||
| 781 | + be truncated. | ||
| 782 | + """ | ||
| 783 | + return self._get_sub_prop("timestampTargetPrecision") | ||
| 784 | + | ||
| 785 | + @timestamp_target_precision.setter | ||
| 786 | + def timestamp_target_precision(self, value: Optional[List[int]]): | ||
| 787 | + if value is not None: | ||
| 788 | + self._set_sub_prop("timestampTargetPrecision", value) | ||
| 789 | + else: | ||
| 790 | + self._del_sub_prop("timestampTargetPrecision") | ||
| 791 | + | ||
| 762 | 792 | ||
| 763 | 793 | class LoadJob(_AsyncJob): | |
| 764 | 794 | """Asynchronous job for loading data into a table. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + 2025-01-01T00:00:00.123456789012Z | ||
| 2 | + 2025-01-02T00:00:00.123456789012Z | ||
| 3 | + 2025-01-03T00:00:00.123456789012Z | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,8 @@ | |||
| 1 | + [ | ||
| 2 | + { | ||
| 3 | + "name": "pico_col", | ||
| 4 | + "type": "TIMESTAMP", | ||
| 5 | + "mode": "NULLABLE", | ||
| 6 | + "timestampPrecision": "12" | ||
| 7 | + } | ||
| 8 | + ] | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -98,12 +98,14 @@ def load_scalars_table( | |||
| 98 | 98 | data_path: str = "scalars.jsonl", | |
| 99 | 99 | source_format=enums.SourceFormat.NEWLINE_DELIMITED_JSON, | |
| 100 | 100 | schema_source="scalars_schema.json", | |
| 101 | + timestamp_target_precision=None, | ||
| 101 | 102 | ) -> str: | |
| 102 | 103 | schema = bigquery_client.schema_from_json(DATA_DIR / schema_source) | |
| 103 | 104 | table_id = data_path.replace(".", "_") + hex(random.randrange(1000000)) | |
| 104 | 105 | job_config = bigquery.LoadJobConfig() | |
| 105 | 106 | job_config.schema = schema | |
| 106 | 107 | job_config.source_format = source_format | |
| 108 | + job_config.timestamp_target_precision = timestamp_target_precision | ||
| 107 | 109 | full_table_id = f"{project_id}.{dataset_id}.{table_id}" | |
| 108 | 110 | with open(DATA_DIR / data_path, "rb") as data_file: | |
| 109 | 111 | job = bigquery_client.load_table_from_file( | |
@@ -169,6 +171,23 @@ def scalars_table_csv( | |||
| 169 | 171 | bigquery_client.delete_table(full_table_id, not_found_ok=True) | |
| 170 | 172 | ||
| 171 | 173 | ||
| 174 | + @pytest.fixture(scope="session") | ||
| 175 | + def scalars_table_pico( | ||
| 176 | + bigquery_client: bigquery.Client, project_id: str, dataset_id: str | ||
| 177 | + ): | ||
| 178 | + full_table_id = load_scalars_table( | ||
| 179 | + bigquery_client, | ||
| 180 | + project_id, | ||
| 181 | + dataset_id, | ||
| 182 | + data_path="pico.csv", | ||
| 183 | + source_format=enums.SourceFormat.CSV, | ||
| 184 | + schema_source="pico_schema.json", | ||
| 185 | + timestamp_target_precision=[12], | ||
| 186 | + ) | ||
| 187 | + yield full_table_id | ||
| 188 | + bigquery_client.delete_table(full_table_id, not_found_ok=True) | ||
| 189 | + | ||
| 190 | + | ||
| 172 | 191 | @pytest.fixture | |
| 173 | 192 | def test_table_name(request, replace_non_anum=re.compile(r"[^a-zA-Z0-9_]").sub): | |
| 174 | 193 | return replace_non_anum("_", request.node.name) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1295,6 +1295,29 @@ def test_load_table_from_json_schema_autodetect_table_exists(self): | |||
| 1295 | 1295 | self.assertEqual(tuple(table.schema), table_schema) | |
| 1296 | 1296 | self.assertEqual(table.num_rows, 2) | |
| 1297 | 1297 | ||
| 1298 | + def test_load_table_from_csv_w_picosecond_timestamp(self): | ||
| 1299 | + dataset_id = _make_dataset_id("bq_system_test") | ||
| 1300 | + self.temp_dataset(dataset_id) | ||
| 1301 | + table_id = "{}.{}.load_table_from_json_basic_use".format( | ||
| 1302 | + Config.CLIENT.project, dataset_id | ||
| 1303 | + ) | ||
| 1304 | + | ||
| 1305 | + table_schema = Config.CLIENT.schema_from_json(DATA_PATH / "pico_schema.json") | ||
| 1306 | + # create the table before loading so that the column order is predictable | ||
| 1307 | + table = helpers.retry_403(Config.CLIENT.create_table)( | ||
| 1308 | + Table(table_id, schema=table_schema) | ||
| 1309 | + ) | ||
| 1310 | + self.to_delete.insert(0, table) | ||
| 1311 | + | ||
| 1312 | + # do not pass an explicit job config to trigger automatic schema detection | ||
| 1313 | + with open(DATA_PATH / "pico.csv", "rb") as f: | ||
| 1314 | + load_job = Config.CLIENT.load_table_from_file(f, table_id) | ||
| 1315 | + load_job.result() | ||
| 1316 | + | ||
| 1317 | + table = Config.CLIENT.get_table(table) | ||
| 1318 | + self.assertEqual(list(table.schema), table_schema) | ||
| 1319 | + self.assertEqual(table.num_rows, 3) | ||
| 1320 | + | ||
| 1298 | 1321 | def test_load_avro_from_uri_then_dump_table(self): | |
| 1299 | 1322 | from google.cloud.bigquery.job import CreateDisposition | |
| 1300 | 1323 | from google.cloud.bigquery.job import SourceFormat | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -132,3 +132,23 @@ def test_list_rows_range(bigquery_client: bigquery.Client, scalars_table_csv: st | |||
| 132 | 132 | ||
| 133 | 133 | row_null = rows[1] | |
| 134 | 134 | assert row_null["range_date"] is None | |
| 135 | + | ||
| 136 | + | ||
| 137 | + def test_list_rows_pico(bigquery_client: bigquery.Client, scalars_table_pico: str): | ||
| 138 | + rows = bigquery_client.list_rows( | ||
| 139 | + scalars_table_pico, timestamp_precision=enums.TimestampPrecision.PICOSECOND | ||
| 140 | + ) | ||
| 141 | + rows = list(rows) | ||
| 142 | + row = rows[0] | ||
| 143 | + assert row["pico_col"] == "2025-01-01T00:00:00.123456789012Z" | ||
| 144 | + | ||
| 145 | + | ||
| 146 | + def test_list_rows_pico_truncate( | ||
| 147 | + bigquery_client: bigquery.Client, scalars_table_pico: str | ||
| 148 | + ): | ||
| 149 | + # For a picosecond timestamp column, if the user does not explicitly set | ||
| 150 | + # timestamp_precision, will return truncated microsecond precision. | ||
| 151 | + rows = bigquery_client.list_rows(scalars_table_pico) | ||
| 152 | + rows = list(rows) | ||
| 153 | + row = rows[0] | ||
| 154 | + assert row["pico_col"] == "1735689600123456" | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,6 +21,7 @@ | |||
| 21 | 21 | import pytest | |
| 22 | 22 | ||
| 23 | 23 | from google.cloud import bigquery | |
| 24 | + from google.cloud.bigquery import enums | ||
| 24 | 25 | from google.cloud.bigquery.query import ArrayQueryParameter | |
| 25 | 26 | from google.cloud.bigquery.query import ScalarQueryParameter | |
| 26 | 27 | from google.cloud.bigquery.query import ScalarQueryParameterType | |
@@ -546,3 +547,15 @@ def test_session(bigquery_client: bigquery.Client, query_api_method: str): | |||
| 546 | 547 | ||
| 547 | 548 | assert len(rows) == 1 | |
| 548 | 549 | 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" | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments