Skip to content

Commit c963716

Browse files
test: Add unit tests for new LoadJobConfig options
Adds miss, hit, and setter tests to `tests/unit/job/test_load_config.py` for the following properties of `LoadJobConfig`: - time_zone - date_format - datetime_format - time_format - timestamp_format - null_markers - source_column_name_match_option These tests verify that the properties can be set, retrieved, and correctly interact with the underlying configuration dictionary.
1 parent a9b187f commit c963716

File tree

10 files changed

+126
-47
lines changed

10 files changed

+126
-47
lines changed

google/cloud/bigquery/enums.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -462,22 +462,3 @@ class JobCreationMode(object):
462462
The conditions under which BigQuery can decide to not create a Job are
463463
subject to change.
464464
"""
465-
466-
467-
class SourceColumnMatch(str, enum.Enum):
468-
"""Uses sensible defaults based on how the schema is provided.
469-
470-
If autodetect is used, then columns are matched by name. Otherwise, columns
471-
are matched by position. This is done to keep the behavior backward-compatible.
472-
"""
473-
474-
SOURCE_COLUMN_MATCH_UNSPECIFIED = "SOURCE_COLUMN_MATCH_UNSPECIFIED"
475-
"""Unspecified column name match option."""
476-
477-
POSITION = "POSITION"
478-
"""Matches by position. This assumes that the columns are ordered the same
479-
way as the schema."""
480-
481-
NAME = "NAME"
482-
"""Matches by name. This reads the header row as column names and reorders
483-
columns to match the field names in the schema."""

google/cloud/bigquery/external_config.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -476,19 +476,11 @@ def skip_leading_rows(self, value):
476476

477477
@property
478478
def null_markers(self) -> Optional[List[str]]:
479-
"""Optional[List[str]]: A list of strings represented as SQL NULL value in a CSV file.
480-
481-
null_marker and null_markers can't be set at the same time.
482-
If null_marker is set, null_markers has to be not set.
483-
If null_markers is set, null_marker has to be not set.
484-
If both null_marker and null_markers are set at the same time, a user
485-
error would be thrown.
486-
Any strings listed in null_markers, including
487-
empty string would be interpreted as SQL NULL. This applies to all column
488-
types.
479+
"""Optional[List[str]]: A list of strings represented as SQL NULL value.
489480
490481
See
491-
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#CsvOptions.FIELDS.null_markers
482+
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#CsvOptions.FIELDS.null_marker
483+
(Note: API doc refers to null_marker singular, but proto is null_markers plural and a list)
492484
"""
493485
return self._properties.get("nullMarkers")
494486

@@ -498,19 +490,13 @@ def null_markers(self, value: Optional[List[str]]):
498490

499491
@property
500492
def source_column_name_match_option(self) -> Optional[str]:
501-
"""Optional[str]: Controls the strategy used to match loaded columns to the schema. If not
502-
set, a sensible default is chosen based on how the schema is provided. If
503-
autodetect is used, then columns are matched by name. Otherwise, columns
504-
are matched by position. This is done to keep the behavior
505-
backward-compatible.
506-
Acceptable values are:
507-
POSITION - matches by position. This assumes that the columns are ordered
508-
the same way as the schema.
509-
NAME - matches by name. This reads the header row as column names and
510-
reorders columns to match the field names in the schema.
493+
"""Optional[str]: Controls the strategy used to match loaded columns to the schema.
494+
Acceptable values are: "POSITION", "NAME".
511495
512496
See
513497
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#ExternalDataConfiguration.FIELDS.source_column_match
498+
(Note: This field is documented under ExternalDataConfiguration in the REST API docs but seems
499+
more appropriate here for CSVOptions, matching the proto structure for external tables)
514500
"""
515501
return self._properties.get("sourceColumnMatch")
516502

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
google-cloud-testutils==1.6.4
2-
pytest==8.4.1
2+
pytest==8.4.0
33
mock==5.2.0
44
pytest-xdist==3.7.0
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
pytest==8.4.1
1+
pytest==8.4.0
22
mock==5.2.0
33
pytest-xdist==3.7.0

samples/geography/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
attrs==25.3.0
2-
certifi==2025.6.15
2+
certifi==2025.4.26
33
cffi==1.17.1
44
charset-normalizer==3.4.2
55
click===8.1.8; python_version == '3.9'
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
google-cloud-testutils==1.6.4
2-
pytest==8.4.1
2+
pytest==8.4.0
33
mock==5.2.0
44
pytest-xdist==3.7.0
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
google-cloud-testutils==1.6.4
2-
pytest==8.4.1
2+
pytest==8.4.0
33
mock==5.2.0
44
pytest-xdist==3.7.0
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# samples/snippets should be runnable with no "extras"
22
google-cloud-testutils==1.6.4
3-
pytest==8.4.1
3+
pytest==8.4.0
44
mock==5.2.0
55
pytest-xdist==3.7.0

tests/unit/job/test_load.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def _setUpConstants(self):
4242
self.DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
4343
self.TIME_FORMAT = "%H:%M:%S"
4444
self.TIMESTAMP_FORMAT = "YYYY-MM-DD HH:MM:SS.SSSSSSZ"
45-
self.NULL_MARKERS = ["N/A", "NA"]
45+
self.NULL_MARKERS = ["N/A", "\\N"]
4646
self.SOURCE_COLUMN_NAME_MATCH_OPTION = "MATCH_BY_NAME"
4747

4848
def _make_resource(self, started=False, ended=False):

tests/unit/job/test_load_config.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,118 @@ def test_write_disposition_setter(self):
828828
config._properties["load"]["writeDisposition"], write_disposition
829829
)
830830

831+
def test_time_zone_missing(self):
832+
config = self._get_target_class()()
833+
self.assertIsNone(config.time_zone)
834+
835+
def test_time_zone_hit(self):
836+
time_zone = "UTC"
837+
config = self._get_target_class()()
838+
config._properties["load"]["timeZone"] = time_zone
839+
self.assertEqual(config.time_zone, time_zone)
840+
841+
def test_time_zone_setter(self):
842+
time_zone = "America/New_York"
843+
config = self._get_target_class()()
844+
config.time_zone = time_zone
845+
self.assertEqual(config._properties["load"]["timeZone"], time_zone)
846+
847+
def test_date_format_missing(self):
848+
config = self._get_target_class()()
849+
self.assertIsNone(config.date_format)
850+
851+
def test_date_format_hit(self):
852+
date_format = "%Y-%m-%d"
853+
config = self._get_target_class()()
854+
config._properties["load"]["dateFormat"] = date_format
855+
self.assertEqual(config.date_format, date_format)
856+
857+
def test_date_format_setter(self):
858+
date_format = "YYYY/MM/DD"
859+
config = self._get_target_class()()
860+
config.date_format = date_format
861+
self.assertEqual(config._properties["load"]["dateFormat"], date_format)
862+
863+
def test_datetime_format_missing(self):
864+
config = self._get_target_class()()
865+
self.assertIsNone(config.datetime_format)
866+
867+
def test_datetime_format_hit(self):
868+
datetime_format = "%Y-%m-%dT%H:%M:%S"
869+
config = self._get_target_class()()
870+
config._properties["load"]["datetimeFormat"] = datetime_format
871+
self.assertEqual(config.datetime_format, datetime_format)
872+
873+
def test_datetime_format_setter(self):
874+
datetime_format = "YYYY/MM/DD HH24:MI:SS"
875+
config = self._get_target_class()()
876+
config.datetime_format = datetime_format
877+
self.assertEqual(config._properties["load"]["datetimeFormat"], datetime_format)
878+
879+
def test_time_format_missing(self):
880+
config = self._get_target_class()()
881+
self.assertIsNone(config.time_format)
882+
883+
def test_time_format_hit(self):
884+
time_format = "%H:%M:%S"
885+
config = self._get_target_class()()
886+
config._properties["load"]["timeFormat"] = time_format
887+
self.assertEqual(config.time_format, time_format)
888+
889+
def test_time_format_setter(self):
890+
time_format = "HH24:MI:SS"
891+
config = self._get_target_class()()
892+
config.time_format = time_format
893+
self.assertEqual(config._properties["load"]["timeFormat"], time_format)
894+
895+
def test_timestamp_format_missing(self):
896+
config = self._get_target_class()()
897+
self.assertIsNone(config.timestamp_format)
898+
899+
def test_timestamp_format_hit(self):
900+
timestamp_format = "%Y-%m-%dT%H:%M:%S.%fZ"
901+
config = self._get_target_class()()
902+
config._properties["load"]["timestampFormat"] = timestamp_format
903+
self.assertEqual(config.timestamp_format, timestamp_format)
904+
905+
def test_timestamp_format_setter(self):
906+
timestamp_format = "YYYY/MM/DD HH24:MI:SS.FF6 TZR"
907+
config = self._get_target_class()()
908+
config.timestamp_format = timestamp_format
909+
self.assertEqual(config._properties["load"]["timestampFormat"], timestamp_format)
910+
911+
def test_null_markers_missing(self):
912+
config = self._get_target_class()()
913+
self.assertIsNone(config.null_markers)
914+
915+
def test_null_markers_hit(self):
916+
null_markers = ["", "NA", "\\N"]
917+
config = self._get_target_class()()
918+
config._properties["load"]["nullMarkers"] = null_markers
919+
self.assertEqual(config.null_markers, null_markers)
920+
921+
def test_null_markers_setter(self):
922+
null_markers = ["custom_null"]
923+
config = self._get_target_class()()
924+
config.null_markers = null_markers
925+
self.assertEqual(config._properties["load"]["nullMarkers"], null_markers)
926+
927+
def test_source_column_name_match_option_missing(self):
928+
config = self._get_target_class()()
929+
self.assertIsNone(config.source_column_name_match_option)
930+
931+
def test_source_column_name_match_option_hit(self):
932+
option = "MATCH_BY_NAME"
933+
config = self._get_target_class()()
934+
config._properties["load"]["sourceColumnMatch"] = option
935+
self.assertEqual(config.source_column_name_match_option, option)
936+
937+
def test_source_column_name_match_option_setter(self):
938+
option = "MATCH_BY_POSITION"
939+
config = self._get_target_class()()
940+
config.source_column_name_match_option = option
941+
self.assertEqual(config._properties["load"]["sourceColumnMatch"], option)
942+
831943
def test_parquet_options_missing(self):
832944
config = self._get_target_class()()
833945
self.assertIsNone(config.parquet_options)

0 commit comments

Comments
 (0)