From 70be24474390d4c2c54dbd9920d9da506894a9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Tue, 25 Jun 2024 12:56:50 +0200 Subject: [PATCH] feat: In SQL targets, users can now disable column type alterations with the `allow_column_alter` built-in setting --- singer_sdk/connectors/sql.py | 6 ++- singer_sdk/helpers/_builtin_setting.py | 51 ++++++++++++++++++++++ singer_sdk/helpers/capabilities.py | 7 +++ singer_sdk/target_base.py | 3 ++ tests/core/helpers/__init__.py | 0 tests/core/helpers/test_builtin_setting.py | 33 ++++++++++++++ tests/core/targets/test_target_sql.py | 1 + 7 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 singer_sdk/helpers/_builtin_setting.py create mode 100644 tests/core/helpers/__init__.py create mode 100644 tests/core/helpers/test_builtin_setting.py diff --git a/singer_sdk/connectors/sql.py b/singer_sdk/connectors/sql.py index 132c375186..2765c6b3d8 100644 --- a/singer_sdk/connectors/sql.py +++ b/singer_sdk/connectors/sql.py @@ -17,6 +17,7 @@ from singer_sdk import typing as th from singer_sdk._singerlib import CatalogEntry, MetadataMapping, Schema from singer_sdk.exceptions import ConfigValidationError +from singer_sdk.helpers._builtin_setting import BuiltinSetting from singer_sdk.helpers.capabilities import TargetLoadMethods if t.TYPE_CHECKING: @@ -39,7 +40,10 @@ class SQLConnector: # noqa: PLR0904 allow_column_add: bool = True # Whether ADD COLUMN is supported. allow_column_rename: bool = True # Whether RENAME COLUMN is supported. - allow_column_alter: bool = False # Whether altering column types is supported. + + #: Whether altering column types is supported. + allow_column_alter = BuiltinSetting(default=True) + allow_merge_upsert: bool = False # Whether MERGE UPSERT is supported. allow_overwrite: bool = False # Whether overwrite load method is supported. allow_temp_tables: bool = True # Whether temp tables are supported. diff --git a/singer_sdk/helpers/_builtin_setting.py b/singer_sdk/helpers/_builtin_setting.py new file mode 100644 index 0000000000..e9d6f192a3 --- /dev/null +++ b/singer_sdk/helpers/_builtin_setting.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import typing as t + +T = t.TypeVar("T") + + +class ConfiguredProtocol(t.Protocol): + """Protocol for objects with a config attribute.""" + + config: dict[str, t.Any] + + +class BuiltinSetting(t.Generic[T]): + """A descriptor that gets a value from a named key of the config attribute.""" + + def __init__(self, custom_key: str | None = None, *, default: T | None = None): + """Initialize the descriptor. + + Args: + custom_key: The key to get from the config attribute instead of the + attribute name. + default: The default value if the key is not found. + """ + self.key = custom_key + self.default = default + + def __set_name__(self, owner: type[ConfiguredProtocol], name: str) -> None: + """Set the name of the attribute. + + Args: + owner: The class of the object. + name: The name of the attribute. + """ + self.key = self.key or name + + def __get__( + self, + instance: ConfiguredProtocol, + owner: type[ConfiguredProtocol], + ) -> T | None: + """Get the value from the instance's config attribute. + + Args: + instance: The instance of the object. + owner: The class of the object. + + Returns: + The value from the config attribute. + """ + return instance.config.get(self.key, self.default) # type: ignore[no-any-return] diff --git a/singer_sdk/helpers/capabilities.py b/singer_sdk/helpers/capabilities.py index f76400c5a0..248871bed5 100644 --- a/singer_sdk/helpers/capabilities.py +++ b/singer_sdk/helpers/capabilities.py @@ -159,6 +159,13 @@ description="Maximum number of rows in each batch.", ), ).to_dict() +TARGET_ALLOW_COLUMN_ALTER_CONFIG = PropertiesList( + Property( + "allow_column_alter", + BooleanType, + description="Allow altering columns in the target database.", + ), +).to_dict() class TargetLoadMethods(str, Enum): diff --git a/singer_sdk/target_base.py b/singer_sdk/target_base.py index d560a555e4..87668b8e51 100644 --- a/singer_sdk/target_base.py +++ b/singer_sdk/target_base.py @@ -18,6 +18,7 @@ from singer_sdk.helpers.capabilities import ( ADD_RECORD_METADATA_CONFIG, BATCH_CONFIG, + TARGET_ALLOW_COLUMN_ALTER_CONFIG, TARGET_BATCH_SIZE_ROWS_CONFIG, TARGET_HARD_DELETE_CONFIG, TARGET_LOAD_METHOD_CONFIG, @@ -685,6 +686,8 @@ def _merge_missing(source_jsonschema: dict, target_jsonschema: dict) -> None: if k not in target_jsonschema["properties"]: target_jsonschema["properties"][k] = v + _merge_missing(TARGET_ALLOW_COLUMN_ALTER_CONFIG, config_jsonschema) + capabilities = cls.capabilities if TargetCapabilities.TARGET_SCHEMA in capabilities: diff --git a/tests/core/helpers/__init__.py b/tests/core/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/helpers/test_builtin_setting.py b/tests/core/helpers/test_builtin_setting.py new file mode 100644 index 0000000000..69e0a75f62 --- /dev/null +++ b/tests/core/helpers/test_builtin_setting.py @@ -0,0 +1,33 @@ +"""Test the BuiltinSetting descriptor.""" + +from __future__ import annotations + +from singer_sdk.helpers._builtin_setting import BuiltinSetting + + +def test_builtin_setting_descriptor(): + class ObjWithConfig: + example = BuiltinSetting(default=1) + + def __init__(self): + self.config = {"example": 1} + + obj = ObjWithConfig() + assert obj.example == 1 + + obj.config["example"] = 2 + assert obj.example == 2 + + +def test_builtin_setting_descriptor_custom_key(): + class ObjWithConfig: + my_attr = BuiltinSetting("example", default=1) + + def __init__(self): + self.config = {"example": 1} + + obj = ObjWithConfig() + assert obj.my_attr == 1 + + obj.config["example"] = 2 + assert obj.my_attr == 2 diff --git a/tests/core/targets/test_target_sql.py b/tests/core/targets/test_target_sql.py index e47845a70d..fbbeca309b 100644 --- a/tests/core/targets/test_target_sql.py +++ b/tests/core/targets/test_target_sql.py @@ -48,6 +48,7 @@ class MyTarget(SQLTargetMock, capabilities=capabilities): about = MyTarget._get_about_info() default_settings = { "add_record_metadata", + "allow_column_alter", "load_method", "batch_size_rows", }