Skip to content

Commit 30dec9d

Browse files
committed
Add default_expr support for datasource defaults
1 parent 7c53418 commit 30dec9d

6 files changed

Lines changed: 94 additions & 14 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "tinybird-sdk"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
description = "Python SDK for Tinybird Forward"
55
readme = "README.md"
66
authors = [

src/tinybird_sdk/generator/datasource.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ def _generate_column_line(column_name: str, column: TypeValidator | ColumnDefini
5858
parts.append(f"`json:{effective_json_path}`")
5959

6060
if validator._modifiers.has_default:
61-
parts.append(f"DEFAULT {_format_default_value(validator._modifiers.default_value, tinybird_type)}")
61+
if isinstance(validator._modifiers.default_expression, str):
62+
parts.append(f"DEFAULT {validator._modifiers.default_expression}")
63+
else:
64+
parts.append(f"DEFAULT {_format_default_value(validator._modifiers.default_value, tinybird_type)}")
6265

6366
if validator._modifiers.codec:
6467
parts.append(f"CODEC({validator._modifiers.codec})")

src/tinybird_sdk/migrate/emit_ts.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,19 @@ def _emit_datasource(ds: DatasourceModel) -> str:
175175
validator = _strict_column_type_to_validator(column.type)
176176

177177
if column.default_expression is not None:
178-
parsed_default = parse_literal_from_datafile(column.default_expression)
179-
literal_value: Any = parsed_default
180-
if isinstance(parsed_default, (int, float)) and _is_boolean_type(column.type):
181-
if parsed_default in {0, 1}:
182-
literal_value = bool(parsed_default)
183-
else:
184-
raise ValueError(
185-
f'Boolean default value must be 0 or 1 for column "{column.name}" in datasource "{ds.name}".'
186-
)
187-
validator += f".default({repr(literal_value)})"
178+
try:
179+
parsed_default = parse_literal_from_datafile(column.default_expression)
180+
literal_value: Any = parsed_default
181+
if isinstance(parsed_default, (int, float)) and _is_boolean_type(column.type):
182+
if parsed_default in {0, 1}:
183+
literal_value = bool(parsed_default)
184+
else:
185+
raise ValueError(
186+
f'Boolean default value must be 0 or 1 for column "{column.name}" in datasource "{ds.name}".'
187+
)
188+
validator += f".default({repr(literal_value)})"
189+
except ValueError:
190+
validator += f".default_expr({_escape_string(column.default_expression)})"
188191

189192
if column.codec:
190193
validator += f".codec({_escape_string(column.codec)})"

src/tinybird_sdk/schema/types.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TypeModifiers:
1212
low_cardinality: bool = False
1313
has_default: bool = False
1414
default_value: Any = None
15+
default_expression: str | None = None
1516
codec: str | None = None
1617

1718

@@ -53,7 +54,22 @@ def default(self, value: Any) -> "TypeValidator":
5354
return TypeValidator(
5455
_python_type=self._python_type,
5556
_tinybirdType=self._tinybirdType,
56-
_modifiers=replace(self._modifiers, has_default=True, default_value=value),
57+
_modifiers=replace(
58+
self._modifiers, has_default=True, default_value=value, default_expression=None
59+
),
60+
)
61+
62+
def default_expr(self, expression: str) -> "TypeValidator":
63+
trimmed = expression.strip()
64+
if not trimmed:
65+
raise ValueError("Default expression cannot be empty.")
66+
67+
return TypeValidator(
68+
_python_type=self._python_type,
69+
_tinybirdType=self._tinybirdType,
70+
_modifiers=replace(
71+
self._modifiers, has_default=True, default_value=None, default_expression=trimmed
72+
),
5773
)
5874

5975
def codec(self, codec: str) -> "TypeValidator":

tests/test_phase1_schema_generator_parity.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from tinybird_sdk import define_datasource, define_kafka_connection, t
5+
from tinybird_sdk import define_datasource, define_kafka_connection, get_modifiers, t
66
from tinybird_sdk.generator.connection import generate_connection
77
from tinybird_sdk.generator.datasource import generate_datasource
88

@@ -80,3 +80,31 @@ def test_generate_datasource_ignores_non_string_json_path(monkeypatch: pytest.Mo
8080

8181
generated = generate_datasource(datasource)
8282
assert "`json:$.id`" in generated.content
83+
84+
85+
def test_type_validator_default_expr_stores_expression() -> None:
86+
validator = t.uuid().default_expr(" generateUUIDv4() ")
87+
modifiers = get_modifiers(validator)
88+
89+
assert modifiers.has_default is True
90+
assert modifiers.default_expression == "generateUUIDv4()"
91+
assert modifiers.default_value is None
92+
93+
94+
def test_generate_datasource_emits_unquoted_default_expression() -> None:
95+
datasource = define_datasource(
96+
"events",
97+
{
98+
"schema": {
99+
"id": t.uuid().default_expr("generateUUIDv4()"),
100+
}
101+
},
102+
)
103+
104+
generated = generate_datasource(datasource)
105+
assert "id UUID `json:$.id` DEFAULT generateUUIDv4()" in generated.content
106+
107+
108+
def test_type_validator_default_expr_rejects_empty_expression() -> None:
109+
with pytest.raises(ValueError, match="Default expression cannot be empty."):
110+
t.uuid().default_expr(" ")

tests/test_phase4_migrate_runner_emitter_parity.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,33 @@ def test_run_migrate_rejects_sink_connection_type_mismatch(tmp_path: Path) -> No
198198

199199
assert result.success is False
200200
assert any("is incompatible with connection" in error.message for error in result.errors)
201+
202+
203+
def test_run_migrate_emits_default_expr_for_sql_function_defaults(tmp_path: Path) -> None:
204+
(tmp_path / "events.datasource").write_text(
205+
"\n".join(
206+
[
207+
"SCHEMA >",
208+
" id UUID DEFAULT generateUUIDv4(),",
209+
" payload String DEFAULT '{}'",
210+
"",
211+
'ENGINE "MergeTree"',
212+
'ENGINE_SORTING_KEY "id"',
213+
]
214+
),
215+
encoding="utf-8",
216+
)
217+
218+
result = run_migrate(
219+
{
220+
"cwd": str(tmp_path),
221+
"patterns": ["*.datasource"],
222+
"dry_run": True,
223+
}
224+
)
225+
226+
assert result.success is True
227+
assert result.errors == []
228+
assert result.output_content is not None
229+
assert '\'id\': t.uuid().default_expr("generateUUIDv4()"),' in result.output_content
230+
assert "'payload': t.string().default('{}')," in result.output_content

0 commit comments

Comments
 (0)