sqlmesh 0.184.2.dev2__py3-none-any.whl → 0.184.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlmesh/_version.py +2 -2
- sqlmesh/cli/main.py +5 -0
- sqlmesh/core/config/connection.py +52 -0
- sqlmesh/core/console.py +10 -6
- sqlmesh/core/context.py +17 -9
- sqlmesh/core/engine_adapter/athena.py +5 -3
- sqlmesh/core/engine_adapter/snowflake.py +18 -3
- sqlmesh/core/macros.py +15 -7
- sqlmesh/core/model/cache.py +4 -1
- sqlmesh/core/plan/evaluator.py +3 -1
- sqlmesh/integrations/github/cicd/controller.py +7 -4
- sqlmesh/lsp/main.py +8 -6
- sqlmesh/magics.py +6 -0
- sqlmesh/utils/cache.py +6 -1
- sqlmesh/utils/windows.py +14 -0
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/METADATA +1 -1
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/RECORD +69 -68
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/WHEEL +1 -1
- web/client/dist/assets/{Audits-DF7amJxU.js → Audits-cGDtiiKB.js} +1 -1
- web/client/dist/assets/{Banner-C3VTntGq.js → Banner-DHp1YP1u.js} +1 -1
- web/client/dist/assets/{ChevronDownIcon-qm4bu8e3.js → ChevronDownIcon-Sazvo9i-.js} +1 -1
- web/client/dist/assets/{ChevronRightIcon-BolEdhGf.js → ChevronRightIcon-84R-3q8m.js} +1 -1
- web/client/dist/assets/{Content-B6xcwedd.js → Content-B9yV2iEZ.js} +1 -1
- web/client/dist/assets/{Content-BRVWZ3mb.js → Content-C65MWvF0.js} +1 -1
- web/client/dist/assets/{Data-Biu-V0fH.js → Data-By25O4M5.js} +1 -1
- web/client/dist/assets/{DataCatalog-B6evo1XW.js → DataCatalog-hSS21hR6.js} +1 -1
- web/client/dist/assets/{Editor-CVxCL2-Y.js → Editor-Bsxwi3aO.js} +1 -1
- web/client/dist/assets/{Editor-CLmFVr2P.js → Editor-CUCC89Ij.js} +1 -1
- web/client/dist/assets/{Errors-oZskb8PG.js → Errors-BOCVwZyl.js} +1 -1
- web/client/dist/assets/{FileExplorer-CGr1vJjw.js → FileExplorer-BZ8API0P.js} +1 -1
- web/client/dist/assets/{Footer-fJbe0D4K.js → Footer-CUwjtSu4.js} +1 -1
- web/client/dist/assets/{Header-BH87N9ET.js → Header-LsiS75Z7.js} +1 -1
- web/client/dist/assets/Input-D0tI9yL5.js +41 -0
- web/client/dist/assets/Lineage-DWpcQYIS.js +1 -0
- web/client/dist/assets/{ListboxShow-BWNLIBMH.js → ListboxShow-DF5PqZhj.js} +1 -1
- web/client/dist/assets/{ModelLineage-c5SOHuSl.js → ModelLineage-BHZN6cbe.js} +1 -1
- web/client/dist/assets/{Models-BNHmcp1o.js → Models-y5gl8Oaf.js} +1 -1
- web/client/dist/assets/{Page-daP33Ypg.js → Page-DbTpD-wK.js} +1 -1
- web/client/dist/assets/{Plan-DUHa3QdG.js → Plan-CiSxSXLS.js} +1 -1
- web/client/dist/assets/{PlusCircleIcon-Bk7UCkgh.js → PlusCircleIcon-DJoyPo8S.js} +1 -1
- web/client/dist/assets/{ReportErrors-DfzGQ2eY.js → ReportErrors-CNXH4GuU.js} +1 -1
- web/client/dist/assets/{Root-uPpx7ocJ.js → Root-RYhutnyD.js} +1 -1
- web/client/dist/assets/{SearchList-87imxiuX.js → SearchList-iv5ny7Mb.js} +1 -1
- web/client/dist/assets/{SelectEnvironment-DRt5ys9a.js → SelectEnvironment-DSHK4OKG.js} +1 -1
- web/client/dist/assets/{SourceList-Cfd2Hs4e.js → SourceList-BXrKA-hR.js} +1 -1
- web/client/dist/assets/{SourceListItem-Cgi7ENFV.js → SourceListItem-BvVyXARN.js} +1 -1
- web/client/dist/assets/{SplitPane-p-JrHTOr.js → SplitPane-BCBwk2UE.js} +1 -1
- web/client/dist/assets/{Tests-Das-Vk4n.js → Tests-BOygb2O9.js} +1 -1
- web/client/dist/assets/{Welcome-33aFy-Y-.js → Welcome-saOWnjhW.js} +1 -1
- web/client/dist/assets/{context-NpzoJDLp.js → context-Cl_qdjcd.js} +1 -1
- web/client/dist/assets/{context-C5ccds54.js → context-cGGhLbGj.js} +1 -1
- web/client/dist/assets/{editor-B8OcqW2l.js → editor-yKXGVuvn.js} +1 -1
- web/client/dist/assets/{file-D-wqO8gv.js → file-CJ8A1f27.js} +1 -1
- web/client/dist/assets/{floating-ui.react-dom-BrhHyVvs.js → floating-ui.react-dom-CfXRycK_.js} +1 -1
- web/client/dist/assets/{help-CQWPRUV2.js → help-aam8B-7S.js} +1 -1
- web/client/dist/assets/{index-xGKh539i.js → index-CQy7Tkrl.js} +2 -2
- web/client/dist/assets/{index-Co-_3xPI.js → index-zO-UPMUq.js} +1 -1
- web/client/dist/assets/{plan-BIz0iaor.js → plan-CiAasU-P.js} +1 -1
- web/client/dist/assets/{popover-BXDPVmB7.js → popover-Ox7TJJ2-.js} +1 -1
- web/client/dist/assets/{project-H8d6rukl.js → project-PqqAyMWE.js} +1 -1
- web/client/dist/index.html +1 -1
- web/server/api/endpoints/commands.py +12 -5
- web/server/api/endpoints/files.py +2 -2
- web/server/api/endpoints/models.py +2 -2
- web/server/models.py +1 -1
- web/server/utils.py +11 -0
- web/client/dist/assets/Input-D4nBSMWJ.js +0 -41
- web/client/dist/assets/Lineage-CPutJH7u.js +0 -1
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/top_level.txt +0 -0
sqlmesh/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.184.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 184,
|
|
20
|
+
__version__ = version = '0.184.3'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 184, 3)
|
sqlmesh/cli/main.py
CHANGED
|
@@ -906,6 +906,11 @@ def create_external_models(obj: Context, **kwargs: t.Any) -> None:
|
|
|
906
906
|
is_flag=True,
|
|
907
907
|
help="Disable the check for a primary key (grain) that is missing or is not unique.",
|
|
908
908
|
)
|
|
909
|
+
@click.option(
|
|
910
|
+
"--warn-grain-check",
|
|
911
|
+
is_flag=True,
|
|
912
|
+
help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.",
|
|
913
|
+
)
|
|
909
914
|
@click.option(
|
|
910
915
|
"--temp-schema",
|
|
911
916
|
type=str,
|
|
@@ -4,6 +4,7 @@ import abc
|
|
|
4
4
|
import base64
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
import importlib
|
|
7
8
|
import pathlib
|
|
8
9
|
import re
|
|
9
10
|
import typing as t
|
|
@@ -24,6 +25,7 @@ from sqlmesh.core.config.common import (
|
|
|
24
25
|
)
|
|
25
26
|
from sqlmesh.core.engine_adapter.shared import CatalogSupport
|
|
26
27
|
from sqlmesh.core.engine_adapter import EngineAdapter
|
|
28
|
+
from sqlmesh.utils import str_to_bool
|
|
27
29
|
from sqlmesh.utils.errors import ConfigError
|
|
28
30
|
from sqlmesh.utils.pydantic import (
|
|
29
31
|
ValidationInfo,
|
|
@@ -49,6 +51,30 @@ FORBIDDEN_STATE_SYNC_ENGINES = {
|
|
|
49
51
|
MOTHERDUCK_TOKEN_REGEX = re.compile(r"(\?|\&)(motherduck_token=)(\S*)")
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
def _get_engine_import_validator(
|
|
55
|
+
import_name: str, engine_type: str, extra_name: t.Optional[str] = None
|
|
56
|
+
) -> t.Callable:
|
|
57
|
+
extra_name = extra_name or engine_type
|
|
58
|
+
|
|
59
|
+
@model_validator(mode="before")
|
|
60
|
+
def validate(cls: t.Any, data: t.Any) -> t.Any:
|
|
61
|
+
check_import = (
|
|
62
|
+
str_to_bool(str(data.pop("check_import", True))) if isinstance(data, dict) else True
|
|
63
|
+
)
|
|
64
|
+
if not check_import:
|
|
65
|
+
return data
|
|
66
|
+
try:
|
|
67
|
+
importlib.import_module(import_name)
|
|
68
|
+
except ImportError:
|
|
69
|
+
raise ConfigError(
|
|
70
|
+
f"Failed to import the '{engine_type}' engine library. Please run `pip install \"sqlmesh[{extra_name}]\"`."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
return validate
|
|
76
|
+
|
|
77
|
+
|
|
52
78
|
class ConnectionConfig(abc.ABC, BaseConfig):
|
|
53
79
|
type_: str
|
|
54
80
|
concurrent_tasks: int
|
|
@@ -428,6 +454,7 @@ class SnowflakeConnectionConfig(ConnectionConfig):
|
|
|
428
454
|
type_: t.Literal["snowflake"] = Field(alias="type", default="snowflake")
|
|
429
455
|
|
|
430
456
|
_concurrent_tasks_validator = concurrent_tasks_validator
|
|
457
|
+
_engine_import_validator = _get_engine_import_validator("snowflake", "snowflake")
|
|
431
458
|
|
|
432
459
|
@model_validator(mode="before")
|
|
433
460
|
def _validate_authenticator(cls, data: t.Any) -> t.Any:
|
|
@@ -621,6 +648,7 @@ class DatabricksConnectionConfig(ConnectionConfig):
|
|
|
621
648
|
|
|
622
649
|
_concurrent_tasks_validator = concurrent_tasks_validator
|
|
623
650
|
_http_headers_validator = http_headers_validator
|
|
651
|
+
_engine_import_validator = _get_engine_import_validator("databricks", "databricks")
|
|
624
652
|
|
|
625
653
|
@model_validator(mode="before")
|
|
626
654
|
def _databricks_connect_validator(cls, data: t.Any) -> t.Any:
|
|
@@ -873,6 +901,8 @@ class BigQueryConnectionConfig(ConnectionConfig):
|
|
|
873
901
|
|
|
874
902
|
type_: t.Literal["bigquery"] = Field(alias="type", default="bigquery")
|
|
875
903
|
|
|
904
|
+
_engine_import_validator = _get_engine_import_validator("google.cloud.bigquery", "bigquery")
|
|
905
|
+
|
|
876
906
|
@field_validator("execution_project")
|
|
877
907
|
def validate_execution_project(
|
|
878
908
|
cls,
|
|
@@ -1015,6 +1045,10 @@ class GCPPostgresConnectionConfig(ConnectionConfig):
|
|
|
1015
1045
|
register_comments: bool = True
|
|
1016
1046
|
pre_ping: bool = True
|
|
1017
1047
|
|
|
1048
|
+
_engine_import_validator = _get_engine_import_validator(
|
|
1049
|
+
"google.cloud.sql", "gcp_postgres", "gcppostgres"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1018
1052
|
@model_validator(mode="before")
|
|
1019
1053
|
def _validate_auth_method(cls, data: t.Any) -> t.Any:
|
|
1020
1054
|
if not isinstance(data, dict):
|
|
@@ -1142,6 +1176,8 @@ class RedshiftConnectionConfig(ConnectionConfig):
|
|
|
1142
1176
|
|
|
1143
1177
|
type_: t.Literal["redshift"] = Field(alias="type", default="redshift")
|
|
1144
1178
|
|
|
1179
|
+
_engine_import_validator = _get_engine_import_validator("redshift_connector", "redshift")
|
|
1180
|
+
|
|
1145
1181
|
@property
|
|
1146
1182
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1147
1183
|
return {
|
|
@@ -1201,6 +1237,8 @@ class PostgresConnectionConfig(ConnectionConfig):
|
|
|
1201
1237
|
|
|
1202
1238
|
type_: t.Literal["postgres"] = Field(alias="type", default="postgres")
|
|
1203
1239
|
|
|
1240
|
+
_engine_import_validator = _get_engine_import_validator("psycopg2", "postgres")
|
|
1241
|
+
|
|
1204
1242
|
@property
|
|
1205
1243
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1206
1244
|
return {
|
|
@@ -1252,6 +1290,8 @@ class MySQLConnectionConfig(ConnectionConfig):
|
|
|
1252
1290
|
|
|
1253
1291
|
type_: t.Literal["mysql"] = Field(alias="type", default="mysql")
|
|
1254
1292
|
|
|
1293
|
+
_engine_import_validator = _get_engine_import_validator("pymysql", "mysql")
|
|
1294
|
+
|
|
1255
1295
|
@property
|
|
1256
1296
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1257
1297
|
connection_keys = {
|
|
@@ -1302,6 +1342,8 @@ class MSSQLConnectionConfig(ConnectionConfig):
|
|
|
1302
1342
|
|
|
1303
1343
|
type_: t.Literal["mssql"] = Field(alias="type", default="mssql")
|
|
1304
1344
|
|
|
1345
|
+
_engine_import_validator = _get_engine_import_validator("pymssql", "mssql")
|
|
1346
|
+
|
|
1305
1347
|
@property
|
|
1306
1348
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1307
1349
|
return {
|
|
@@ -1357,6 +1399,8 @@ class SparkConnectionConfig(ConnectionConfig):
|
|
|
1357
1399
|
|
|
1358
1400
|
type_: t.Literal["spark"] = Field(alias="type", default="spark")
|
|
1359
1401
|
|
|
1402
|
+
_engine_import_validator = _get_engine_import_validator("pyspark", "spark")
|
|
1403
|
+
|
|
1360
1404
|
@property
|
|
1361
1405
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1362
1406
|
return {
|
|
@@ -1473,6 +1517,8 @@ class TrinoConnectionConfig(ConnectionConfig):
|
|
|
1473
1517
|
|
|
1474
1518
|
type_: t.Literal["trino"] = Field(alias="type", default="trino")
|
|
1475
1519
|
|
|
1520
|
+
_engine_import_validator = _get_engine_import_validator("trino", "trino")
|
|
1521
|
+
|
|
1476
1522
|
@field_validator("schema_location_mapping", mode="before")
|
|
1477
1523
|
@classmethod
|
|
1478
1524
|
def _validate_regex_keys(
|
|
@@ -1623,6 +1669,8 @@ class ClickhouseConnectionConfig(ConnectionConfig):
|
|
|
1623
1669
|
|
|
1624
1670
|
type_: t.Literal["clickhouse"] = Field(alias="type", default="clickhouse")
|
|
1625
1671
|
|
|
1672
|
+
_engine_import_validator = _get_engine_import_validator("clickhouse_connect", "clickhouse")
|
|
1673
|
+
|
|
1626
1674
|
@property
|
|
1627
1675
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1628
1676
|
kwargs = {
|
|
@@ -1727,6 +1775,8 @@ class AthenaConnectionConfig(ConnectionConfig):
|
|
|
1727
1775
|
|
|
1728
1776
|
type_: t.Literal["athena"] = Field(alias="type", default="athena")
|
|
1729
1777
|
|
|
1778
|
+
_engine_import_validator = _get_engine_import_validator("pyathena", "athena")
|
|
1779
|
+
|
|
1730
1780
|
@model_validator(mode="after")
|
|
1731
1781
|
def _root_validator(self) -> Self:
|
|
1732
1782
|
work_group = self.work_group
|
|
@@ -1793,6 +1843,8 @@ class RisingwaveConnectionConfig(ConnectionConfig):
|
|
|
1793
1843
|
|
|
1794
1844
|
type_: t.Literal["risingwave"] = Field(alias="type", default="risingwave")
|
|
1795
1845
|
|
|
1846
|
+
_engine_import_validator = _get_engine_import_validator("psycopg2", "risingwave")
|
|
1847
|
+
|
|
1796
1848
|
@property
|
|
1797
1849
|
def _connection_kwargs_keys(self) -> t.Set[str]:
|
|
1798
1850
|
return {
|
sqlmesh/core/console.py
CHANGED
|
@@ -1161,7 +1161,7 @@ class TerminalConsole(Console):
|
|
|
1161
1161
|
default_catalog: t.Optional[str],
|
|
1162
1162
|
) -> None:
|
|
1163
1163
|
"""Indicates that a new snapshot promotion progress has begun."""
|
|
1164
|
-
if self.promotion_progress is None:
|
|
1164
|
+
if snapshots and self.promotion_progress is None:
|
|
1165
1165
|
self.promotion_progress = make_progress_bar(
|
|
1166
1166
|
"Updating virtual layer ", self.console, justify="left"
|
|
1167
1167
|
)
|
|
@@ -1782,10 +1782,13 @@ class TerminalConsole(Console):
|
|
|
1782
1782
|
"""Get the user's change category for the directly modified models."""
|
|
1783
1783
|
plan = plan_builder.build()
|
|
1784
1784
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1785
|
+
if plan.restatements:
|
|
1786
|
+
self._print("\n[bold]Restating models\n")
|
|
1787
|
+
else:
|
|
1788
|
+
self.show_environment_difference_summary(
|
|
1789
|
+
plan.context_diff,
|
|
1790
|
+
no_diff=no_diff,
|
|
1791
|
+
)
|
|
1789
1792
|
|
|
1790
1793
|
if plan.context_diff.has_changes:
|
|
1791
1794
|
self.show_model_difference_summary(
|
|
@@ -3458,7 +3461,8 @@ class DebuggerTerminalConsole(TerminalConsole):
|
|
|
3458
3461
|
environment_naming_info: EnvironmentNamingInfo,
|
|
3459
3462
|
default_catalog: t.Optional[str],
|
|
3460
3463
|
) -> None:
|
|
3461
|
-
|
|
3464
|
+
if snapshots:
|
|
3465
|
+
self._write(f"Starting promotion for {len(snapshots)} snapshots")
|
|
3462
3466
|
|
|
3463
3467
|
def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None:
|
|
3464
3468
|
self._write(f"Promoting {snapshot.name}")
|
sqlmesh/core/context.py
CHANGED
|
@@ -1629,6 +1629,7 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1629
1629
|
show_sample: bool = True,
|
|
1630
1630
|
decimals: int = 3,
|
|
1631
1631
|
skip_grain_check: bool = False,
|
|
1632
|
+
warn_grain_check: bool = False,
|
|
1632
1633
|
temp_schema: t.Optional[str] = None,
|
|
1633
1634
|
) -> t.List[TableDiff]:
|
|
1634
1635
|
"""Show a diff between two tables.
|
|
@@ -1703,22 +1704,29 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
1703
1704
|
target_env.naming_info, adapter.dialect
|
|
1704
1705
|
)
|
|
1705
1706
|
model_on = on or model.on
|
|
1706
|
-
models_to_diff.append((model, adapter, source, target, model_on))
|
|
1707
1707
|
if not model_on:
|
|
1708
1708
|
models_without_grain.append(model)
|
|
1709
|
+
else:
|
|
1710
|
+
models_to_diff.append((model, adapter, source, target, model_on))
|
|
1711
|
+
|
|
1712
|
+
if models_without_grain:
|
|
1713
|
+
model_names = "\n".join(
|
|
1714
|
+
f"─ {model.name} \n at '{model._path}'" for model in models_without_grain
|
|
1715
|
+
)
|
|
1716
|
+
message = (
|
|
1717
|
+
"SQLMesh doesn't know how to join the tables for the following models:\n"
|
|
1718
|
+
f"{model_names}\n\n"
|
|
1719
|
+
"Please specify a `grain` in each model definition. It must be unique and not null."
|
|
1720
|
+
)
|
|
1721
|
+
if warn_grain_check:
|
|
1722
|
+
self.console.log_warning(message)
|
|
1723
|
+
else:
|
|
1724
|
+
raise SQLMeshError(message)
|
|
1709
1725
|
|
|
1710
1726
|
if models_to_diff:
|
|
1711
1727
|
self.console.show_table_diff_details(
|
|
1712
1728
|
[model[0].name for model in models_to_diff],
|
|
1713
1729
|
)
|
|
1714
|
-
if models_without_grain:
|
|
1715
|
-
model_names = "\n".join(
|
|
1716
|
-
f"─ {model.name} \n at '{model._path}'" for model in models_without_grain
|
|
1717
|
-
)
|
|
1718
|
-
raise SQLMeshError(
|
|
1719
|
-
f"SQLMesh doesn't know how to join the tables for the following models:\n{model_names}\n"
|
|
1720
|
-
"\nPlease specify the `grain` in each model definition. Must be unique and not null."
|
|
1721
|
-
)
|
|
1722
1730
|
|
|
1723
1731
|
self.console.start_table_diff_progress(len(models_to_diff))
|
|
1724
1732
|
try:
|
|
@@ -8,7 +8,7 @@ from sqlmesh.utils.aws import validate_s3_uri, parse_s3_uri
|
|
|
8
8
|
from sqlmesh.core.engine_adapter.mixins import PandasNativeFetchDFSupportMixin, RowDiffMixin
|
|
9
9
|
from sqlmesh.core.engine_adapter.trino import TrinoEngineAdapter
|
|
10
10
|
from sqlmesh.core.node import IntervalUnit
|
|
11
|
-
import
|
|
11
|
+
import posixpath
|
|
12
12
|
from sqlmesh.utils.errors import SQLMeshError
|
|
13
13
|
from sqlmesh.core.engine_adapter.shared import (
|
|
14
14
|
CatalogSupport,
|
|
@@ -403,11 +403,13 @@ class AthenaEngineAdapter(PandasNativeFetchDFSupportMixin, RowDiffMixin):
|
|
|
403
403
|
|
|
404
404
|
elif self.s3_warehouse_location:
|
|
405
405
|
# If the user has set `s3_warehouse_location` in the connection config, the base URI is <s3_warehouse_location>/<catalog>/<schema>/
|
|
406
|
-
base_uri =
|
|
406
|
+
base_uri = posixpath.join(
|
|
407
|
+
self.s3_warehouse_location, table.catalog or "", table.db or ""
|
|
408
|
+
)
|
|
407
409
|
else:
|
|
408
410
|
return None
|
|
409
411
|
|
|
410
|
-
full_uri = validate_s3_uri(
|
|
412
|
+
full_uri = validate_s3_uri(posixpath.join(base_uri, table.text("this") or ""), base=True)
|
|
411
413
|
return exp.LocationProperty(this=exp.Literal.string(full_uri))
|
|
412
414
|
|
|
413
415
|
def _find_matching_columns(
|
|
@@ -68,6 +68,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
68
68
|
},
|
|
69
69
|
)
|
|
70
70
|
MANAGED_TABLE_KIND = "DYNAMIC TABLE"
|
|
71
|
+
SNOWPARK = "snowpark"
|
|
71
72
|
|
|
72
73
|
@contextlib.contextmanager
|
|
73
74
|
def session(self, properties: SessionProperties) -> t.Iterator[None]:
|
|
@@ -104,9 +105,16 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
104
105
|
@property
|
|
105
106
|
def snowpark(self) -> t.Optional[SnowparkSession]:
|
|
106
107
|
if snowpark:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if not self._connection_pool.get_attribute(self.SNOWPARK):
|
|
109
|
+
# Snowpark sessions are not thread safe so we create a session per thread to prevent them from interfering with each other
|
|
110
|
+
# The sessions are cleaned up when close() is called
|
|
111
|
+
new_session = snowpark.Session.builder.configs(
|
|
112
|
+
{"connection": self._connection_pool.get()}
|
|
113
|
+
).create()
|
|
114
|
+
self._connection_pool.set_attribute(self.SNOWPARK, new_session)
|
|
115
|
+
|
|
116
|
+
return self._connection_pool.get_attribute(self.SNOWPARK)
|
|
117
|
+
|
|
110
118
|
return None
|
|
111
119
|
|
|
112
120
|
@property
|
|
@@ -584,3 +592,10 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi
|
|
|
584
592
|
return columns_to_types_from_dtypes(query_or_df.sample(n=1).to_pandas().dtypes.items())
|
|
585
593
|
|
|
586
594
|
return super()._columns_to_types(query_or_df, columns_to_types)
|
|
595
|
+
|
|
596
|
+
def close(self) -> t.Any:
|
|
597
|
+
if snowpark_session := self._connection_pool.get_attribute(self.SNOWPARK):
|
|
598
|
+
snowpark_session.close() # type: ignore
|
|
599
|
+
self._connection_pool.set_attribute(self.SNOWPARK, None)
|
|
600
|
+
|
|
601
|
+
return super().close()
|
sqlmesh/core/macros.py
CHANGED
|
@@ -1090,10 +1090,10 @@ def pivot(
|
|
|
1090
1090
|
column: SQL,
|
|
1091
1091
|
values: t.List[SQL],
|
|
1092
1092
|
alias: bool = True,
|
|
1093
|
-
agg:
|
|
1094
|
-
cmp:
|
|
1095
|
-
prefix:
|
|
1096
|
-
suffix:
|
|
1093
|
+
agg: exp.Expression = exp.Literal.string("SUM"),
|
|
1094
|
+
cmp: exp.Expression = exp.Literal.string("="),
|
|
1095
|
+
prefix: exp.Expression = exp.Literal.string(""),
|
|
1096
|
+
suffix: exp.Expression = exp.Literal.string(""),
|
|
1097
1097
|
then_value: SQL = SQL("1"),
|
|
1098
1098
|
else_value: SQL = SQL("0"),
|
|
1099
1099
|
quote: bool = True,
|
|
@@ -1107,18 +1107,26 @@ def pivot(
|
|
|
1107
1107
|
>>> sql = "SELECT date_day, @PIVOT(status, ['cancelled', 'completed']) FROM rides GROUP BY 1"
|
|
1108
1108
|
>>> MacroEvaluator().transform(parse_one(sql)).sql()
|
|
1109
1109
|
'SELECT date_day, SUM(CASE WHEN status = \\'cancelled\\' THEN 1 ELSE 0 END) AS "\\'cancelled\\'", SUM(CASE WHEN status = \\'completed\\' THEN 1 ELSE 0 END) AS "\\'completed\\'" FROM rides GROUP BY 1'
|
|
1110
|
+
>>> sql = "SELECT @PIVOT(a, ['v'], then_value := tv, suffix := '_sfx', quote := FALSE)"
|
|
1111
|
+
>>> MacroEvaluator(dialect="bigquery").transform(parse_one(sql)).sql("bigquery")
|
|
1112
|
+
"SELECT SUM(CASE WHEN a = 'v' THEN tv ELSE 0 END) AS `v_sfx`"
|
|
1110
1113
|
"""
|
|
1111
1114
|
aggregates: t.List[exp.Expression] = []
|
|
1112
1115
|
for value in values:
|
|
1113
|
-
proj = f"{agg}("
|
|
1116
|
+
proj = f"{agg.name}("
|
|
1114
1117
|
if distinct:
|
|
1115
1118
|
proj += "DISTINCT "
|
|
1116
1119
|
|
|
1117
|
-
proj += f"CASE WHEN {column} {cmp} {value} THEN {then_value} ELSE {else_value} END) "
|
|
1120
|
+
proj += f"CASE WHEN {column} {cmp.name} {value} THEN {then_value} ELSE {else_value} END) "
|
|
1118
1121
|
node = evaluator.parse_one(proj)
|
|
1119
1122
|
|
|
1120
1123
|
if alias:
|
|
1121
|
-
node = node.as_(
|
|
1124
|
+
node = node.as_(
|
|
1125
|
+
f"{prefix.name}{value}{suffix.name}",
|
|
1126
|
+
quoted=quote,
|
|
1127
|
+
copy=False,
|
|
1128
|
+
dialect=evaluator.dialect,
|
|
1129
|
+
)
|
|
1122
1130
|
|
|
1123
1131
|
aggregates.append(node)
|
|
1124
1132
|
|
sqlmesh/core/model/cache.py
CHANGED
|
@@ -15,6 +15,7 @@ from sqlmesh.core import constants as c
|
|
|
15
15
|
from sqlmesh.core.model.definition import ExternalModel, Model, SqlModel, _Model
|
|
16
16
|
from sqlmesh.utils.cache import FileCache
|
|
17
17
|
from sqlmesh.utils.hashing import crc32
|
|
18
|
+
from sqlmesh.utils.windows import IS_WINDOWS
|
|
18
19
|
|
|
19
20
|
from dataclasses import dataclass
|
|
20
21
|
|
|
@@ -149,8 +150,10 @@ class OptimizedQueryCache:
|
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
def optimized_query_cache_pool(optimized_query_cache: OptimizedQueryCache) -> ProcessPoolExecutor:
|
|
153
|
+
# fork doesnt work on Windows. ref: https://docs.python.org/3/library/multiprocessing.html#multiprocessing-start-methods
|
|
154
|
+
context_type = "spawn" if IS_WINDOWS else "fork"
|
|
152
155
|
return ProcessPoolExecutor(
|
|
153
|
-
mp_context=mp.get_context(
|
|
156
|
+
mp_context=mp.get_context(context_type),
|
|
154
157
|
initializer=_init_optimized_query_cache,
|
|
155
158
|
initargs=(optimized_query_cache,),
|
|
156
159
|
max_workers=c.MAX_FORK_WORKERS,
|
sqlmesh/core/plan/evaluator.py
CHANGED
|
@@ -133,7 +133,9 @@ class BuiltInPlanEvaluator(PlanEvaluator):
|
|
|
133
133
|
push_completion_status = self._push(plan, snapshots, deployability_index_for_creation)
|
|
134
134
|
if push_completion_status.is_nothing_to_do:
|
|
135
135
|
self.console.log_status_update(
|
|
136
|
-
"
|
|
136
|
+
""
|
|
137
|
+
if plan.restatements
|
|
138
|
+
else "\n[green]SKIP: No physical layer updates to perform[/green]\n"
|
|
137
139
|
)
|
|
138
140
|
update_intervals_for_new_snapshots(plan.new_snapshots, self.state_sync)
|
|
139
141
|
self._restate(plan, snapshots_by_name)
|
|
@@ -457,10 +457,13 @@ class GithubController:
|
|
|
457
457
|
try:
|
|
458
458
|
# Clear out any output that might exist from prior steps
|
|
459
459
|
self._console.clear_captured_outputs()
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
460
|
+
if plan.restatements:
|
|
461
|
+
self._console._print("\n**Restating models**\n")
|
|
462
|
+
else:
|
|
463
|
+
self._console.show_environment_difference_summary(
|
|
464
|
+
context_diff=plan.context_diff,
|
|
465
|
+
no_diff=False,
|
|
466
|
+
)
|
|
464
467
|
if plan.context_diff.has_changes:
|
|
465
468
|
self._console.show_model_difference_summary(
|
|
466
469
|
context_diff=plan.context_diff,
|
sqlmesh/lsp/main.py
CHANGED
|
@@ -92,9 +92,7 @@ class SQLMeshLanguageServer:
|
|
|
92
92
|
return get_sql_completions(None, uri)
|
|
93
93
|
|
|
94
94
|
@self.server.feature(API_FEATURE)
|
|
95
|
-
def api(
|
|
96
|
-
ls: LanguageServer, request: ApiRequest
|
|
97
|
-
) -> t.Union[ApiResponseGetModels, ApiResponseGetLineage, ApiResponseGetColumnLineage]:
|
|
95
|
+
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
|
|
98
96
|
ls.log_trace(f"API request: {request}")
|
|
99
97
|
if self.lsp_context is None:
|
|
100
98
|
raise RuntimeError("No context found")
|
|
@@ -105,7 +103,9 @@ class SQLMeshLanguageServer:
|
|
|
105
103
|
if request.method == "GET":
|
|
106
104
|
if path_parts == ["api", "models"]:
|
|
107
105
|
# /api/models
|
|
108
|
-
return ApiResponseGetModels(
|
|
106
|
+
return ApiResponseGetModels(
|
|
107
|
+
data=get_models(self.lsp_context.context)
|
|
108
|
+
).model_dump(mode="json")
|
|
109
109
|
|
|
110
110
|
if path_parts[:2] == ["api", "lineage"]:
|
|
111
111
|
if len(path_parts) == 3:
|
|
@@ -113,7 +113,7 @@ class SQLMeshLanguageServer:
|
|
|
113
113
|
model_name = urllib.parse.unquote(path_parts[2])
|
|
114
114
|
lineage = model_lineage(model_name, self.lsp_context.context)
|
|
115
115
|
non_set_lineage = {k: v for k, v in lineage.items() if v is not None}
|
|
116
|
-
return ApiResponseGetLineage(data=non_set_lineage)
|
|
116
|
+
return ApiResponseGetLineage(data=non_set_lineage).model_dump(mode="json")
|
|
117
117
|
|
|
118
118
|
if len(path_parts) == 4:
|
|
119
119
|
# /api/lineage/{model}/{column}
|
|
@@ -123,7 +123,9 @@ class SQLMeshLanguageServer:
|
|
|
123
123
|
column_lineage_response = column_lineage(
|
|
124
124
|
model_name, column, False, self.lsp_context.context
|
|
125
125
|
)
|
|
126
|
-
return ApiResponseGetColumnLineage(data=column_lineage_response)
|
|
126
|
+
return ApiResponseGetColumnLineage(data=column_lineage_response).model_dump(
|
|
127
|
+
mode="json"
|
|
128
|
+
)
|
|
127
129
|
|
|
128
130
|
raise NotImplementedError(f"API request not implemented: {request.url}")
|
|
129
131
|
|
sqlmesh/magics.py
CHANGED
|
@@ -702,6 +702,11 @@ class SQLMeshMagics(Magics):
|
|
|
702
702
|
action="store_true",
|
|
703
703
|
help="Disable the check for a primary key (grain) that is missing or is not unique.",
|
|
704
704
|
)
|
|
705
|
+
@argument(
|
|
706
|
+
"--warn-grain-check",
|
|
707
|
+
action="store_true",
|
|
708
|
+
help="Warn if any selected model is missing a grain, and compute diffs for the remaining models.",
|
|
709
|
+
)
|
|
705
710
|
@line_magic
|
|
706
711
|
@pass_sqlmesh_context
|
|
707
712
|
def table_diff(self, context: Context, line: str) -> None:
|
|
@@ -723,6 +728,7 @@ class SQLMeshMagics(Magics):
|
|
|
723
728
|
show_sample=args.show_sample,
|
|
724
729
|
decimals=args.decimals,
|
|
725
730
|
skip_grain_check=args.skip_grain_check,
|
|
731
|
+
warn_grain_check=args.warn_grain_check,
|
|
726
732
|
)
|
|
727
733
|
|
|
728
734
|
@magic_arguments()
|
sqlmesh/utils/cache.py
CHANGED
|
@@ -12,6 +12,7 @@ from sqlglot import __version__ as SQLGLOT_VERSION
|
|
|
12
12
|
from sqlmesh.utils import sanitize_name
|
|
13
13
|
from sqlmesh.utils.date import to_datetime
|
|
14
14
|
from sqlmesh.utils.errors import SQLMeshError
|
|
15
|
+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -132,4 +133,8 @@ class FileCache(t.Generic[T]):
|
|
|
132
133
|
|
|
133
134
|
def _cache_entry_path(self, name: str, entry_id: str = "") -> Path:
|
|
134
135
|
entry_file_name = "__".join(p for p in (self._cache_version, name, entry_id) if p)
|
|
135
|
-
|
|
136
|
+
full_path = self._path / sanitize_name(entry_file_name)
|
|
137
|
+
if IS_WINDOWS:
|
|
138
|
+
# handle paths longer than 260 chars
|
|
139
|
+
full_path = fix_windows_path(full_path)
|
|
140
|
+
return full_path
|
sqlmesh/utils/windows.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
IS_WINDOWS = platform.system() == "Windows"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fix_windows_path(path: Path) -> Path:
|
|
8
|
+
"""
|
|
9
|
+
Windows paths are limited to 260 characters: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
|
10
|
+
Users can change this by updating a registry entry but we cant rely on that.
|
|
11
|
+
We can quite commonly generate a cache file path that exceeds 260 characters which causes a FileNotFound error.
|
|
12
|
+
If we prefix the path with "\\?\" then we can have paths up to 32,767 characters
|
|
13
|
+
"""
|
|
14
|
+
return Path("\\\\?\\" + str(path.absolute()))
|