Skip to content

Commit 83ef0c2

Browse files
feat: In SQL targets, users can now disable column type alterations with the allow_column_alter built-in setting
1 parent 9e051aa commit 83ef0c2

File tree

7 files changed

+139
-3
lines changed

7 files changed

+139
-3
lines changed

singer_sdk/connectors/sql.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
from singer_sdk import typing as th
1818
from singer_sdk._singerlib import CatalogEntry, MetadataMapping, Schema
1919
from singer_sdk.exceptions import ConfigValidationError
20-
from singer_sdk.helpers.capabilities import TargetLoadMethods
20+
from singer_sdk.helpers.capabilities import (
21+
TARGET_ALLOW_COLUMN_ALTER_CONFIG,
22+
TargetLoadMethods,
23+
)
2124

2225
if t.TYPE_CHECKING:
2326
from sqlalchemy.engine import Engine
@@ -39,7 +42,10 @@ class SQLConnector: # noqa: PLR0904
3942

4043
allow_column_add: bool = True # Whether ADD COLUMN is supported.
4144
allow_column_rename: bool = True # Whether RENAME COLUMN is supported.
42-
allow_column_alter: bool = False # Whether altering column types is supported.
45+
46+
#: Whether altering column types is supported.
47+
allow_column_alter = TARGET_ALLOW_COLUMN_ALTER_CONFIG.attribute(default=True)
48+
4349
allow_merge_upsert: bool = False # Whether MERGE UPSERT is supported.
4450
allow_overwrite: bool = False # Whether overwrite load method is supported.
4551
allow_temp_tables: bool = True # Whether temp tables are supported.
@@ -1126,7 +1132,7 @@ def _adapt_column_type(
11261132
return
11271133

11281134
# Not the same type, generic type or compatible types
1129-
# calling merge_sql_types for assistnace
1135+
# calling merge_sql_types for assistance
11301136
compatible_sql_type = self.merge_sql_types([current_type, sql_type])
11311137

11321138
if str(compatible_sql_type) == str(current_type):
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
T = t.TypeVar("T")
6+
7+
8+
class ConfigProperty(t.Generic[T]):
9+
"""A descriptor that gets a value from a named key of the config attribute."""
10+
11+
def __init__(self, custom_key: str | None = None, *, default: T | None = None):
12+
"""Initialize the descriptor.
13+
14+
Args:
15+
custom_key: The key to get from the config attribute instead of the
16+
attribute name.
17+
default: The default value if the key is not found.
18+
"""
19+
self.key = custom_key
20+
self.default = default
21+
22+
def __set_name__(self, owner, name: str) -> None: # noqa: ANN001
23+
"""Set the name of the attribute.
24+
25+
Args:
26+
owner: The class of the object.
27+
name: The name of the attribute.
28+
"""
29+
self.key = self.key or name
30+
31+
def __get__(self, instance, owner) -> T | None: # noqa: ANN001
32+
"""Get the value from the instance's config attribute.
33+
34+
Args:
35+
instance: The instance of the object.
36+
owner: The class of the object.
37+
38+
Returns:
39+
The value from the config attribute.
40+
"""
41+
return instance.config.get(self.key, self.default) # type: ignore[no-any-return]

singer_sdk/helpers/capabilities.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from enum import Enum, EnumMeta
77
from warnings import warn
88

9+
from singer_sdk.helpers._config_property import ConfigProperty
910
from singer_sdk.typing import (
1011
ArrayType,
1112
BooleanType,
@@ -19,6 +20,47 @@
1920
)
2021

2122
_EnumMemberT = t.TypeVar("_EnumMemberT")
23+
T = t.TypeVar("T")
24+
25+
26+
class Builtin:
27+
"""Use this class to define built-in setting(s) for a plugin."""
28+
29+
def __init__(
30+
self,
31+
schema: dict[str, t.Any],
32+
*,
33+
capability: CapabilitiesEnum | None = None,
34+
**kwargs: t.Any,
35+
):
36+
"""Initialize the descriptor.
37+
38+
Args:
39+
schema: The JSON schema for the setting.
40+
capability: The capability that the setting is associated with.
41+
kwargs: Additional keyword arguments.
42+
"""
43+
self.schema = schema
44+
self.capability = capability
45+
self.kwargs = kwargs
46+
47+
def attribute( # noqa: PLR6301
48+
self,
49+
custom_key: str | None = None,
50+
*,
51+
default: T | None = None,
52+
) -> ConfigProperty[T]:
53+
"""Generate a class attribute for the setting.
54+
55+
Args:
56+
custom_key: Custom key to use in the config.
57+
default: Default value for the setting.
58+
59+
Returns:
60+
Class attribute for the setting.
61+
"""
62+
return ConfigProperty(custom_key=custom_key, default=default)
63+
2264

2365
# Default JSON Schema to support config for built-in capabilities:
2466

@@ -160,6 +202,16 @@
160202
),
161203
).to_dict()
162204

205+
TARGET_ALLOW_COLUMN_ALTER_CONFIG = Builtin(
206+
schema=PropertiesList(
207+
Property(
208+
"allow_column_alter",
209+
BooleanType,
210+
description="Allow altering columns in the target database.",
211+
),
212+
).to_dict(),
213+
)
214+
163215

164216
class TargetLoadMethods(str, Enum):
165217
"""Target-specific capabilities."""

singer_sdk/target_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from singer_sdk.helpers.capabilities import (
1919
ADD_RECORD_METADATA_CONFIG,
2020
BATCH_CONFIG,
21+
TARGET_ALLOW_COLUMN_ALTER_CONFIG,
2122
TARGET_BATCH_SIZE_ROWS_CONFIG,
2223
TARGET_HARD_DELETE_CONFIG,
2324
TARGET_LOAD_METHOD_CONFIG,
@@ -685,6 +686,8 @@ def _merge_missing(source_jsonschema: dict, target_jsonschema: dict) -> None:
685686
if k not in target_jsonschema["properties"]:
686687
target_jsonschema["properties"][k] = v
687688

689+
_merge_missing(TARGET_ALLOW_COLUMN_ALTER_CONFIG.schema, config_jsonschema)
690+
688691
capabilities = cls.capabilities
689692

690693
if TargetCapabilities.TARGET_SCHEMA in capabilities:

tests/core/helpers/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Test the BuiltinSetting descriptor."""
2+
3+
from __future__ import annotations
4+
5+
from singer_sdk.helpers._config_property import ConfigProperty
6+
7+
8+
def test_builtin_setting_descriptor():
9+
class ObjWithConfig:
10+
example = ConfigProperty(default=1)
11+
12+
def __init__(self):
13+
self.config = {"example": 1}
14+
15+
obj = ObjWithConfig()
16+
assert obj.example == 1
17+
18+
obj.config["example"] = 2
19+
assert obj.example == 2
20+
21+
22+
def test_builtin_setting_descriptor_custom_key():
23+
class ObjWithConfig:
24+
my_attr = ConfigProperty("example", default=1)
25+
26+
def __init__(self):
27+
self.config = {"example": 1}
28+
29+
obj = ObjWithConfig()
30+
assert obj.my_attr == 1
31+
32+
obj.config["example"] = 2
33+
assert obj.my_attr == 2

tests/core/targets/test_target_sql.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class MyTarget(SQLTargetMock, capabilities=capabilities):
4848
about = MyTarget._get_about_info()
4949
default_settings = {
5050
"add_record_metadata",
51+
"allow_column_alter",
5152
"load_method",
5253
"batch_size_rows",
5354
}

0 commit comments

Comments
 (0)