Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Dataset.isCaseInsensitive #1671

Merged
20 changes: 20 additions & 0 deletions google/cloud/bigquery/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ class Dataset(object):
"default_table_expiration_ms": "defaultTableExpirationMs",
"friendly_name": "friendlyName",
"default_encryption_configuration": "defaultEncryptionConfiguration",
"is_case_insensitive": "isCaseInsensitive",
"storage_billing_model": "storageBillingModel",
"max_time_travel_hours": "maxTimeTravelHours",
"default_rounding_mode": "defaultRoundingMode",
Expand Down Expand Up @@ -822,6 +823,25 @@ def default_encryption_configuration(self, value):
api_repr = value.to_api_repr()
self._properties["defaultEncryptionConfiguration"] = api_repr

@property
def is_case_insensitive(self):
"""Optional[bool]: True if the dataset and its table names are case-insensitive, otherwise False.
By default, this is False, which means the dataset and its table names are case-sensitive.
This field does not affect routine references.

Raises:
ValueError: for invalid value types.
"""
return self._properties.get("isCaseInsensitive") or False

@is_case_insensitive.setter
def is_case_insensitive(self, value):
if not isinstance(value, bool) and value is not None:
raise ValueError("Pass a boolean value, or None")
if value is None:
value = False
self._properties["isCaseInsensitive"] = value

@property
def storage_billing_model(self):
"""Union[str, None]: StorageBillingModel of the dataset as set by the user
Expand Down
61 changes: 59 additions & 2 deletions tests/system/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ def test_create_dataset(self):
self.assertTrue(_dataset_exists(dataset))
self.assertEqual(dataset.dataset_id, DATASET_ID)
self.assertEqual(dataset.project, Config.CLIENT.project)
self.assertIs(dataset.is_case_insensitive, False)

def test_create_dataset_case_sensitive(self):
DATASET_ID = _make_dataset_id("create_cs_dataset")
dataset = self.temp_dataset(DATASET_ID, is_case_insensitive=False)
self.assertIs(dataset.is_case_insensitive, False)

def test_create_dataset_case_insensitive(self):
DATASET_ID = _make_dataset_id("create_ci_dataset")
dataset = self.temp_dataset(DATASET_ID, is_case_insensitive=True)
self.assertIs(dataset.is_case_insensitive, True)

def test_create_dataset_max_time_travel_hours(self):
DATASET_ID = _make_dataset_id("create_ci_dataset")
Expand Down Expand Up @@ -283,16 +294,19 @@ def test_update_dataset(self):
self.assertIsNone(dataset.friendly_name)
self.assertIsNone(dataset.description)
self.assertEqual(dataset.labels, {})
self.assertIs(dataset.is_case_insensitive, False)

dataset.friendly_name = "Friendly"
dataset.description = "Description"
dataset.labels = {"priority": "high", "color": "blue"}
dataset.is_case_insensitive = True
ds2 = Config.CLIENT.update_dataset(
dataset, ("friendly_name", "description", "labels")
dataset, ("friendly_name", "description", "labels", "is_case_insensitive")
)
self.assertEqual(ds2.friendly_name, "Friendly")
self.assertEqual(ds2.description, "Description")
self.assertEqual(ds2.labels, {"priority": "high", "color": "blue"})
self.assertIs(ds2.is_case_insensitive, True)

ds2.labels = {
"color": "green", # change
Expand Down Expand Up @@ -347,6 +361,48 @@ def test_create_table(self):
self.assertTrue(_table_exists(table))
self.assertEqual(table.table_id, table_id)

def test_create_tables_in_case_insensitive_dataset(self):
ci_dataset = self.temp_dataset(
_make_dataset_id("create_table"), is_case_insensitive=True
)
table_arg = Table(ci_dataset.table("test_table2"), schema=SCHEMA)
tablemc_arg = Table(ci_dataset.table("Test_taBLe2")) # same name, in Mixed Case

table = helpers.retry_403(Config.CLIENT.create_table)(table_arg)
self.to_delete.insert(0, table)

self.assertTrue(_table_exists(table_arg))
self.assertTrue(_table_exists(tablemc_arg))
self.assertIs(ci_dataset.is_case_insensitive, True)

def test_create_tables_in_case_sensitive_dataset(self):
ci_dataset = self.temp_dataset(
_make_dataset_id("create_table"), is_case_insensitive=False
)
table_arg = Table(ci_dataset.table("test_table3"), schema=SCHEMA)
tablemc_arg = Table(ci_dataset.table("Test_taBLe3")) # same name, in Mixed Case

table = helpers.retry_403(Config.CLIENT.create_table)(table_arg)
self.to_delete.insert(0, table)

self.assertTrue(_table_exists(table_arg))
self.assertFalse(_table_exists(tablemc_arg))
self.assertIs(ci_dataset.is_case_insensitive, False)

def test_create_tables_in_default_sensitivity_dataset(self):
dataset = self.temp_dataset(_make_dataset_id("create_table"))
table_arg = Table(dataset.table("test_table4"), schema=SCHEMA)
tablemc_arg = Table(
dataset.table("Test_taBLe4")
) # same name, in MC (Mixed Case)

table = helpers.retry_403(Config.CLIENT.create_table)(table_arg)
self.to_delete.insert(0, table)

self.assertTrue(_table_exists(table_arg))
self.assertFalse(_table_exists(tablemc_arg))
self.assertIs(dataset.is_case_insensitive, False)

def test_create_table_with_real_custom_policy(self):
from google.cloud.bigquery.schema import PolicyTagList

Expand Down Expand Up @@ -2308,7 +2364,8 @@ def temp_dataset(self, dataset_id, *args, **kwargs):
dataset.max_time_travel_hours = kwargs.get("max_time_travel_hours")
if kwargs.get("default_rounding_mode"):
dataset.default_rounding_mode = kwargs.get("default_rounding_mode")

if kwargs.get("is_case_insensitive"):
dataset.is_case_insensitive = kwargs.get("is_case_insensitive")
dataset = helpers.retry_403(Config.CLIENT.create_dataset)(dataset)
self.to_delete.append(dataset)
return dataset
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,9 @@ def _verify_resource_properties(self, dataset, resource):
self.assertEqual(dataset.description, resource.get("description"))
self.assertEqual(dataset.friendly_name, resource.get("friendlyName"))
self.assertEqual(dataset.location, resource.get("location"))
self.assertEqual(
dataset.is_case_insensitive, resource.get("isCaseInsensitive") or False
)
if "defaultEncryptionConfiguration" in resource:
self.assertEqual(
dataset.default_encryption_configuration.kms_key_name,
Expand Down Expand Up @@ -781,6 +784,7 @@ def test_ctor_defaults(self):
self.assertIsNone(dataset.description)
self.assertIsNone(dataset.friendly_name)
self.assertIsNone(dataset.location)
self.assertEqual(dataset.is_case_insensitive, False)

def test_ctor_string(self):
dataset = self._make_one("some-project.some_dset")
Expand Down Expand Up @@ -818,6 +822,7 @@ def test_ctor_explicit(self):
self.assertIsNone(dataset.description)
self.assertIsNone(dataset.friendly_name)
self.assertIsNone(dataset.location)
self.assertEqual(dataset.is_case_insensitive, False)

def test_access_entries_setter_non_list(self):
dataset = self._make_one(self.DS_REF)
Expand Down Expand Up @@ -910,6 +915,26 @@ def test_labels_getter_missing_value(self):
dataset = self._make_one(self.DS_REF)
self.assertEqual(dataset.labels, {})

def test_is_case_insensitive_setter_bad_value(self):
dataset = self._make_one(self.DS_REF)
with self.assertRaises(ValueError):
dataset.is_case_insensitive = 0

def test_is_case_insensitive_setter_true(self):
dataset = self._make_one(self.DS_REF)
dataset.is_case_insensitive = True
self.assertEqual(dataset.is_case_insensitive, True)

def test_is_case_insensitive_setter_none(self):
dataset = self._make_one(self.DS_REF)
dataset.is_case_insensitive = None
self.assertEqual(dataset.is_case_insensitive, False)

def test_is_case_insensitive_setter_false(self):
dataset = self._make_one(self.DS_REF)
dataset.is_case_insensitive = False
self.assertEqual(dataset.is_case_insensitive, False)

def test_from_api_repr_missing_identity(self):
self._setUpConstants()
RESOURCE = {}
Expand Down