Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions target_clickhouse/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ def to_sql_type(
):
sql_type = clickhouse_sqlalchemy_types.Nullable(sql_type)

# Wrap any type in Nullable if the JSON schema allows null values
# and it's not already Nullable and not a primary key.
schema_type = jsonschema_type.get("type", [])
if (
isinstance(schema_type, list)
and "null" in schema_type
and not is_primary_key
and not isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)
):
sql_type = clickhouse_sqlalchemy_types.Nullable(sql_type)

return sql_type

def create_empty_table(
Expand Down
68 changes: 68 additions & 0 deletions tests/test_nullable_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Tests for nullable type handling in to_sql_type."""

from clickhouse_sqlalchemy import types as clickhouse_sqlalchemy_types

from target_clickhouse.connectors import ClickhouseConnector


def _to_sql_type(jsonschema_type, is_primary_key=False):
"""Call to_sql_type as an unbound method — avoids full connector init."""
return ClickhouseConnector.to_sql_type(
None,
jsonschema_type,
is_primary_key=is_primary_key,
)


class TestNullableTypeMapping:
"""Tests for nullable type wrapping in ClickhouseConnector.to_sql_type."""

def test_nullable_bool_returns_nullable(self):
"""Nullable bool schema should produce Nullable(Bool)."""
sql_type = _to_sql_type({"type": ["boolean", "null"]})
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_non_nullable_bool_returns_plain_bool(self):
"""Non-nullable bool schema should not be wrapped."""
sql_type = _to_sql_type({"type": ["boolean"]})
assert not isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_nullable_integer_returns_nullable(self):
"""Nullable integer schema should produce Nullable(Int64)."""
sql_type = _to_sql_type({"type": ["integer", "null"]})
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_nullable_string_returns_nullable(self):
"""Nullable string schema should produce Nullable(String)."""
sql_type = _to_sql_type({"type": ["string", "null"]})
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_nullable_number_returns_nullable(self):
"""Nullable number schema should produce Nullable(Float)."""
sql_type = _to_sql_type({"type": ["number", "null"]})
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_nullable_datetime_stays_nullable(self):
"""Datetime was already wrapped — should not double-wrap."""
sql_type = _to_sql_type(
{"type": ["string", "null"], "format": "date-time"},
)
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_primary_key_not_nullable(self):
"""Primary key columns should never be Nullable."""
sql_type = _to_sql_type(
{"type": ["boolean", "null"]},
is_primary_key=True,
)
assert not isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)

def test_non_nullable_integer_returns_int64(self):
"""Non-nullable integer should produce Int64."""
sql_type = _to_sql_type({"type": ["integer"]})
assert isinstance(sql_type, clickhouse_sqlalchemy_types.Int64)

def test_string_type_not_list(self):
"""Handle case where type is a string, not a list."""
sql_type = _to_sql_type({"type": "boolean"})
assert not isinstance(sql_type, clickhouse_sqlalchemy_types.Nullable)
Loading