Skip to content

Commit a5ee072

Browse files
committed
Merge branch 'main' into fix/6017-resolve-table-mapping
2 parents c0eecff + 2c30f83 commit a5ee072

20 files changed

Lines changed: 278 additions & 34 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
"postCreateCommand": "bash .devcontainer/post-create-command.sh",
1212
"customizations": {
1313
"vscode": {
14-
"extensions": [
15-
"ms-python.python",
16-
"ms-python.vscode-pylance"
17-
]
14+
"extensions": ["ms-python.python", "ms-python.vscode-pylance"]
1815
}
1916
},
2017
"remoteUser": "vscode"

.github/workflows/pr.yaml

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
outputs:
1717
python: ${{ steps.filter.outputs.python }}
1818
client: ${{ steps.filter.outputs.client }}
19+
vscode: ${{ steps.filter.outputs.vscode }}
1920
ci: ${{ steps.filter.outputs.ci }}
2021
steps:
2122
- uses: actions/checkout@v7
@@ -34,6 +35,8 @@ jobs:
3435
- 'pyproject.toml'
3536
client:
3637
- 'web/client/**'
38+
vscode:
39+
- 'vscode/**'
3740
ci:
3841
- '.github/**'
3942
- 'Makefile'
@@ -188,9 +191,10 @@ jobs:
188191

189192
ui-style:
190193
needs: [changes]
191-
if: false
192-
# needs.changes.outputs.client == 'true' || needs.changes.outputs.ci ==
193-
# 'true' || github.ref == 'refs/heads/main'
194+
if:
195+
needs.changes.outputs.client == 'true' || needs.changes.outputs.vscode ==
196+
'true' || needs.changes.outputs.ci == 'true' || github.ref ==
197+
'refs/heads/main'
194198
runs-on: ubuntu-latest
195199
steps:
196200
- uses: actions/checkout@v7
@@ -252,7 +256,17 @@ jobs:
252256
fail-fast: false
253257
matrix:
254258
engine:
255-
[duckdb, postgres, mysql, mssql, trino, spark, clickhouse, risingwave, starrocks]
259+
[
260+
duckdb,
261+
postgres,
262+
mysql,
263+
mssql,
264+
trino,
265+
spark,
266+
clickhouse,
267+
risingwave,
268+
starrocks,
269+
]
256270
env:
257271
PYTEST_XDIST_AUTO_NUM_WORKERS: 2
258272
SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: '1'
@@ -393,10 +407,13 @@ jobs:
393407
retention-days: 7
394408

395409
test-vscode:
410+
needs: changes
411+
if:
412+
needs.changes.outputs.vscode == 'true' || needs.changes.outputs.ci ==
413+
'true' || github.ref == 'refs/heads/main'
396414
env:
397415
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
398416
runs-on: ubuntu-latest
399-
if: false
400417
steps:
401418
- uses: actions/checkout@v7
402419
- uses: actions/setup-node@v7
@@ -457,7 +474,19 @@ jobs:
457474
strategy:
458475
fail-fast: false
459476
matrix:
460-
dbt-version: ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11', '1.12']
477+
dbt-version:
478+
[
479+
'1.3',
480+
'1.4',
481+
'1.5',
482+
'1.6',
483+
'1.7',
484+
'1.8',
485+
'1.9',
486+
'1.10',
487+
'1.11',
488+
'1.12',
489+
]
461490
steps:
462491
- uses: actions/checkout@v7
463492
- name: Set up Python

sqlmesh/core/engine_adapter/base.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,8 @@ def clone_table(
10941094
replace: bool = False,
10951095
exists: bool = True,
10961096
clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
1097+
table_format: t.Optional[str] = None,
1098+
table_kind: t.Optional[str] = None,
10971099
**kwargs: t.Any,
10981100
) -> None:
10991101
"""Creates a table with the target name by cloning the source table.
@@ -1103,6 +1105,10 @@ def clone_table(
11031105
source_table_name: The name of the source table that should be cloned.
11041106
replace: Whether or not to replace an existing table.
11051107
exists: Indicates whether to include the IF NOT EXISTS check.
1108+
clone_kwargs: Additional arguments for the CLONE clause.
1109+
table_format: The table format of the source table, if any. Engines that require
1110+
format-specific DDL to clone a table use it to derive `table_kind`.
1111+
table_kind: The kind of table to create. Defaults to `TABLE`.
11061112
"""
11071113
if not self.SUPPORTS_CLONING:
11081114
raise NotImplementedError(f"Engine does not support cloning: {type(self)}")
@@ -1111,7 +1117,7 @@ def clone_table(
11111117
self.execute(
11121118
exp.Create(
11131119
this=exp.to_table(target_table_name),
1114-
kind="TABLE",
1120+
kind=table_kind or "TABLE",
11151121
replace=replace,
11161122
exists=exists,
11171123
clone=exp.Clone(
@@ -1214,9 +1220,15 @@ def get_alter_operations(
12141220
def alter_table(
12151221
self,
12161222
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
1223+
table_format: t.Optional[str] = None,
12171224
) -> None:
12181225
"""
12191226
Performs the alter statements to change the current table into the structure of the target table.
1227+
1228+
Args:
1229+
alter_expressions: The alter operations to apply.
1230+
table_format: The table format of the target table, if any. Engines that require
1231+
format-specific DDL to alter a table use it to adjust the generated statements.
12201232
"""
12211233
with self.transaction():
12221234
for alter_expression in [

sqlmesh/core/engine_adapter/bigquery.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def create_mapping_schema(
405405
def alter_table(
406406
self,
407407
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
408+
table_format: t.Optional[str] = None,
408409
) -> None:
409410
"""
410411
Performs the alter statements to change the current table into the structure of the target table,

sqlmesh/core/engine_adapter/clickhouse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ def delete_from(self, table_name: TableName, where: t.Union[str, exp.Expr]) -> N
699699
def alter_table(
700700
self,
701701
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
702+
table_format: t.Optional[str] = None,
702703
) -> None:
703704
"""
704705
Performs the alter statements to change the current table into the structure of the target table.

sqlmesh/core/engine_adapter/databricks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ def clone_table(
386386
replace: bool = False,
387387
exists: bool = True,
388388
clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
389+
table_format: t.Optional[str] = None,
390+
table_kind: t.Optional[str] = None,
389391
**kwargs: t.Any,
390392
) -> None:
391393
clone_kwargs = clone_kwargs or {}
@@ -395,6 +397,8 @@ def clone_table(
395397
source_table_name,
396398
replace=replace,
397399
clone_kwargs=clone_kwargs,
400+
table_format=table_format,
401+
table_kind=table_kind,
398402
**kwargs,
399403
)
400404

sqlmesh/core/engine_adapter/fabric.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@ def set_current_catalog(self, catalog_name: t.Optional[str]) -> None:
225225
self._target_catalog = target_catalog
226226

227227
def alter_table(
228-
self, alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]]
228+
self,
229+
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
230+
table_format: t.Optional[str] = None,
229231
) -> None:
230232
"""
231233
Applies alter expressions to a table. Fabric has limited support for ALTER TABLE,

sqlmesh/core/engine_adapter/snowflake.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
SourceQuery,
2525
set_catalog,
2626
)
27+
from sqlmesh.core.schema_diff import TableAlterOperation
2728
from sqlmesh.utils import optional_import, get_source_columns_to_types
2829
from sqlmesh.utils.errors import SQLMeshError
2930
from sqlmesh.utils.pandas import columns_to_types_from_dtypes
@@ -667,6 +668,8 @@ def clone_table(
667668
replace: bool = False,
668669
exists: bool = True,
669670
clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
671+
table_format: t.Optional[str] = None,
672+
table_kind: t.Optional[str] = None,
670673
**kwargs: t.Any,
671674
) -> None:
672675
# The Snowflake adapter should use the transient property to clone transient tables
@@ -675,14 +678,43 @@ def clone_table(
675678
if isinstance(table_type, exp.TransientProperty):
676679
kwargs["properties"] = exp.Properties(expressions=[table_type])
677680

681+
# Snowflake rejects `CREATE TABLE ... CLONE` for Iceberg tables, it requires
682+
# `CREATE ICEBERG TABLE ... CLONE` instead
683+
if table_format and not table_kind:
684+
table_kind = f"{table_format.upper()} TABLE"
685+
678686
super().clone_table(
679687
target_table_name,
680688
source_table_name,
681689
replace=replace,
682690
clone_kwargs=clone_kwargs,
691+
table_kind=table_kind,
683692
**kwargs,
684693
)
685694

695+
def alter_table(
696+
self,
697+
alter_expressions: t.Union[t.List[exp.Alter], t.List[TableAlterOperation]],
698+
table_format: t.Optional[str] = None,
699+
) -> None:
700+
# Snowflake rejects `ALTER TABLE` for Iceberg tables, it requires
701+
# `ALTER ICEBERG TABLE` instead
702+
if table_format:
703+
table_kind = f"{table_format.upper()} TABLE"
704+
resolved_expressions = []
705+
for alter_expression in alter_expressions:
706+
resolved_expression = (
707+
alter_expression.expression
708+
if isinstance(alter_expression, TableAlterOperation)
709+
else alter_expression.copy()
710+
)
711+
resolved_expression.set("kind", table_kind)
712+
resolved_expressions.append(resolved_expression)
713+
714+
super().alter_table(resolved_expressions)
715+
else:
716+
super().alter_table(alter_expressions)
717+
686718
@t.overload
687719
def _columns_to_types(
688720
self,

sqlmesh/core/snapshot/evaluator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,7 @@ def _clone_snapshot_in_dev(
11041104
target_table_name,
11051105
snapshot.table_name(),
11061106
rendered_physical_properties=rendered_physical_properties,
1107+
table_format=snapshot.model.table_format,
11071108
)
11081109
self._migrate_target_table(
11091110
target_table_name=target_table_name,
@@ -2161,7 +2162,7 @@ def migrate(
21612162
_check_additive_schema_change(
21622163
snapshot, alter_operations, kwargs["allow_additive_snapshots"]
21632164
)
2164-
self.adapter.alter_table(alter_operations)
2165+
self.adapter.alter_table(alter_operations, table_format=snapshot.model.table_format)
21652166

21662167
# Apply grants after schema migration
21672168
deployability_index = kwargs.get("deployability_index")

sqlmesh/core/test/definition.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,9 @@ def assert_equal(
263263
for col, value in object_sentinel_values.items():
264264
try:
265265
# can't use `isinstance()` here - https://stackoverflow.com/a/68743663/1707525
266-
if type(value) is datetime.date:
267-
expected[col] = pd.to_datetime(expected[col]).dt.date
268-
elif type(value) is datetime.time:
269-
expected[col] = pd.to_datetime(expected[col]).dt.time
270-
elif type(value) is datetime.datetime:
271-
expected[col] = pd.to_datetime(expected[col]).dt.to_pydatetime()
266+
value_type = type(value)
267+
if value_type in (datetime.date, datetime.time, datetime.datetime):
268+
expected[col] = _parse_expected_datetime_column(expected[col], value_type)
272269
except Exception as e:
273270
from sqlmesh.core.console import get_console
274271

@@ -1014,6 +1011,34 @@ def _raise_error(msg: str, path: Path | None = None) -> None:
10141011
raise TestError(f"Failed to run test:\n{msg}")
10151012

10161013

1014+
def _parse_expected_datetime_column(series: pd.Series, target_type: type) -> pd.Series:
1015+
"""Convert a series of expected values to python ``date``/``time``/``datetime``.
1016+
1017+
Falls back to microsecond resolution when pandas' default nanosecond
1018+
parsing overflows. SQL ``TIMESTAMP`` columns can carry values outside
1019+
pandas' default ``datetime64[ns]`` range (1677-09-21..2262-04-11), so
1020+
unit tests may compare against values like ``0001-01-01`` which are
1021+
valid in the database but overflow the default resolution.
1022+
"""
1023+
import pandas as pd
1024+
from pandas.errors import OutOfBoundsDatetime
1025+
1026+
try:
1027+
parsed = pd.to_datetime(series)
1028+
except OutOfBoundsDatetime:
1029+
parsed = series.astype("datetime64[us]")
1030+
1031+
if target_type is datetime.date:
1032+
return parsed.dt.date
1033+
if target_type is datetime.time:
1034+
return parsed.dt.time
1035+
# `Series.dt.to_pydatetime()` returns an `ndarray` in pandas 2.x. Wrap it in a
1036+
# Series with ``dtype=object`` so pandas does not coerce the values back to
1037+
# ``pd.Timestamp`` (which would reintroduce the nanosecond overflow this
1038+
# function exists to avoid).
1039+
return pd.Series(parsed.dt.to_pydatetime(), index=parsed.index, dtype="object")
1040+
1041+
10171042
def _normalize_df_value(value: t.Any) -> t.Any:
10181043
"""Normalize data in a pandas dataframe so ruamel and sqlglot can deal with it."""
10191044
import numpy as np

0 commit comments

Comments
 (0)