Skip to content

Commit 9e0b1da

Browse files
authored
Merge branch 'main' into fix/databricks-seed-null-booleans
2 parents b2edb87 + 9460918 commit 9e0b1da

33 files changed

Lines changed: 1221 additions & 56 deletions

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ install-dev-dbt-%:
6363
fi; \
6464
if [ "$$version" = "1.3.0" ]; then \
6565
echo "Applying overrides for dbt $$version - upgrading google-cloud-bigquery"; \
66-
$(PIP) install 'google-cloud-bigquery>=3.0.0' --upgrade; \
66+
$(PIP) install 'google-cloud-bigquery>=3.0.0' \
67+
'pyOpenSSL>=24.0.0' --upgrade; \
6768
fi; \
6869
mv pyproject.toml.backup pyproject.toml; \
6970
echo "Restored original pyproject.toml"

docs/guides/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ The examples specify a Snowflake connection whose password is stored in an envir
170170
account: <account>
171171
```
172172

173+
!!! tip "Base64-encoded secrets"
174+
175+
If a secret is distributed base64-encoded in a single environment variable (for example a BigQuery service-account key), pipe the variable through the built-in `b64decode` filter to decode it to text inline:
176+
177+
```yaml
178+
keyfile_json: {{ env_var('BIGQUERY_KEY_B64') | b64decode }}
179+
```
180+
181+
A matching `b64encode` filter is also available. Both return UTF-8 text, so they are intended for string/JSON secrets rather than arbitrary binary data.
182+
173183
=== "Python"
174184

175185
Python accesses environment variables via the `os` library's `environ` dictionary.

docs/integrations/engines/databricks.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,25 @@ MODEL (
310310

311311
If you attempt to alter without having this property set, you will get an error similar to `databricks.sql.exc.ServerOperationError: [DELTA_UNSUPPORTED_DROP_COLUMN] DROP COLUMN is not supported for your Delta table.`.
312312
[Databricks Documentation for more details](https://docs.databricks.com/en/delta/column-mapping.html#requirements).
313+
314+
## Liquid Clustering
315+
316+
SQLMesh supports the liquid clustering keywords AUTO and NONE
317+
318+
```sql
319+
MODEL (
320+
name sqlmesh_example.new_model,
321+
...
322+
clustered_by AUTO
323+
)
324+
```
325+
326+
To cluster by a column called `auto` or `none`, use parentheses and backticks
327+
328+
```sql
329+
MODEL (
330+
name sqlmesh_example.new_model,
331+
...
332+
clustered_by (`auto`)
333+
)
334+
```

docs/prerequisites.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This page describes the system prerequisites needed to run SQLMesh and provides
44

55
## SQLMesh prerequisites
66

7-
You'll need Python 3.8 or higher to use SQLMesh. You can check your python version by running the following command:
7+
You'll need Python 3.9 or higher to use SQLMesh. You can check your python version by running the following command:
88
```bash
99
python3 --version
1010
```

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ authors = [{ name = "SQLMesh Contributors" }]
77
license = { file = "LICENSE" }
88
requires-python = ">= 3.9"
99
dependencies = [
10-
"astor",
1110
"click",
1211
"croniter",
1312
"duckdb>=0.10.0,!=0.10.3",
@@ -202,7 +201,6 @@ disable_error_code = "annotation-unchecked"
202201
[[tool.mypy.overrides]]
203202
module = [
204203
"api.*",
205-
"astor.*",
206204
"IPython.*",
207205
"hyperscript.*",
208206
"py.*",

sqlmesh/core/config/connection.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from sys import version_info
1414

1515
import pydantic
16+
from pydantic import Field, computed_field
1617
from packaging import version
17-
from pydantic import Field
1818
from pydantic_core import from_json
1919
from sqlglot import exp
2020
from sqlglot.errors import ParseError
@@ -110,7 +110,14 @@ class ConnectionConfig(abc.ABC, BaseConfig):
110110
catalog_type_overrides: t.Optional[t.Dict[str, str]] = None
111111

112112
# Whether to share a single connection across threads or create a new connection per thread.
113-
shared_connection: t.ClassVar[bool] = False
113+
#
114+
# MyPy throws a "Decorators on top of @property are not supported" error despite this being a
115+
# valid decoration, and Pydantic recommend disabling the MyPy hint for this reason - see:
116+
# https://pydantic.dev/docs/validation/2.0/usage/computed_fields/
117+
@computed_field # type: ignore[prop-decorator]
118+
@property
119+
def shared_connection(self) -> bool:
120+
return False
114121

115122
@property
116123
@abc.abstractmethod
@@ -311,7 +318,10 @@ class BaseDuckDBConnectionConfig(ConnectionConfig):
311318

312319
token: t.Optional[str] = None
313320

314-
shared_connection: t.ClassVar[bool] = True
321+
@computed_field # type: ignore[prop-decorator]
322+
@property
323+
def shared_connection(self) -> bool:
324+
return True
315325

316326
_data_file_to_adapter: t.ClassVar[t.Dict[str, EngineAdapter]] = {}
317327

@@ -820,11 +830,15 @@ class DatabricksConnectionConfig(ConnectionConfig):
820830
DISPLAY_NAME: t.ClassVar[t.Literal["Databricks"]] = "Databricks"
821831
DISPLAY_ORDER: t.ClassVar[t.Literal[3]] = 3
822832

823-
shared_connection: t.ClassVar[bool] = True
824-
825833
_concurrent_tasks_validator = concurrent_tasks_validator
826834
_http_headers_validator = http_headers_validator
827835

836+
@computed_field # type: ignore[prop-decorator]
837+
@property
838+
def shared_connection(self) -> bool:
839+
"""The connection should only be shared if U2M OAuth is being used"""
840+
return self.auth_type is not None and self.oauth_client_secret is None
841+
828842
@model_validator(mode="before")
829843
def _databricks_connect_validator(cls, data: t.Any) -> t.Any:
830844
# SQLQueryContextLogger will output any error SQL queries even if they are in a try/except block.

sqlmesh/core/config/loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,4 @@ def convert_config_type(
272272
config_obj: Config,
273273
config_type: t.Type[C],
274274
) -> C:
275-
return config_type.parse_obj(config_obj.dict())
275+
return config_type.parse_obj(config_obj.dict(exclude_computed_fields=True))

sqlmesh/core/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,5 @@
9696
HYBRID = "hybrid"
9797

9898
DISABLE_SQLMESH_STATE_MIGRATION = "SQLMESH__AIRFLOW__DISABLE_STATE_MIGRATION"
99+
100+
LIQUID_CLUSTERING_KEYWORDS: frozenset = frozenset({"AUTO", "NONE"})

sqlmesh/core/dialect.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from sqlglot.schema import MappingSchema
2525
from sqlglot.tokens import Token
2626

27-
from sqlmesh.core.constants import MAX_MODEL_DEFINITION_SIZE
27+
from sqlmesh.core.constants import LIQUID_CLUSTERING_KEYWORDS, MAX_MODEL_DEFINITION_SIZE
2828
from sqlmesh.utils import get_source_columns_to_types
2929
from sqlmesh.utils.errors import SQLMeshError, ConfigError
3030
from sqlmesh.utils.pandas import columns_to_types_from_df
@@ -663,6 +663,27 @@ def parse(self: Parser) -> t.Optional[exp.Expr]:
663663
value = exp.tuple_(*partitioned_by.this.expressions)
664664
else:
665665
value = partitioned_by.this
666+
elif key == "clustered_by":
667+
# Bare AUTO / NONE are Databricks liquid clustering keywords, not column refs.
668+
# Detect keywords by token type: unquoted bare identifiers arrive as VAR tokens.
669+
# Backtick-quoted identifiers (e.g. `auto`) have IDENTIFIER token type and are
670+
# treated as real column names.
671+
if (
672+
self._curr is not None
673+
and self._curr.token_type == TokenType.VAR
674+
and self._curr.text.upper() in LIQUID_CLUSTERING_KEYWORDS
675+
):
676+
value = exp.Var(this=self._curr.text.upper())
677+
self._advance()
678+
else:
679+
parsed = self._parse_bracket(self._parse_field(any_token=True))
680+
# Unwrap Paren wrapping a bare column to match partitioned_by normalisation:
681+
# clustered_by (a) → stored as Column(a), not Paren(Column(a)).
682+
# Preserve parens around function expressions: (TO_DATE(col)) stays as-is.
683+
if isinstance(parsed, exp.Paren) and isinstance(parsed.this, exp.Column):
684+
value = parsed.unnest()
685+
else:
686+
value = parsed
666687
else:
667688
value = self._parse_bracket(self._parse_field(any_token=True))
668689

sqlmesh/core/engine_adapter/databricks.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from sqlglot import exp
88

9+
from sqlmesh.core.constants import LIQUID_CLUSTERING_KEYWORDS
910
from sqlmesh.core.dialect import to_schema
1011
from sqlmesh.core.engine_adapter.mixins import GrantsFromInfoSchemaMixin
1112
from sqlmesh.core.engine_adapter.shared import (
@@ -153,7 +154,7 @@ def _set_spark_engine_adapter_if_needed(self) -> None:
153154
host=self._extra_config["databricks_connect_server_hostname"],
154155
token=self._extra_config.get("databricks_connect_access_token"),
155156
)
156-
if "databricks_connect_use_serverless" in self._extra_config:
157+
if self._extra_config.get("databricks_connect_use_serverless"):
157158
connect_kwargs["serverless"] = True
158159
else:
159160
connect_kwargs["cluster_id"] = self._extra_config["databricks_connect_cluster_id"]
@@ -442,10 +443,16 @@ def _build_table_properties_exp(
442443
table_kind=table_kind,
443444
)
444445
if clustered_by:
445-
# Databricks expects wrapped CLUSTER BY expressions
446-
clustered_by_exp = exp.Cluster(
447-
expressions=[exp.Tuple(expressions=[c.copy() for c in clustered_by])]
448-
)
446+
if len(clustered_by) == 1 and isinstance(clustered_by[0], exp.Var):
447+
if clustered_by[0].name.upper() not in LIQUID_CLUSTERING_KEYWORDS:
448+
raise ValueError(f"Unexpected bare Var in clustered_by: {clustered_by[0]!r}")
449+
# exp.Cluster with a bare Var generates: CLUSTER BY AUTO (no parens)
450+
clustered_by_exp = exp.Cluster(expressions=[clustered_by[0].copy()])
451+
else:
452+
# Databricks expects column expressions wrapped in a tuple
453+
clustered_by_exp = exp.Cluster(
454+
expressions=[exp.Tuple(expressions=[c.copy() for c in clustered_by])]
455+
)
449456
expressions = properties.expressions if properties else []
450457
expressions.append(clustered_by_exp)
451458
properties = exp.Properties(expressions=expressions)

0 commit comments

Comments
 (0)