Skip to content

Commit 8d5d96e

Browse files
authored
feat: support bigquery no op coerce types (#1566)
1 parent 6d65e6b commit 8d5d96e

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

sqlmesh/core/engine_adapter/bigquery.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,20 @@ class BigQueryEngineAdapter(InsertOverwriteWithMergeMixin):
5454
exp.DataType.build("INT64", dialect=DIALECT): {
5555
exp.DataType.build("NUMERIC", dialect=DIALECT),
5656
exp.DataType.build("FLOAT64", dialect=DIALECT),
57+
exp.DataType.build("BIGNUMERIC", dialect=DIALECT),
5758
},
5859
exp.DataType.build("NUMERIC", dialect=DIALECT): {
5960
exp.DataType.build("FLOAT64", dialect=DIALECT),
61+
exp.DataType.build("BIGNUMERIC", dialect=DIALECT),
62+
},
63+
exp.DataType.build("BIGNUMERIC", dialect=DIALECT): {
64+
exp.DataType.build("FLOAT64", dialect=DIALECT),
6065
},
6166
exp.DataType.build("DATE", dialect=DIALECT): {
6267
exp.DataType.build("DATETIME", dialect=DIALECT),
6368
},
6469
},
70+
support_coercing_compatible_types=True,
6571
)
6672

6773
@property

sqlmesh/core/schema_diff.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
from __future__ import annotations
22

3+
import logging
34
import typing as t
5+
from collections import defaultdict
46
from enum import Enum, auto
57

68
from sqlglot import exp
79
from sqlglot.helper import ensure_list, seq_get
810

911
from sqlmesh.utils.pydantic import PydanticModel
1012

13+
if t.TYPE_CHECKING:
14+
from sqlmesh.core._typing import TableName
15+
16+
logger = logging.getLogger(__name__)
17+
1118

1219
class TableAlterOperationType(Enum):
1320
ADD = auto()
@@ -292,6 +299,9 @@ class SchemaDiffer(PydanticModel):
292299
support_nested_operations: bool = False
293300
array_element_selector: str = ""
294301
compatible_types: t.Dict[exp.DataType, t.Set[exp.DataType]] = {}
302+
support_coercing_compatible_types: bool = False
303+
304+
_coerceable_types: t.Dict[exp.DataType, t.Set[exp.DataType]] = {}
295305

296306
@classmethod
297307
def _dict_to_struct(cls, value: t.Dict[str, exp.DataType]) -> exp.DataType:
@@ -303,13 +313,37 @@ def _dict_to_struct(cls, value: t.Dict[str, exp.DataType]) -> exp.DataType:
303313
nested=True,
304314
)
305315

316+
@property
317+
def coerceable_types(self) -> t.Dict[exp.DataType, t.Set[exp.DataType]]:
318+
if not self._coerceable_types:
319+
if not self.support_coercing_compatible_types or not self.compatible_types:
320+
return {}
321+
coerceable_types = defaultdict(set)
322+
for source_type, target_types in self.compatible_types.items():
323+
for target_type in target_types:
324+
coerceable_types[target_type].add(source_type)
325+
self._coerceable_types = coerceable_types
326+
return self._coerceable_types
327+
306328
def _is_compatible_type(self, current_type: exp.DataType, new_type: exp.DataType) -> bool:
307329
if current_type == new_type:
308330
return True
309331
if current_type in self.compatible_types:
310332
return new_type in self.compatible_types[current_type]
311333
return False
312334

335+
def _is_coerceable_type(self, current_type: exp.DataType, new_type: exp.DataType) -> bool:
336+
if not self.support_coercing_compatible_types:
337+
return False
338+
if current_type in self.coerceable_types:
339+
is_coerceable = new_type in self.coerceable_types[current_type]
340+
if is_coerceable:
341+
logger.warning(
342+
f"Coercing type {current_type} to {new_type} which means an alter will not be performed and therefore the resulting table structure will not match what is in the query.\nUpdate your model to cast the value to {current_type} type in order to remove this warning.",
343+
)
344+
return is_coerceable
345+
return False
346+
313347
def _get_matching_kwarg(
314348
self,
315349
current_kwarg: t.Union[str, exp.ColumnDef],
@@ -438,7 +472,9 @@ def _alter_operation(
438472
new_array_type,
439473
root_struct,
440474
)
441-
if self._is_compatible_type(current_type, new_type):
475+
if self._is_coerceable_type(current_type, new_type):
476+
return []
477+
elif self._is_compatible_type(current_type, new_type):
442478
struct.expressions.pop(pos)
443479
struct.expressions.insert(pos, new_kwarg)
444480
col_pos = (
@@ -535,7 +571,7 @@ def compare_structs(
535571

536572
def compare_columns(
537573
self,
538-
table_name: t.Union[str, exp.Table],
574+
table_name: TableName,
539575
current: t.Dict[str, exp.DataType],
540576
new: t.Dict[str, exp.DataType],
541577
) -> t.List[exp.AlterTable]:

tests/core/test_schema_diff.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,46 @@ def test_schema_diff_calculate_type_transitions():
688688
support_nested_operations=True,
689689
),
690690
),
691+
# ############
692+
# Coerce Tests
693+
# ############
694+
# Single coercion results in no operation
695+
(
696+
"STRUCT<id INT, name STRING, revenue FLOAT>",
697+
"STRUCT<id INT, name STRING, revenue INT>",
698+
[],
699+
dict(
700+
support_positional_add=True,
701+
support_nested_operations=True,
702+
support_coercing_compatible_types=True,
703+
compatible_types={
704+
exp.DataType.build("INT"): {exp.DataType.build("FLOAT")},
705+
},
706+
),
707+
),
708+
# Coercion with an alter results in a single alter
709+
(
710+
"STRUCT<id INT, name STRING, revenue FLOAT, total INT>",
711+
"STRUCT<id INT, name STRING, revenue INT, total FLOAT>",
712+
[
713+
TableAlterOperation.alter_type(
714+
TableAlterColumn.primitive("total"),
715+
"FLOAT",
716+
current_type="INT",
717+
# Note that the resulting table struct will not match what we defined as the desired
718+
# result since it could be coerced
719+
expected_table_struct="STRUCT<id INT, name STRING, revenue FLOAT, total FLOAT>",
720+
)
721+
],
722+
dict(
723+
support_positional_add=False,
724+
support_nested_operations=True,
725+
support_coercing_compatible_types=True,
726+
compatible_types={
727+
exp.DataType.build("INT"): {exp.DataType.build("FLOAT")},
728+
},
729+
),
730+
),
691731
],
692732
)
693733
def test_struct_diff(

0 commit comments

Comments
 (0)