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.
Files changed (71) hide show
  1. sqlmesh/_version.py +2 -2
  2. sqlmesh/cli/main.py +5 -0
  3. sqlmesh/core/config/connection.py +52 -0
  4. sqlmesh/core/console.py +10 -6
  5. sqlmesh/core/context.py +17 -9
  6. sqlmesh/core/engine_adapter/athena.py +5 -3
  7. sqlmesh/core/engine_adapter/snowflake.py +18 -3
  8. sqlmesh/core/macros.py +15 -7
  9. sqlmesh/core/model/cache.py +4 -1
  10. sqlmesh/core/plan/evaluator.py +3 -1
  11. sqlmesh/integrations/github/cicd/controller.py +7 -4
  12. sqlmesh/lsp/main.py +8 -6
  13. sqlmesh/magics.py +6 -0
  14. sqlmesh/utils/cache.py +6 -1
  15. sqlmesh/utils/windows.py +14 -0
  16. {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/METADATA +1 -1
  17. {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/RECORD +69 -68
  18. {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/WHEEL +1 -1
  19. web/client/dist/assets/{Audits-DF7amJxU.js → Audits-cGDtiiKB.js} +1 -1
  20. web/client/dist/assets/{Banner-C3VTntGq.js → Banner-DHp1YP1u.js} +1 -1
  21. web/client/dist/assets/{ChevronDownIcon-qm4bu8e3.js → ChevronDownIcon-Sazvo9i-.js} +1 -1
  22. web/client/dist/assets/{ChevronRightIcon-BolEdhGf.js → ChevronRightIcon-84R-3q8m.js} +1 -1
  23. web/client/dist/assets/{Content-B6xcwedd.js → Content-B9yV2iEZ.js} +1 -1
  24. web/client/dist/assets/{Content-BRVWZ3mb.js → Content-C65MWvF0.js} +1 -1
  25. web/client/dist/assets/{Data-Biu-V0fH.js → Data-By25O4M5.js} +1 -1
  26. web/client/dist/assets/{DataCatalog-B6evo1XW.js → DataCatalog-hSS21hR6.js} +1 -1
  27. web/client/dist/assets/{Editor-CVxCL2-Y.js → Editor-Bsxwi3aO.js} +1 -1
  28. web/client/dist/assets/{Editor-CLmFVr2P.js → Editor-CUCC89Ij.js} +1 -1
  29. web/client/dist/assets/{Errors-oZskb8PG.js → Errors-BOCVwZyl.js} +1 -1
  30. web/client/dist/assets/{FileExplorer-CGr1vJjw.js → FileExplorer-BZ8API0P.js} +1 -1
  31. web/client/dist/assets/{Footer-fJbe0D4K.js → Footer-CUwjtSu4.js} +1 -1
  32. web/client/dist/assets/{Header-BH87N9ET.js → Header-LsiS75Z7.js} +1 -1
  33. web/client/dist/assets/Input-D0tI9yL5.js +41 -0
  34. web/client/dist/assets/Lineage-DWpcQYIS.js +1 -0
  35. web/client/dist/assets/{ListboxShow-BWNLIBMH.js → ListboxShow-DF5PqZhj.js} +1 -1
  36. web/client/dist/assets/{ModelLineage-c5SOHuSl.js → ModelLineage-BHZN6cbe.js} +1 -1
  37. web/client/dist/assets/{Models-BNHmcp1o.js → Models-y5gl8Oaf.js} +1 -1
  38. web/client/dist/assets/{Page-daP33Ypg.js → Page-DbTpD-wK.js} +1 -1
  39. web/client/dist/assets/{Plan-DUHa3QdG.js → Plan-CiSxSXLS.js} +1 -1
  40. web/client/dist/assets/{PlusCircleIcon-Bk7UCkgh.js → PlusCircleIcon-DJoyPo8S.js} +1 -1
  41. web/client/dist/assets/{ReportErrors-DfzGQ2eY.js → ReportErrors-CNXH4GuU.js} +1 -1
  42. web/client/dist/assets/{Root-uPpx7ocJ.js → Root-RYhutnyD.js} +1 -1
  43. web/client/dist/assets/{SearchList-87imxiuX.js → SearchList-iv5ny7Mb.js} +1 -1
  44. web/client/dist/assets/{SelectEnvironment-DRt5ys9a.js → SelectEnvironment-DSHK4OKG.js} +1 -1
  45. web/client/dist/assets/{SourceList-Cfd2Hs4e.js → SourceList-BXrKA-hR.js} +1 -1
  46. web/client/dist/assets/{SourceListItem-Cgi7ENFV.js → SourceListItem-BvVyXARN.js} +1 -1
  47. web/client/dist/assets/{SplitPane-p-JrHTOr.js → SplitPane-BCBwk2UE.js} +1 -1
  48. web/client/dist/assets/{Tests-Das-Vk4n.js → Tests-BOygb2O9.js} +1 -1
  49. web/client/dist/assets/{Welcome-33aFy-Y-.js → Welcome-saOWnjhW.js} +1 -1
  50. web/client/dist/assets/{context-NpzoJDLp.js → context-Cl_qdjcd.js} +1 -1
  51. web/client/dist/assets/{context-C5ccds54.js → context-cGGhLbGj.js} +1 -1
  52. web/client/dist/assets/{editor-B8OcqW2l.js → editor-yKXGVuvn.js} +1 -1
  53. web/client/dist/assets/{file-D-wqO8gv.js → file-CJ8A1f27.js} +1 -1
  54. web/client/dist/assets/{floating-ui.react-dom-BrhHyVvs.js → floating-ui.react-dom-CfXRycK_.js} +1 -1
  55. web/client/dist/assets/{help-CQWPRUV2.js → help-aam8B-7S.js} +1 -1
  56. web/client/dist/assets/{index-xGKh539i.js → index-CQy7Tkrl.js} +2 -2
  57. web/client/dist/assets/{index-Co-_3xPI.js → index-zO-UPMUq.js} +1 -1
  58. web/client/dist/assets/{plan-BIz0iaor.js → plan-CiAasU-P.js} +1 -1
  59. web/client/dist/assets/{popover-BXDPVmB7.js → popover-Ox7TJJ2-.js} +1 -1
  60. web/client/dist/assets/{project-H8d6rukl.js → project-PqqAyMWE.js} +1 -1
  61. web/client/dist/index.html +1 -1
  62. web/server/api/endpoints/commands.py +12 -5
  63. web/server/api/endpoints/files.py +2 -2
  64. web/server/api/endpoints/models.py +2 -2
  65. web/server/models.py +1 -1
  66. web/server/utils.py +11 -0
  67. web/client/dist/assets/Input-D4nBSMWJ.js +0 -41
  68. web/client/dist/assets/Lineage-CPutJH7u.js +0 -1
  69. {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/entry_points.txt +0 -0
  70. {sqlmesh-0.184.2.dev2.dist-info → sqlmesh-0.184.3.dist-info}/licenses/LICENSE +0 -0
  71. {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.2.dev2'
21
- __version_tuple__ = version_tuple = (0, 184, 2, 'dev2')
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
- self.show_environment_difference_summary(
1786
- plan.context_diff,
1787
- no_diff=no_diff,
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
- self._write(f"Starting promotion for {len(snapshots)} snapshots")
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 os
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 = os.path.join(self.s3_warehouse_location, table.catalog or "", table.db or "")
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(os.path.join(base_uri, table.text("this") or ""), base=True)
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
- return snowpark.Session.builder.configs(
108
- {"connection": self._connection_pool.get()}
109
- ).getOrCreate()
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: SQL = SQL("SUM"),
1094
- cmp: SQL = SQL("="),
1095
- prefix: SQL = SQL(""),
1096
- suffix: SQL = SQL(""),
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_(f"{prefix}{value}{suffix}", quoted=quote, copy=False)
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
 
@@ -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("fork"),
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,
@@ -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
- "\n[green]SKIP: No physical layer updates to perform[/green]\n"
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
- self._console.show_environment_difference_summary(
461
- context_diff=plan.context_diff,
462
- no_diff=False,
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(data=get_models(self.lsp_context.context))
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
- return self._path / sanitize_name(entry_file_name)
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
@@ -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()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmesh
3
- Version: 0.184.2.dev2
3
+ Version: 0.184.3
4
4
  Summary: Next-generation data transformation framework
5
5
  Author-email: "TobikoData Inc." <engineering@tobikodata.com>
6
6
  License: Apache License