Edit on GitHub

sqlmesh.core.config.connection

   1from __future__ import annotations
   2
   3import abc
   4import base64
   5import importlib
   6import logging
   7import os
   8import pathlib
   9import re
  10import typing as t
  11from enum import Enum
  12from functools import partial
  13from sys import version_info
  14
  15import pydantic
  16from pydantic import Field, computed_field
  17from packaging import version
  18from pydantic_core import from_json
  19from sqlglot import exp
  20from sqlglot.errors import ParseError
  21from sqlglot.helper import subclasses
  22
  23from sqlmesh.core import engine_adapter
  24from sqlmesh.core.config.base import BaseConfig
  25from sqlmesh.core.config.common import (
  26    compile_regex_mapping,
  27    concurrent_tasks_validator,
  28    http_headers_validator,
  29)
  30from sqlmesh.core.engine_adapter import EngineAdapter
  31from sqlmesh.core.engine_adapter.shared import CatalogSupport
  32from sqlmesh.utils import debug_mode_enabled, str_to_bool
  33from sqlmesh.utils.aws import validate_s3_uri
  34from sqlmesh.utils.errors import ConfigError
  35from sqlmesh.utils.pydantic import (
  36    ValidationInfo,
  37    field_validator,
  38    get_concrete_types_from_typehint,
  39    model_validator,
  40    validation_data,
  41    validation_error_message,
  42)
  43
  44if t.TYPE_CHECKING:
  45    from sqlmesh.core._typing import Self
  46
  47logger = logging.getLogger(__name__)
  48
  49RECOMMENDED_STATE_SYNC_ENGINES = {
  50    "postgres",
  51    "gcp_postgres",
  52    "mysql",
  53    "mssql",
  54    "azuresql",
  55}
  56FORBIDDEN_STATE_SYNC_ENGINES = {
  57    # Do not support row-level operations
  58    "spark",
  59    "trino",
  60    # Nullable types are problematic
  61    "clickhouse",
  62    "starrocks",
  63}
  64MOTHERDUCK_TOKEN_REGEX = re.compile(r"(\?|\&)(motherduck_token=)(\S*)")
  65PASSWORD_REGEX = re.compile(r"(password=)(\S+)")
  66SUPPORTS_MSSQL_PYTHON_DRIVER = (version_info.major, version_info.minor) >= (3, 10)
  67
  68
  69def _get_engine_import_validator(
  70    import_name: str, engine_type: str, extra_name: t.Optional[str] = None, decorate: bool = True
  71) -> t.Callable:
  72    extra_name = extra_name or engine_type
  73
  74    def validate(cls: t.Any, data: t.Any) -> t.Any:
  75        check_import = (
  76            str_to_bool(str(data.pop("check_import", True))) if isinstance(data, dict) else True
  77        )
  78        if not check_import:
  79            return data
  80        try:
  81            importlib.import_module(import_name)
  82        except ImportError:
  83            if debug_mode_enabled():
  84                raise
  85
  86            logger.exception("Failed to import the engine library")
  87
  88            raise ConfigError(
  89                f"Failed to import the '{engine_type}' engine library. This may be due to a missing "
  90                "or incompatible installation. Please ensure the required dependency is installed by "
  91                f'running: `pip install "sqlmesh[{extra_name}]"`. For more details, check the logs '
  92                "in the 'logs/' folder, or rerun the command with the '--debug' flag."
  93            )
  94
  95        return data
  96
  97    return model_validator(mode="before")(validate) if decorate else validate
  98
  99
 100class ConnectionConfig(abc.ABC, BaseConfig):
 101    type_: str
 102    DIALECT: t.ClassVar[str]
 103    DISPLAY_NAME: t.ClassVar[str]
 104    DISPLAY_ORDER: t.ClassVar[int]
 105    concurrent_tasks: int
 106    register_comments: bool
 107    pre_ping: bool
 108    pretty_sql: bool = False
 109    schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None
 110    catalog_type_overrides: t.Optional[t.Dict[str, str]] = None
 111
 112    # Whether to share a  single connection across threads or create a new connection per thread.
 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
 121
 122    @property
 123    @abc.abstractmethod
 124    def _connection_kwargs_keys(self) -> t.Set[str]:
 125        """keywords that should be passed into the connection"""
 126
 127    @property
 128    @abc.abstractmethod
 129    def _engine_adapter(self) -> t.Type[EngineAdapter]:
 130        """The engine adapter for this connection"""
 131
 132    @property
 133    @abc.abstractmethod
 134    def _connection_factory(self) -> t.Callable:
 135        """A function that is called to return a connection object for the given Engine Adapter"""
 136
 137    @property
 138    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
 139        """The static connection kwargs for this connection"""
 140        return {}
 141
 142    @property
 143    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
 144        """kwargs that are for execution config only"""
 145        return {}
 146
 147    @property
 148    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
 149        """A function that is called to initialize the cursor"""
 150        return None
 151
 152    @property
 153    def is_recommended_for_state_sync(self) -> bool:
 154        """Whether this engine is recommended for being used as a state sync for production state syncs"""
 155        return self.type_ in RECOMMENDED_STATE_SYNC_ENGINES
 156
 157    @property
 158    def is_forbidden_for_state_sync(self) -> bool:
 159        """Whether this engine is forbidden from being used as a state sync"""
 160        return self.type_ in FORBIDDEN_STATE_SYNC_ENGINES
 161
 162    @property
 163    def _connection_factory_with_kwargs(self) -> t.Callable[[], t.Any]:
 164        """A function that is called to return a connection object for the given Engine Adapter"""
 165        return partial(
 166            self._connection_factory,
 167            **{
 168                **self._static_connection_kwargs,
 169                **{k: v for k, v in self.dict().items() if k in self._connection_kwargs_keys},
 170            },
 171        )
 172
 173    def connection_validator(self) -> t.Callable[[], None]:
 174        """A function that validates the connection configuration"""
 175        return self.create_engine_adapter().ping
 176
 177    def create_engine_adapter(
 178        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
 179    ) -> EngineAdapter:
 180        """Returns a new instance of the Engine Adapter."""
 181
 182        concurrent_tasks = concurrent_tasks or self.concurrent_tasks
 183        return self._engine_adapter(
 184            self._connection_factory_with_kwargs,
 185            multithreaded=concurrent_tasks > 1,
 186            default_catalog=self.get_catalog(),
 187            cursor_init=self._cursor_init,
 188            register_comments=register_comments_override or self.register_comments,
 189            pre_ping=self.pre_ping,
 190            pretty_sql=self.pretty_sql,
 191            shared_connection=self.shared_connection,
 192            schema_differ_overrides=self.schema_differ_overrides,
 193            catalog_type_overrides=self.catalog_type_overrides,
 194            **self._extra_engine_config,
 195        )
 196
 197    def get_catalog(self) -> t.Optional[str]:
 198        """The catalog for this connection"""
 199        if hasattr(self, "catalog"):
 200            return self.catalog
 201        if hasattr(self, "database"):
 202            return self.database
 203        if hasattr(self, "db"):
 204            return self.db
 205        return None
 206
 207    @model_validator(mode="before")
 208    @classmethod
 209    def _expand_json_strings_to_concrete_types(cls, data: t.Any) -> t.Any:
 210        """
 211        There are situations where a connection config class has a field that is some kind of complex type
 212        (eg a list of strings or a dict) but the value is being supplied from a source such as an environment variable
 213
 214        When this happens, the value is supplied as a string rather than a Python object. We need some way
 215        of turning this string into the corresponding Python list or dict.
 216
 217        Rather than doing this piecemeal on every config subclass, this provides a generic implementatation
 218        to identify fields that may be be supplied as JSON strings and handle them transparently
 219        """
 220        if data and isinstance(data, dict):
 221            for maybe_json_field_name in cls._get_list_and_dict_field_names():
 222                if (value := data.get(maybe_json_field_name)) and isinstance(value, str):
 223                    # crude JSON check as we dont want to try and parse every string we get
 224                    value = value.strip()
 225                    if value.startswith("{") or value.startswith("["):
 226                        data[maybe_json_field_name] = from_json(value)
 227
 228        return data
 229
 230    @classmethod
 231    def _get_list_and_dict_field_names(cls) -> t.Set[str]:
 232        field_names = set()
 233        for name, field in cls.model_fields.items():
 234            if field.annotation:
 235                field_types = get_concrete_types_from_typehint(field.annotation)
 236
 237                # check if the field type is something that could concievably be supplied as a json string
 238                if any(ft is t for t in (list, tuple, set, dict) for ft in field_types):
 239                    field_names.add(name)
 240
 241        return field_names
 242
 243
 244class DuckDBAttachOptions(BaseConfig):
 245    type: str
 246    path: str
 247    read_only: bool = False
 248
 249    # DuckLake specific options
 250    data_path: t.Optional[str] = None
 251    override_data_path: t.Optional[bool] = False
 252    encrypted: bool = False
 253    data_inlining_row_limit: t.Optional[int] = None
 254    metadata_schema: t.Optional[str] = None
 255
 256    def to_sql(self, alias: str) -> str:
 257        options = []
 258        # 'duckdb' is actually not a supported type, but we'd like to allow it for
 259        # fully qualified attach options or integration testing, similar to duckdb-dbt
 260        if self.type not in ("duckdb", "ducklake", "motherduck"):
 261            options.append(f"TYPE {self.type.upper()}")
 262        if self.read_only:
 263            options.append("READ_ONLY")
 264
 265        # DuckLake specific options
 266        path = self.path
 267        if self.type == "ducklake":
 268            if not path.startswith("ducklake:"):
 269                path = f"ducklake:{path}"
 270            if self.data_path is not None:
 271                options.append(f"DATA_PATH '{self.data_path}'")
 272                if self.override_data_path:
 273                    options.append("OVERRIDE_DATA_PATH true")
 274            if self.encrypted:
 275                options.append("ENCRYPTED")
 276            if self.data_inlining_row_limit is not None:
 277                options.append(f"DATA_INLINING_ROW_LIMIT {self.data_inlining_row_limit}")
 278            if self.metadata_schema is not None:
 279                options.append(f"METADATA_SCHEMA '{self.metadata_schema}'")
 280
 281        options_sql = f" ({', '.join(options)})" if options else ""
 282        alias_sql = ""
 283        # TODO: Add support for Postgres schema. Currently adding it blocks access to the information_schema
 284
 285        # MotherDuck does not support aliasing
 286        alias_sql = (
 287            f" AS {alias}" if not (self.type == "motherduck" or self.path.startswith("md:")) else ""
 288        )
 289        return f"ATTACH IF NOT EXISTS '{path}'{alias_sql}{options_sql}"
 290
 291
 292class BaseDuckDBConnectionConfig(ConnectionConfig):
 293    """Common configuration for the DuckDB-based connections.
 294
 295    Args:
 296        database: The optional database name. If not specified, the in-memory database will be used.
 297        catalogs: Key is the name of the catalog and value is the path.
 298        extensions: A list of autoloadable extensions to load.
 299        connector_config: A dictionary of configuration to pass into the duckdb connector.
 300        secrets: A list of dictionaries used to generate DuckDB secrets for authenticating with external services (e.g. S3).
 301        filesystems: A list of dictionaries used to register `fsspec` filesystems to the DuckDB cursor.
 302        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
 303        register_comments: Whether or not to register model comments with the SQL engine.
 304        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
 305        token: The optional MotherDuck token. If not specified and a MotherDuck path is in the catalog, the user will be prompted to login with their web browser.
 306    """
 307
 308    database: t.Optional[str] = None
 309    catalogs: t.Optional[t.Dict[str, t.Union[str, DuckDBAttachOptions]]] = None
 310    extensions: t.List[t.Union[str, t.Dict[str, t.Any]]] = []
 311    connector_config: t.Dict[str, t.Any] = {}
 312    secrets: t.Union[t.List[t.Dict[str, t.Any]], t.Dict[str, t.Dict[str, t.Any]]] = []
 313    filesystems: t.List[t.Dict[str, t.Any]] = []
 314
 315    concurrent_tasks: int = 1
 316    register_comments: bool = True
 317    pre_ping: t.Literal[False] = False
 318
 319    token: t.Optional[str] = None
 320
 321    @computed_field  # type: ignore[prop-decorator]
 322    @property
 323    def shared_connection(self) -> bool:
 324        return True
 325
 326    _data_file_to_adapter: t.ClassVar[t.Dict[str, EngineAdapter]] = {}
 327
 328    @model_validator(mode="before")
 329    def _validate_database_catalogs(cls, data: t.Any) -> t.Any:
 330        if not isinstance(data, dict):
 331            return data
 332
 333        db_path = data.get("database")
 334        if db_path and data.get("catalogs"):
 335            raise ConfigError(
 336                "Cannot specify both `database` and `catalogs`. Define all your catalogs in `catalogs` and have the first entry be the default catalog"
 337            )
 338        if isinstance(db_path, str) and db_path.startswith("md:"):
 339            raise ConfigError(
 340                "Please use connection type 'motherduck' without the `md:` prefix if you want to use a MotherDuck database as the single `database`."
 341            )
 342
 343        return data
 344
 345    @property
 346    def _engine_adapter(self) -> t.Type[EngineAdapter]:
 347        return engine_adapter.DuckDBEngineAdapter
 348
 349    @property
 350    def _connection_kwargs_keys(self) -> t.Set[str]:
 351        return {"database"}
 352
 353    @property
 354    def _connection_factory(self) -> t.Callable:
 355        import duckdb
 356
 357        return duckdb.connect
 358
 359    @property
 360    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
 361        """A function that is called to initialize the cursor"""
 362        import duckdb
 363        from duckdb import BinderException
 364
 365        def init(cursor: duckdb.DuckDBPyConnection) -> None:
 366            for extension in self.extensions:
 367                extension = extension if isinstance(extension, dict) else {"name": extension}
 368
 369                install_command = f"INSTALL {extension['name']}"
 370
 371                if extension.get("repository"):
 372                    install_command = f"{install_command} FROM {extension['repository']}"
 373
 374                if extension.get("force_install"):
 375                    install_command = f"FORCE {install_command}"
 376
 377                try:
 378                    cursor.execute(install_command)
 379                    cursor.execute(f"LOAD {extension['name']}")
 380                except Exception as e:
 381                    raise ConfigError(f"Failed to load extension {extension['name']}: {e}")
 382
 383            if self.connector_config:
 384                option_names = list(self.connector_config)
 385                in_part = ",".join("?" for _ in range(len(option_names)))
 386
 387                cursor.execute(
 388                    f"SELECT name, value FROM duckdb_settings() WHERE name IN ({in_part})",
 389                    option_names,
 390                )
 391
 392                existing_values = {field: setting for field, setting in cursor.fetchall()}
 393
 394                # only set connector_config items if the values differ from what is already set
 395                # trying to set options like 'temp_directory' even to the same value can throw errors like:
 396                # Not implemented Error: Cannot switch temporary directory after the current one has been used
 397                for field, setting in self.connector_config.items():
 398                    if existing_values.get(field) != setting:
 399                        try:
 400                            cursor.execute(f"SET {field} = '{setting}'")
 401                        except Exception as e:
 402                            raise ConfigError(
 403                                f"Failed to set connector config {field} to {setting}: {e}"
 404                            )
 405
 406            if self.secrets:
 407                duckdb_version = duckdb.__version__
 408                if version.parse(duckdb_version) < version.parse("0.10.0"):
 409                    from sqlmesh.core.console import get_console
 410
 411                    get_console().log_warning(
 412                        f"DuckDB version {duckdb_version} does not support secrets-based authentication (requires 0.10.0 or later).\n"
 413                        "To use secrets, please upgrade DuckDB. For older versions, configure legacy authentication via `connector_config`.\n"
 414                        "More info: https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html"
 415                    )
 416                else:
 417                    if isinstance(self.secrets, list):
 418                        secrets_items = [(secret_dict, "") for secret_dict in self.secrets]
 419                    else:
 420                        secrets_items = [
 421                            (secret_dict, secret_name)
 422                            for secret_name, secret_dict in self.secrets.items()
 423                        ]
 424
 425                    for secret_dict, secret_name in secrets_items:
 426                        secret_settings: t.List[str] = []
 427                        for field, setting in secret_dict.items():
 428                            secret_settings.append(f"{field} '{setting}'")
 429                        if secret_settings:
 430                            secret_clause = ", ".join(secret_settings)
 431                            try:
 432                                cursor.execute(
 433                                    f"CREATE OR REPLACE SECRET {secret_name} ({secret_clause});"
 434                                )
 435                            except Exception as e:
 436                                raise ConfigError(f"Failed to create secret: {e}")
 437
 438            if self.filesystems:
 439                from fsspec import filesystem  # type: ignore
 440
 441                for file_system in self.filesystems:
 442                    options = file_system.copy()
 443                    fs = options.pop("fs")
 444                    fs = filesystem(fs, **options)
 445                    cursor.register_filesystem(fs)
 446
 447            for i, (alias, path_options) in enumerate(
 448                (getattr(self, "catalogs", None) or {}).items()
 449            ):
 450                # we parse_identifier and generate to ensure that `alias` has exactly one set of quotes
 451                # regardless of whether it comes in quoted or not
 452                alias = exp.parse_identifier(alias, dialect="duckdb").sql(
 453                    identify=True, dialect="duckdb"
 454                )
 455                try:
 456                    if isinstance(path_options, DuckDBAttachOptions):
 457                        query = path_options.to_sql(alias)
 458                    else:
 459                        query = f"ATTACH IF NOT EXISTS '{path_options}'"
 460                        if not path_options.startswith("md:"):
 461                            query += f" AS {alias}"
 462                    cursor.execute(query)
 463                except BinderException as e:
 464                    # If a user tries to create a catalog pointing at `:memory:` and with the name `memory`
 465                    # then we don't want to raise since this happens by default. They are just doing this to
 466                    # set it as the default catalog.
 467                    # If a user tried to attach a MotherDuck database/share which has already by attached via
 468                    # `ATTACH 'md:'`, then we don't want to raise since this is expected.
 469                    if (
 470                        not (
 471                            'database with name "memory" already exists' in str(e)
 472                            and path_options == ":memory:"
 473                        )
 474                        and f"""database with name "{path_options.path.replace("md:", "")}" already exists"""
 475                        not in str(e)
 476                    ):
 477                        raise e
 478                if i == 0 and not getattr(self, "database", None):
 479                    cursor.execute(f"USE {alias}")
 480
 481        return init
 482
 483    def create_engine_adapter(
 484        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
 485    ) -> EngineAdapter:
 486        """Checks if another engine adapter has already been created that shares a catalog that points to the same data
 487        file. If so, it uses that same adapter instead of creating a new one. As a result, any additional configuration
 488        associated with the new adapter will be ignored."""
 489        data_files = set((self.catalogs or {}).values())
 490        if self.database:
 491            if isinstance(self, MotherDuckConnectionConfig):
 492                data_files.add(
 493                    f"md:{self.database}"
 494                    + (f"?motherduck_token={self.token}" if self.token else "")
 495                )
 496            else:
 497                data_files.add(self.database)
 498        data_files.discard(":memory:")
 499        for data_file in data_files:
 500            key = data_file if isinstance(data_file, str) else data_file.path
 501            adapter = BaseDuckDBConnectionConfig._data_file_to_adapter.get(key)
 502            if adapter is not None:
 503                logger.info(
 504                    f"Using existing DuckDB adapter due to overlapping data file: {self._mask_sensitive_data(key)}"
 505                )
 506                return adapter
 507
 508        if data_files:
 509            masked_files = {
 510                self._mask_sensitive_data(file if isinstance(file, str) else file.path)
 511                for file in data_files
 512            }
 513            logger.info(f"Creating new DuckDB adapter for data files: {masked_files}")
 514        else:
 515            logger.info("Creating new DuckDB adapter for in-memory database")
 516        adapter = super().create_engine_adapter(
 517            register_comments_override, concurrent_tasks=concurrent_tasks
 518        )
 519        for data_file in data_files:
 520            key = data_file if isinstance(data_file, str) else data_file.path
 521            BaseDuckDBConnectionConfig._data_file_to_adapter[key] = adapter
 522        return adapter
 523
 524    def get_catalog(self) -> t.Optional[str]:
 525        if self.database:
 526            # Remove `:` from the database name in order to handle if `:memory:` is passed in
 527            return pathlib.Path(self.database.replace(":memory:", "memory")).stem
 528        if self.catalogs:
 529            return list(self.catalogs)[0]
 530        return None
 531
 532    def _mask_sensitive_data(self, string: str) -> str:
 533        # Mask MotherDuck tokens with fixed number of asterisks
 534        result = MOTHERDUCK_TOKEN_REGEX.sub(
 535            lambda m: f"{m.group(1)}{m.group(2)}{'*' * 8 if m.group(3) else ''}", string
 536        )
 537        # Mask PostgreSQL/MySQL passwords with fixed number of asterisks
 538        result = PASSWORD_REGEX.sub(lambda m: f"{m.group(1)}{'*' * 8}", result)
 539        return result
 540
 541
 542class MotherDuckConnectionConfig(BaseDuckDBConnectionConfig):
 543    """Configuration for the MotherDuck connection."""
 544
 545    type_: t.Literal["motherduck"] = Field(alias="type", default="motherduck")
 546    DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb"
 547    DISPLAY_NAME: t.ClassVar[t.Literal["MotherDuck"]] = "MotherDuck"
 548    DISPLAY_ORDER: t.ClassVar[t.Literal[5]] = 5
 549
 550    @property
 551    def _connection_kwargs_keys(self) -> t.Set[str]:
 552        return set()
 553
 554    @property
 555    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
 556        """kwargs that are for execution config only"""
 557        from sqlmesh import __version__
 558
 559        custom_user_agent_config = {"custom_user_agent": f"SQLMesh/{__version__}"}
 560        connection_str = "md:"
 561        if self.database:
 562            # Attach single MD database instead of all databases on the account
 563            connection_str += f"{self.database}?attach_mode=single"
 564        if self.token:
 565            connection_str += f"{'&' if self.database else '?'}motherduck_token={self.token}"
 566        return {"database": connection_str, "config": custom_user_agent_config}
 567
 568    @property
 569    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
 570        return {"is_motherduck": True}
 571
 572
 573class DuckDBConnectionConfig(BaseDuckDBConnectionConfig):
 574    """Configuration for the DuckDB connection."""
 575
 576    type_: t.Literal["duckdb"] = Field(alias="type", default="duckdb")
 577    DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb"
 578    DISPLAY_NAME: t.ClassVar[t.Literal["DuckDB"]] = "DuckDB"
 579    DISPLAY_ORDER: t.ClassVar[t.Literal[1]] = 1
 580
 581
 582class SnowflakeConnectionConfig(ConnectionConfig):
 583    """Configuration for the Snowflake connection.
 584
 585    Args:
 586        account: The Snowflake account name.
 587        user: The Snowflake username.
 588        password: The Snowflake password.
 589        warehouse: The optional warehouse name.
 590        database: The optional database name.
 591        role: The optional role name.
 592        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
 593        authenticator: The optional authenticator name. Defaults to username/password authentication ("snowflake").
 594                       Options: https://github.com/snowflakedb/snowflake-connector-python/blob/e937591356c067a77f34a0a42328907fda792c23/src/snowflake/connector/network.py#L178-L183
 595        token: The optional oauth access token to use for authentication when authenticator is set to "oauth".
 596        private_key: The optional private key to use for authentication. Key can be Base64-encoded DER format (representing the key bytes), a plain-text PEM format, or bytes (Python config only). https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#using-key-pair-authentication-key-pair-rotation
 597        private_key_path: The optional path to the private key to use for authentication. This would be used instead of `private_key`.
 598        private_key_passphrase: The optional passphrase to use to decrypt `private_key` or `private_key_path`. Keys can be created without encryption so only provide this if needed.
 599        register_comments: Whether or not to register model comments with the SQL engine.
 600        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
 601        session_parameters: The optional session parameters to set for the connection.
 602        host: Host address for the connection.
 603        port: Port for the connection.
 604    """
 605
 606    account: str
 607    user: t.Optional[str] = None
 608    password: t.Optional[str] = None
 609    warehouse: t.Optional[str] = None
 610    database: t.Optional[str] = None
 611    role: t.Optional[str] = None
 612    authenticator: t.Optional[str] = None
 613    token: t.Optional[str] = None
 614    host: t.Optional[str] = None
 615    port: t.Optional[int] = None
 616    application: t.Literal["Tobiko_SQLMesh"] = "Tobiko_SQLMesh"
 617
 618    # Private Key Auth
 619    private_key: t.Optional[t.Union[str, bytes]] = None
 620    private_key_path: t.Optional[str] = None
 621    private_key_passphrase: t.Optional[str] = None
 622
 623    concurrent_tasks: int = 4
 624    register_comments: bool = True
 625    pre_ping: bool = False
 626
 627    session_parameters: t.Optional[dict] = None
 628
 629    type_: t.Literal["snowflake"] = Field(alias="type", default="snowflake")
 630    DIALECT: t.ClassVar[t.Literal["snowflake"]] = "snowflake"
 631    DISPLAY_NAME: t.ClassVar[t.Literal["Snowflake"]] = "Snowflake"
 632    DISPLAY_ORDER: t.ClassVar[t.Literal[2]] = 2
 633
 634    _concurrent_tasks_validator = concurrent_tasks_validator
 635
 636    @model_validator(mode="before")
 637    def _validate_authenticator(cls, data: t.Any) -> t.Any:
 638        if not isinstance(data, dict):
 639            return data
 640
 641        from snowflake.connector.network import DEFAULT_AUTHENTICATOR, OAUTH_AUTHENTICATOR
 642
 643        auth = data.get("authenticator")
 644        auth = auth.upper() if auth else DEFAULT_AUTHENTICATOR
 645        user = data.get("user")
 646        password = data.get("password")
 647        data["private_key"] = cls._get_private_key(data, auth)  # type: ignore
 648
 649        if (
 650            auth == DEFAULT_AUTHENTICATOR
 651            and not data.get("private_key")
 652            and (not user or not password)
 653        ):
 654            raise ConfigError("User and password must be provided if using default authentication")
 655
 656        if auth == OAUTH_AUTHENTICATOR and not data.get("token"):
 657            raise ConfigError("Token must be provided if using oauth authentication")
 658
 659        return data
 660
 661    _engine_import_validator = _get_engine_import_validator(
 662        "snowflake.connector.network", "snowflake"
 663    )
 664
 665    @classmethod
 666    def _get_private_key(cls, values: t.Dict[str, t.Optional[str]], auth: str) -> t.Optional[bytes]:
 667        """
 668        source: https://github.com/dbt-labs/dbt-snowflake/blob/0374b4ec948982f2ac8ec0c95d53d672ad19e09c/dbt/adapters/snowflake/connections.py#L247C5-L285C1
 669
 670        Overall code change: Use local variables instead of class attributes + Validation
 671        """
 672        # Start custom code
 673        from cryptography.hazmat.backends import default_backend
 674        from cryptography.hazmat.primitives import serialization
 675        from snowflake.connector.network import (
 676            DEFAULT_AUTHENTICATOR,
 677            KEY_PAIR_AUTHENTICATOR,
 678        )
 679
 680        private_key = values.get("private_key")
 681        private_key_path = values.get("private_key_path")
 682        private_key_passphrase = values.get("private_key_passphrase")
 683        user = values.get("user")
 684        password = values.get("password")
 685        auth = auth if auth and auth != DEFAULT_AUTHENTICATOR else KEY_PAIR_AUTHENTICATOR
 686
 687        if not private_key and not private_key_path:
 688            return None
 689        if private_key and private_key_path:
 690            raise ConfigError("Cannot specify both `private_key` and `private_key_path`")
 691        if auth != KEY_PAIR_AUTHENTICATOR:
 692            raise ConfigError(
 693                f"Private key or private key path can only be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
 694            )
 695        if not user:
 696            raise ConfigError(
 697                f"User must be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
 698            )
 699        if password:
 700            raise ConfigError(
 701                f"Password cannot be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
 702            )
 703
 704        if isinstance(private_key, bytes):
 705            return private_key
 706        # End Custom Code
 707
 708        if private_key_passphrase:
 709            encoded_passphrase = private_key_passphrase.encode()
 710        else:
 711            encoded_passphrase = None
 712
 713        if private_key:
 714            if private_key.startswith("-"):
 715                p_key = serialization.load_pem_private_key(
 716                    data=bytes(private_key, "utf-8"),
 717                    password=encoded_passphrase,
 718                    backend=default_backend(),
 719                )
 720
 721            else:
 722                p_key = serialization.load_der_private_key(
 723                    data=base64.b64decode(private_key),
 724                    password=encoded_passphrase,
 725                    backend=default_backend(),
 726                )
 727
 728        elif private_key_path:
 729            with open(private_key_path, "rb") as key:
 730                p_key = serialization.load_pem_private_key(
 731                    key.read(), password=encoded_passphrase, backend=default_backend()
 732                )
 733        else:
 734            return None
 735
 736        return p_key.private_bytes(
 737            encoding=serialization.Encoding.DER,
 738            format=serialization.PrivateFormat.PKCS8,
 739            encryption_algorithm=serialization.NoEncryption(),
 740        )
 741
 742    @property
 743    def _connection_kwargs_keys(self) -> t.Set[str]:
 744        return {
 745            "user",
 746            "password",
 747            "account",
 748            "warehouse",
 749            "database",
 750            "role",
 751            "authenticator",
 752            "token",
 753            "private_key",
 754            "session_parameters",
 755            "application",
 756            "host",
 757            "port",
 758        }
 759
 760    @property
 761    def _engine_adapter(self) -> t.Type[EngineAdapter]:
 762        return engine_adapter.SnowflakeEngineAdapter
 763
 764    @property
 765    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
 766        return {"autocommit": False}
 767
 768    @property
 769    def _connection_factory(self) -> t.Callable:
 770        from snowflake import connector
 771
 772        return connector.connect
 773
 774
 775class DatabricksConnectionConfig(ConnectionConfig):
 776    """
 777    Databricks connection that uses the SQL connector for SQL models and then Databricks Connect for Dataframe operations
 778
 779    Arg Source: https://github.com/databricks/databricks-sql-python/blob/main/src/databricks/sql/client.py#L39
 780    OAuth ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-machine-to-machine-m2m-authentication
 781
 782    Args:
 783        server_hostname: Databricks instance host name.
 784        http_path: Http path either to a DBSQL endpoint (e.g. /sql/1.0/endpoints/1234567890abcdef)
 785            or to a DBR interactive cluster (e.g. /sql/protocolv1/o/1234567890123456/1234-123456-slid123)
 786        access_token: Http Bearer access token, e.g. Databricks Personal Access Token.
 787        auth_type: Set to 'databricks-oauth' or 'azure-oauth' to trigger OAuth (or dont set at all to use `access_token`)
 788        oauth_client_id: Client ID to use when auth_type is set to one of the 'oauth' types
 789        oauth_client_secret: Client Secret to use when auth_type is set to one of the 'oauth' types
 790        catalog: Default catalog to use for SQL models. Defaults to None which means it will use the default set in
 791            the Databricks cluster (most likely `hive_metastore`).
 792        http_headers: An optional list of (k, v) pairs that will be set as Http headers on every request
 793        session_configuration: An optional dictionary of Spark session parameters.
 794            Execute the SQL command `SET -v` to get a full list of available commands.
 795        databricks_connect_server_hostname: The hostname to use when establishing a connecting using Databricks Connect.
 796            Defaults to the `server_hostname` value.
 797        databricks_connect_access_token: The access token to use when establishing a connecting using Databricks Connect.
 798            Defaults to the `access_token` value.
 799        databricks_connect_cluster_id: The cluster id to use when establishing a connecting using Databricks Connect.
 800            Defaults to deriving the cluster id from the `http_path` value.
 801        force_databricks_connect: Force all queries to run using Databricks Connect instead of the SQL connector.
 802        disable_databricks_connect: Even if databricks connect is installed, do not use it.
 803        disable_spark_session: Do not use SparkSession if it is available (like when running in a notebook).
 804        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
 805    """
 806
 807    server_hostname: t.Optional[str] = None
 808    http_path: t.Optional[str] = None
 809    access_token: t.Optional[str] = None
 810    auth_type: t.Optional[str] = None
 811    oauth_client_id: t.Optional[str] = None
 812    oauth_client_secret: t.Optional[str] = None
 813    catalog: t.Optional[str] = None
 814    http_headers: t.Optional[t.List[t.Tuple[str, str]]] = None
 815    session_configuration: t.Optional[t.Dict[str, t.Any]] = None
 816    databricks_connect_server_hostname: t.Optional[str] = None
 817    databricks_connect_access_token: t.Optional[str] = None
 818    databricks_connect_cluster_id: t.Optional[str] = None
 819    databricks_connect_use_serverless: bool = False
 820    force_databricks_connect: bool = False
 821    disable_databricks_connect: bool = False
 822    disable_spark_session: bool = False
 823
 824    concurrent_tasks: int = 1
 825    register_comments: bool = True
 826    pre_ping: t.Literal[False] = False
 827
 828    type_: t.Literal["databricks"] = Field(alias="type", default="databricks")
 829    DIALECT: t.ClassVar[t.Literal["databricks"]] = "databricks"
 830    DISPLAY_NAME: t.ClassVar[t.Literal["Databricks"]] = "Databricks"
 831    DISPLAY_ORDER: t.ClassVar[t.Literal[3]] = 3
 832
 833    _concurrent_tasks_validator = concurrent_tasks_validator
 834    _http_headers_validator = http_headers_validator
 835
 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
 842    @model_validator(mode="before")
 843    def _databricks_connect_validator(cls, data: t.Any) -> t.Any:
 844        # SQLQueryContextLogger will output any error SQL queries even if they are in a try/except block.
 845        # Disabling this allows SQLMesh to determine what should be shown to the user.
 846        # Ex: We describe a table to see if it exists and therefore that execution can fail but we don't need to show
 847        # the user since it is expected if the table doesn't exist. Without this change the user would see the error.
 848        logging.getLogger("SQLQueryContextLogger").setLevel(logging.CRITICAL)
 849
 850        if not isinstance(data, dict):
 851            return data
 852
 853        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 854
 855        if DatabricksEngineAdapter.can_access_spark_session(
 856            bool(data.get("disable_spark_session"))
 857        ):
 858            return data
 859
 860        databricks_connect_use_serverless = data.get("databricks_connect_use_serverless")
 861        server_hostname, http_path, access_token, auth_type = (
 862            data.get("server_hostname"),
 863            data.get("http_path"),
 864            data.get("access_token"),
 865            data.get("auth_type"),
 866        )
 867
 868        if (not server_hostname or not http_path or not access_token) and (
 869            not databricks_connect_use_serverless and not auth_type
 870        ):
 871            raise ValueError(
 872                "`server_hostname`, `http_path`, and `access_token` are required for Databricks connections when not running in a notebook"
 873            )
 874        if (
 875            databricks_connect_use_serverless
 876            and not server_hostname
 877            and not data.get("databricks_connect_server_hostname")
 878        ):
 879            raise ValueError(
 880                "`server_hostname` or `databricks_connect_server_hostname` is required when `databricks_connect_use_serverless` is set"
 881            )
 882        if DatabricksEngineAdapter.can_access_databricks_connect(
 883            bool(data.get("disable_databricks_connect"))
 884        ):
 885            if not data.get("databricks_connect_access_token"):
 886                data["databricks_connect_access_token"] = access_token
 887            if not data.get("databricks_connect_server_hostname"):
 888                data["databricks_connect_server_hostname"] = f"https://{server_hostname}"
 889            if not databricks_connect_use_serverless and not data.get(
 890                "databricks_connect_cluster_id"
 891            ):
 892                if t.TYPE_CHECKING:
 893                    assert http_path is not None
 894                data["databricks_connect_cluster_id"] = http_path.split("/")[-1]
 895
 896        if auth_type:
 897            from databricks.sql.auth.auth import AuthType
 898
 899            all_data = [m.value for m in AuthType]
 900            if auth_type not in all_data:
 901                raise ValueError(
 902                    f"`auth_type` {auth_type} does not match a valid option: {all_data}"
 903                )
 904
 905            client_id = data.get("oauth_client_id")
 906            client_secret = data.get("oauth_client_secret")
 907
 908            if client_secret and not client_id:
 909                raise ValueError(
 910                    "`oauth_client_id` is required when `oauth_client_secret` is specified"
 911                )
 912
 913            if not http_path:
 914                raise ValueError("`http_path` is still required when using `auth_type`")
 915
 916        return data
 917
 918    _engine_import_validator = _get_engine_import_validator("databricks", "databricks")
 919
 920    @property
 921    def _connection_kwargs_keys(self) -> t.Set[str]:
 922        if self.use_spark_session_only:
 923            return set()
 924        return {
 925            "server_hostname",
 926            "http_path",
 927            "access_token",
 928            "http_headers",
 929            "session_configuration",
 930            "catalog",
 931        }
 932
 933    @property
 934    def _engine_adapter(self) -> t.Type[engine_adapter.DatabricksEngineAdapter]:
 935        return engine_adapter.DatabricksEngineAdapter
 936
 937    @property
 938    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
 939        return {
 940            k: v
 941            for k, v in self.dict().items()
 942            if k.startswith("databricks_connect_")
 943            or k in ("catalog", "disable_databricks_connect", "disable_spark_session")
 944        }
 945
 946    @property
 947    def use_spark_session_only(self) -> bool:
 948        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 949
 950        return (
 951            DatabricksEngineAdapter.can_access_spark_session(self.disable_spark_session)
 952            or self.force_databricks_connect
 953        )
 954
 955    @property
 956    def _connection_factory(self) -> t.Callable:
 957        if self.use_spark_session_only:
 958            from sqlmesh.engines.spark.db_api.spark_session import connection
 959
 960            return connection
 961
 962        from databricks import sql  # type: ignore
 963
 964        return sql.connect
 965
 966    @property
 967    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
 968        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 969
 970        if not self.use_spark_session_only:
 971            conn_kwargs: t.Dict[str, t.Any] = {
 972                "_user_agent_entry": "sqlmesh",
 973            }
 974
 975            if self.auth_type and "oauth" in self.auth_type:
 976                # there are two types of oauth: User-to-Machine (U2M) and Machine-to-Machine (M2M)
 977                if self.oauth_client_secret:
 978                    # if a client_secret exists, then a client_id also exists and we are using M2M
 979                    # ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-machine-to-machine-m2m-authentication
 980                    # ref: https://github.com/databricks/databricks-sql-python/blob/main/examples/m2m_oauth.py
 981                    from databricks.sdk.core import Config, oauth_service_principal
 982
 983                    config = Config(
 984                        host=f"https://{self.server_hostname}",
 985                        client_id=self.oauth_client_id,
 986                        client_secret=self.oauth_client_secret,
 987                    )
 988                    conn_kwargs["credentials_provider"] = lambda: oauth_service_principal(config)
 989                else:
 990                    # if auth_type is set to an 'oauth' type but no client_id/secret are set, then we are using U2M
 991                    # ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-user-to-machine-u2m-authentication
 992                    conn_kwargs["auth_type"] = self.auth_type
 993
 994            return conn_kwargs
 995
 996        if DatabricksEngineAdapter.can_access_spark_session(self.disable_spark_session):
 997            from pyspark.sql import SparkSession
 998
 999            return dict(
1000                spark=SparkSession.getActiveSession(),
1001                catalog=self.catalog,
1002            )
1003
1004        from databricks.connect import DatabricksSession
1005
1006        if t.TYPE_CHECKING:
1007            assert self.databricks_connect_server_hostname is not None
1008            assert self.databricks_connect_access_token is not None
1009
1010        if self.databricks_connect_use_serverless:
1011            builder = DatabricksSession.builder.remote(
1012                host=self.databricks_connect_server_hostname,
1013                token=self.databricks_connect_access_token,
1014                serverless=True,
1015            )
1016        else:
1017            if t.TYPE_CHECKING:
1018                assert self.databricks_connect_cluster_id is not None
1019            builder = DatabricksSession.builder.remote(
1020                host=self.databricks_connect_server_hostname,
1021                token=self.databricks_connect_access_token,
1022                cluster_id=self.databricks_connect_cluster_id,
1023            )
1024
1025        return dict(
1026            spark=builder.userAgent("sqlmesh").getOrCreate(),
1027            catalog=self.catalog,
1028        )
1029
1030
1031class BigQueryConnectionMethod(str, Enum):
1032    OAUTH = "oauth"
1033    OAUTH_SECRETS = "oauth-secrets"
1034    SERVICE_ACCOUNT = "service-account"
1035    SERVICE_ACCOUNT_JSON = "service-account-json"
1036
1037
1038class BigQueryPriority(str, Enum):
1039    BATCH = "batch"
1040    INTERACTIVE = "interactive"
1041
1042    @property
1043    def is_batch(self) -> bool:
1044        return self == self.BATCH
1045
1046    @property
1047    def is_interactive(self) -> bool:
1048        return self == self.INTERACTIVE
1049
1050    @property
1051    def bigquery_constant(self) -> str:
1052        from google.cloud.bigquery import QueryPriority
1053
1054        if self.is_batch:
1055            return QueryPriority.BATCH
1056        return QueryPriority.INTERACTIVE
1057
1058
1059class BigQueryConnectionConfig(ConnectionConfig):
1060    """
1061    BigQuery Connection Configuration.
1062    """
1063
1064    method: BigQueryConnectionMethod = BigQueryConnectionMethod.OAUTH
1065
1066    project: t.Optional[str] = None
1067    execution_project: t.Optional[str] = None
1068    quota_project: t.Optional[str] = None
1069    location: t.Optional[str] = None
1070    # Keyfile Auth
1071    keyfile: t.Optional[str] = None
1072    keyfile_json: t.Optional[t.Dict[str, t.Any]] = None
1073    # Oath Secret Auth
1074    token: t.Optional[str] = None
1075    refresh_token: t.Optional[str] = None
1076    client_id: t.Optional[str] = None
1077    client_secret: t.Optional[str] = None
1078    token_uri: t.Optional[str] = None
1079    scopes: t.Tuple[str, ...] = ("https://www.googleapis.com/auth/bigquery",)
1080    impersonated_service_account: t.Optional[str] = None
1081    # Extra Engine Config
1082    job_creation_timeout_seconds: t.Optional[int] = None
1083    job_execution_timeout_seconds: t.Optional[int] = None
1084    job_retries: t.Optional[int] = 1
1085    job_retry_deadline_seconds: t.Optional[int] = None
1086    priority: t.Optional[BigQueryPriority] = None
1087    maximum_bytes_billed: t.Optional[int] = None
1088    reservation: t.Optional[str] = None
1089
1090    concurrent_tasks: int = 1
1091    register_comments: bool = True
1092    pre_ping: t.Literal[False] = False
1093
1094    type_: t.Literal["bigquery"] = Field(alias="type", default="bigquery")
1095    DIALECT: t.ClassVar[t.Literal["bigquery"]] = "bigquery"
1096    DISPLAY_NAME: t.ClassVar[t.Literal["BigQuery"]] = "BigQuery"
1097    DISPLAY_ORDER: t.ClassVar[t.Literal[4]] = 4
1098
1099    _engine_import_validator = _get_engine_import_validator("google.cloud.bigquery", "bigquery")
1100
1101    @field_validator("execution_project")
1102    def validate_execution_project(
1103        cls,
1104        v: t.Optional[str],
1105        info: ValidationInfo,
1106    ) -> t.Optional[str]:
1107        if v and not validation_data(info).get("project"):
1108            raise ConfigError(
1109                "If the `execution_project` field is specified, you must also specify the `project` field to provide a default object location."
1110            )
1111        return v
1112
1113    @field_validator("quota_project")
1114    def validate_quota_project(
1115        cls,
1116        v: t.Optional[str],
1117        info: ValidationInfo,
1118    ) -> t.Optional[str]:
1119        if v and not validation_data(info).get("project"):
1120            raise ConfigError(
1121                "If the `quota_project` field is specified, you must also specify the `project` field to provide a default object location."
1122            )
1123        return v
1124
1125    @property
1126    def _connection_kwargs_keys(self) -> t.Set[str]:
1127        return set()
1128
1129    @property
1130    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1131        return engine_adapter.BigQueryEngineAdapter
1132
1133    @property
1134    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
1135        """The static connection kwargs for this connection"""
1136        import google.auth
1137        from google.api_core import client_info, client_options
1138        from google.auth import impersonated_credentials
1139        from google.oauth2 import credentials, service_account
1140
1141        if self.method == BigQueryConnectionMethod.OAUTH:
1142            creds, _ = google.auth.default(scopes=self.scopes)
1143        elif self.method == BigQueryConnectionMethod.SERVICE_ACCOUNT:
1144            creds = service_account.Credentials.from_service_account_file(
1145                self.keyfile, scopes=self.scopes
1146            )
1147        elif self.method == BigQueryConnectionMethod.SERVICE_ACCOUNT_JSON:
1148            creds = service_account.Credentials.from_service_account_info(
1149                self.keyfile_json, scopes=self.scopes
1150            )
1151        elif self.method == BigQueryConnectionMethod.OAUTH_SECRETS:
1152            creds = credentials.Credentials(
1153                token=self.token,
1154                refresh_token=self.refresh_token,
1155                client_id=self.client_id,
1156                client_secret=self.client_secret,
1157                token_uri=self.token_uri,
1158                scopes=self.scopes,
1159            )
1160        else:
1161            raise ConfigError("Invalid BigQuery Connection Method")
1162
1163        if self.impersonated_service_account:
1164            creds = impersonated_credentials.Credentials(
1165                source_credentials=creds,
1166                target_principal=self.impersonated_service_account,
1167                target_scopes=self.scopes,
1168            )
1169
1170        options = client_options.ClientOptions(quota_project_id=self.quota_project)
1171        project = self.execution_project or self.project or None
1172
1173        client = google.cloud.bigquery.Client(
1174            project=project and exp.parse_identifier(project, dialect="bigquery").name,
1175            credentials=creds,
1176            location=self.location,
1177            client_info=client_info.ClientInfo(user_agent="sqlmesh"),
1178            client_options=options,
1179        )
1180
1181        return {
1182            "client": client,
1183        }
1184
1185    @property
1186    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1187        return {
1188            k: v
1189            for k, v in self.dict().items()
1190            if k
1191            in {
1192                "job_creation_timeout_seconds",
1193                "job_execution_timeout_seconds",
1194                "job_retries",
1195                "job_retry_deadline_seconds",
1196                "priority",
1197                "maximum_bytes_billed",
1198                "reservation",
1199            }
1200        }
1201
1202    @property
1203    def _connection_factory(self) -> t.Callable:
1204        from google.cloud.bigquery.dbapi import connect
1205
1206        return connect
1207
1208    def get_catalog(self) -> t.Optional[str]:
1209        return self.project
1210
1211
1212class GCPPostgresConnectionConfig(ConnectionConfig):
1213    """
1214    Postgres Connection Configuration for GCP.
1215
1216    Args:
1217        instance_connection_string: Connection name for the postgres instance.
1218        user: Postgres or IAM user's name
1219        password: The postgres user's password. Only needed when the user is a postgres user.
1220        enable_iam_auth: Set to True when user is an IAM user.
1221        db: Name of the db to connect to.
1222        keyfile: string path to json service account credentials file
1223        keyfile_json: dict service account credentials info
1224        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
1225    """
1226
1227    instance_connection_string: str
1228    user: str
1229    password: t.Optional[str] = None
1230    enable_iam_auth: t.Optional[bool] = None
1231    db: str
1232    ip_type: t.Union[t.Literal["public"], t.Literal["private"], t.Literal["psc"]] = "public"
1233    # Keyfile Auth
1234    keyfile: t.Optional[str] = None
1235    keyfile_json: t.Optional[t.Dict[str, t.Any]] = None
1236    timeout: t.Optional[int] = None
1237    scopes: t.Tuple[str, ...] = ("https://www.googleapis.com/auth/sqlservice.admin",)
1238    driver: str = "pg8000"
1239
1240    type_: t.Literal["gcp_postgres"] = Field(alias="type", default="gcp_postgres")
1241    DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres"
1242    DISPLAY_NAME: t.ClassVar[t.Literal["GCP Postgres"]] = "GCP Postgres"
1243    DISPLAY_ORDER: t.ClassVar[t.Literal[13]] = 13
1244
1245    concurrent_tasks: int = 4
1246    register_comments: bool = True
1247    pre_ping: bool = True
1248
1249    _engine_import_validator = _get_engine_import_validator(
1250        "google.cloud.sql", "gcp_postgres", "gcppostgres"
1251    )
1252
1253    @model_validator(mode="before")
1254    def _validate_auth_method(cls, data: t.Any) -> t.Any:
1255        if not isinstance(data, dict):
1256            return data
1257
1258        password = data.get("password")
1259        enable_iam_auth = data.get("enable_iam_auth")
1260
1261        if not password and not enable_iam_auth:
1262            raise ConfigError(
1263                "GCP Postgres connection configuration requires either password set"
1264                " for a postgres user account or enable_iam_auth set to 'True'"
1265                " for an IAM user account."
1266            )
1267
1268        return data
1269
1270    @property
1271    def _connection_kwargs_keys(self) -> t.Set[str]:
1272        return {
1273            "instance_connection_string",
1274            "driver",
1275            "user",
1276            "password",
1277            "db",
1278            "enable_iam_auth",
1279            "timeout",
1280        }
1281
1282    @property
1283    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1284        return engine_adapter.PostgresEngineAdapter
1285
1286    @property
1287    def _connection_factory(self) -> t.Callable:
1288        from google.cloud.sql.connector import Connector
1289        from google.oauth2 import service_account
1290
1291        creds = None
1292        if self.keyfile:
1293            creds = service_account.Credentials.from_service_account_file(
1294                self.keyfile, scopes=self.scopes
1295            )
1296        elif self.keyfile_json:
1297            creds = service_account.Credentials.from_service_account_info(
1298                self.keyfile_json, scopes=self.scopes
1299            )
1300
1301        kwargs = {
1302            "credentials": creds,
1303            "ip_type": self.ip_type,
1304        }
1305
1306        if self.timeout:
1307            kwargs["timeout"] = self.timeout
1308
1309        return Connector(**kwargs).connect  # type: ignore
1310
1311
1312class RedshiftConnectionConfig(ConnectionConfig):
1313    """
1314    Redshift Connection Configuration.
1315
1316    Arg Source: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/__init__.py#L146
1317    Note: A subset of properties were selected. Please open an issue/PR if you want to see more supported.
1318
1319    Args:
1320        user: The username to use for authentication with the Amazon Redshift cluster.
1321        password: The password to use for authentication with the Amazon Redshift cluster.
1322        database: The name of the database instance to connect to.
1323        host: The hostname of the Amazon Redshift cluster.
1324        port: The port number of the Amazon Redshift cluster. Default value is 5439.
1325        source_address: No description provided
1326        unix_sock: No description provided
1327        ssl: Is SSL enabled. Default value is ``True``. SSL must be enabled when authenticating using IAM.
1328        sslmode: The security of the connection to the Amazon Redshift cluster. 'verify-ca' and 'verify-full' are supported.
1329        timeout: The number of seconds before the connection to the server will timeout. By default there is no timeout.
1330        tcp_keepalive: Is `TCP keepalive <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_ used. The default value is ``True``.
1331        application_name: Sets the application name. The default value is None.
1332        preferred_role: The IAM role preferred for the current connection.
1333        principal_arn: The ARN of the IAM entity (user or role) for which you are generating a policy.
1334        credentials_provider: The class name of the IdP that will be used for authenticating with the Amazon Redshift cluster.
1335        region: The AWS region where the Amazon Redshift cluster is located.
1336        cluster_identifier: The cluster identifier of the Amazon Redshift cluster.
1337        iam: If IAM authentication is enabled. Default value is False. IAM must be True when authenticating using an IdP.
1338        db_user: The database user to authenticate as. Required when using IAM authentication.
1339        is_serverless: Redshift end-point is serverless or provisional. Default value false.
1340        serverless_acct_id: The account ID of the serverless. Default value None
1341        serverless_work_group: The name of work group for serverless end point. Default value None.
1342        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
1343        enable_merge: Whether to use the Redshift merge operation instead of the SQLMesh logical merge.
1344    """
1345
1346    user: t.Optional[str] = None
1347    password: t.Optional[str] = None
1348    database: t.Optional[str] = None
1349    host: t.Optional[str] = None
1350    port: t.Optional[int] = None
1351    source_address: t.Optional[str] = None
1352    unix_sock: t.Optional[str] = None
1353    ssl: t.Optional[bool] = None
1354    sslmode: t.Optional[str] = None
1355    timeout: t.Optional[int] = None
1356    tcp_keepalive: t.Optional[bool] = None
1357    application_name: t.Optional[str] = None
1358    preferred_role: t.Optional[str] = None
1359    principal_arn: t.Optional[str] = None
1360    credentials_provider: t.Optional[str] = None
1361    region: t.Optional[str] = None
1362    cluster_identifier: t.Optional[str] = None
1363    iam: t.Optional[bool] = None
1364    db_user: t.Optional[str] = None
1365    is_serverless: t.Optional[bool] = None
1366    serverless_acct_id: t.Optional[str] = None
1367    serverless_work_group: t.Optional[str] = None
1368    enable_merge: t.Optional[bool] = None
1369
1370    concurrent_tasks: int = 4
1371    register_comments: bool = True
1372    pre_ping: bool = False
1373
1374    type_: t.Literal["redshift"] = Field(alias="type", default="redshift")
1375    DIALECT: t.ClassVar[t.Literal["redshift"]] = "redshift"
1376    DISPLAY_NAME: t.ClassVar[t.Literal["Redshift"]] = "Redshift"
1377    DISPLAY_ORDER: t.ClassVar[t.Literal[7]] = 7
1378
1379    _engine_import_validator = _get_engine_import_validator("redshift_connector", "redshift")
1380
1381    @property
1382    def _connection_kwargs_keys(self) -> t.Set[str]:
1383        return {
1384            "user",
1385            "password",
1386            "database",
1387            "host",
1388            "port",
1389            "source_address",
1390            "unix_sock",
1391            "ssl",
1392            "sslmode",
1393            "timeout",
1394            "tcp_keepalive",
1395            "application_name",
1396            "preferred_role",
1397            "principal_arn",
1398            "credentials_provider",
1399            "region",
1400            "cluster_identifier",
1401            "iam",
1402            "db_user",
1403            "is_serverless",
1404            "serverless_acct_id",
1405            "serverless_work_group",
1406        }
1407
1408    @property
1409    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1410        return engine_adapter.RedshiftEngineAdapter
1411
1412    @property
1413    def _connection_factory(self) -> t.Callable:
1414        from redshift_connector import connect
1415
1416        return connect
1417
1418    @property
1419    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1420        return {"enable_merge": self.enable_merge}
1421
1422
1423class PostgresConnectionConfig(ConnectionConfig):
1424    host: str
1425    user: str
1426    password: str
1427    port: int
1428    database: str
1429    keepalives_idle: t.Optional[int] = None
1430    connect_timeout: int = 10
1431    role: t.Optional[str] = None
1432    sslmode: t.Optional[str] = None
1433    application_name: t.Optional[str] = None
1434
1435    concurrent_tasks: int = 4
1436    register_comments: bool = True
1437    pre_ping: bool = True
1438
1439    type_: t.Literal["postgres"] = Field(alias="type", default="postgres")
1440    DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres"
1441    DISPLAY_NAME: t.ClassVar[t.Literal["Postgres"]] = "Postgres"
1442    DISPLAY_ORDER: t.ClassVar[t.Literal[12]] = 12
1443
1444    _engine_import_validator = _get_engine_import_validator("psycopg2", "postgres")
1445
1446    @property
1447    def _connection_kwargs_keys(self) -> t.Set[str]:
1448        return {
1449            "host",
1450            "user",
1451            "password",
1452            "port",
1453            "database",
1454            "keepalives_idle",
1455            "connect_timeout",
1456            "sslmode",
1457            "application_name",
1458        }
1459
1460    @property
1461    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1462        return engine_adapter.PostgresEngineAdapter
1463
1464    @property
1465    def _connection_factory(self) -> t.Callable:
1466        from psycopg2 import connect
1467
1468        return connect
1469
1470    @property
1471    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
1472        if not self.role:
1473            return None
1474
1475        def init(cursor: t.Any) -> None:
1476            cursor.execute(f"SET ROLE {self.role}")
1477
1478        return init
1479
1480
1481class MySQLConnectionConfig(ConnectionConfig):
1482    host: str
1483    user: str
1484    password: str
1485    port: t.Optional[int] = None
1486    database: t.Optional[str] = None
1487    charset: t.Optional[str] = None
1488    collation: t.Optional[str] = None
1489    ssl_disabled: t.Optional[bool] = None
1490
1491    concurrent_tasks: int = 4
1492    register_comments: bool = True
1493    pre_ping: bool = True
1494
1495    type_: t.Literal["mysql"] = Field(alias="type", default="mysql")
1496    DIALECT: t.ClassVar[t.Literal["mysql"]] = "mysql"
1497    DISPLAY_NAME: t.ClassVar[t.Literal["MySQL"]] = "MySQL"
1498    DISPLAY_ORDER: t.ClassVar[t.Literal[14]] = 14
1499
1500    _engine_import_validator = _get_engine_import_validator("pymysql", "mysql")
1501
1502    @property
1503    def _connection_kwargs_keys(self) -> t.Set[str]:
1504        connection_keys = {
1505            "host",
1506            "user",
1507            "password",
1508        }
1509        if self.port is not None:
1510            connection_keys.add("port")
1511        if self.database is not None:
1512            connection_keys.add("database")
1513        if self.charset is not None:
1514            connection_keys.add("charset")
1515        if self.collation is not None:
1516            connection_keys.add("collation")
1517        if self.ssl_disabled is not None:
1518            connection_keys.add("ssl_disabled")
1519        return connection_keys
1520
1521    @property
1522    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1523        return engine_adapter.MySQLEngineAdapter
1524
1525    @property
1526    def _connection_factory(self) -> t.Callable:
1527        from pymysql import connect
1528
1529        return connect
1530
1531
1532class MSSQLConnectionConfig(ConnectionConfig):
1533    host: str
1534    user: t.Optional[str] = None
1535    password: t.Optional[str] = None
1536    database: t.Optional[str] = ""
1537    timeout: t.Optional[int] = 0
1538    login_timeout: t.Optional[int] = 60
1539    login_attempts: t.Optional[int] = 1
1540    charset: t.Optional[str] = "UTF-8"
1541    appname: t.Optional[str] = None
1542    port: t.Optional[int] = 1433
1543    conn_properties: t.Optional[t.Union[t.List[str], str]] = None
1544    autocommit: t.Optional[bool] = False
1545    tds_version: t.Optional[str] = None
1546
1547    # Driver options
1548    driver: t.Literal["pymssql", "pyodbc", "mssql-python"] = "pymssql"
1549    # PyODBC specific options
1550    driver_name: t.Optional[str] = None  # e.g. "ODBC Driver 18 for SQL Server"
1551    trust_server_certificate: t.Optional[bool] = None
1552    encrypt: t.Optional[bool] = None
1553    # Dictionary of arbitrary ODBC connection properties
1554    # See: https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute
1555    odbc_properties: t.Optional[t.Dict[str, t.Any]] = None
1556
1557    concurrent_tasks: int = 4
1558    register_comments: bool = True
1559    pre_ping: bool = True
1560
1561    type_: t.Literal["mssql"] = Field(alias="type", default="mssql")
1562    DIALECT: t.ClassVar[t.Literal["tsql"]] = "tsql"
1563    DISPLAY_NAME: t.ClassVar[t.Literal["MSSQL"]] = "MSSQL"
1564    DISPLAY_ORDER: t.ClassVar[t.Literal[11]] = 11
1565
1566    @model_validator(mode="before")
1567    @classmethod
1568    def _mssql_engine_import_validator(cls, data: t.Any) -> t.Any:
1569        if not isinstance(data, dict):
1570            return data
1571
1572        driver = data.get("driver", "pymssql")
1573
1574        # Define the mapping of driver to import module and extra name
1575        driver_configs = {
1576            "pymssql": ("pymssql", "mssql"),
1577            "pyodbc": ("pyodbc", "mssql-odbc"),
1578            "mssql-python": ("mssql_python", "mssql-python"),
1579        }
1580
1581        if driver not in driver_configs:
1582            raise ValueError(f"Unsupported driver: {driver}")
1583
1584        import_module, extra_name = driver_configs[driver]
1585
1586        # Use _get_engine_import_validator with decorate=False to get the raw validation function
1587        # This avoids the __wrapped__ issue in Python 3.9
1588        validator_func = _get_engine_import_validator(
1589            import_module, driver, extra_name, decorate=False
1590        )
1591
1592        # Call the raw validation function directly
1593        return validator_func(cls, data)
1594
1595    @property
1596    def _connection_kwargs_keys(self) -> t.Set[str]:
1597        base_keys = {
1598            "host",
1599            "user",
1600            "password",
1601            "database",
1602            "timeout",
1603            "login_timeout",
1604            "charset",
1605            "appname",
1606            "port",
1607            "conn_properties",
1608            "autocommit",
1609            "tds_version",
1610        }
1611
1612        if self.driver == "pyodbc":
1613            base_keys.update(
1614                {
1615                    "driver_name",
1616                    "trust_server_certificate",
1617                    "encrypt",
1618                    "odbc_properties",
1619                }
1620            )
1621            # Remove pymssql-specific parameters
1622            base_keys.discard("tds_version")
1623            base_keys.discard("conn_properties")
1624
1625        elif self.driver == "mssql-python":
1626            base_keys.update(
1627                {
1628                    "trust_server_certificate",
1629                    "encrypt",
1630                    "odbc_properties",
1631                    "login_attempts",
1632                }
1633            )
1634            # Remove pymssql-specific parameters
1635            base_keys.discard("tds_version")
1636            base_keys.discard("conn_properties")
1637
1638        return base_keys
1639
1640    @property
1641    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1642        return engine_adapter.MSSQLEngineAdapter
1643
1644    @property
1645    def _connection_factory(self) -> t.Callable:
1646        if self.driver == "pymssql":
1647            import pymssql
1648
1649            return pymssql.connect
1650
1651        if self.driver == "mssql-python":
1652            # The `mssql-python` implementation is API-compatible with
1653            # with the `pyodbc` equivalent for documented parameters.
1654
1655            if not SUPPORTS_MSSQL_PYTHON_DRIVER:
1656                raise ConfigError("The `mssql-python` driver requires Python 3.10 or higher.")
1657
1658            import mssql_python
1659
1660            def connect_mssql_python(**kwargs: t.Any) -> t.Callable:
1661                # Extract parameters for connection string
1662                host = kwargs.pop("host")
1663                port = kwargs.pop("port", 1433)
1664                database = kwargs.pop("database", "")
1665                user = kwargs.pop("user", None)
1666                password = kwargs.pop("password", None)
1667                authentication = kwargs.pop("authentication", None)
1668                trust_server_certificate = kwargs.pop("trust_server_certificate", False)
1669                encrypt = kwargs.pop("encrypt", True)
1670                timeout = kwargs.pop("timeout", 0)
1671                login_timeout = kwargs.pop("login_timeout", 59)
1672                login_attempts = kwargs.pop("login_attempts", 1)
1673
1674                # Build connection string
1675                conn_str_parts = [
1676                    f"Server={host},{port}",
1677                ]
1678
1679                if database:
1680                    conn_str_parts.append(f"Database={database}")
1681
1682                # Add security options
1683                conn_str_parts.append(f"Encrypt={'yes' if encrypt else 'no'}")
1684                if trust_server_certificate:
1685                    conn_str_parts.append("TrustServerCertificate=yes")
1686
1687                # `Connection Timeout=` is not a valid option so we leverage `ConnectRetry*`.
1688                # See the following:
1689                # - https://github.com/microsoft/mssql-python/issues/339
1690                # - https://github.com/microsoft/mssql-python/wiki/Connection-to-SQL-Database
1691                # - https://github.com/microsoft/mssql-python/wiki/Connection#timeout
1692                conn_str_parts.append(f"ConnectRetryCount={login_attempts}")
1693                conn_str_parts.append(f"ConnectRetryInterval={min(int(login_timeout), 60)}")
1694
1695                # Standard SQL Server authentication
1696                if user:
1697                    conn_str_parts.append(f"UID={user}")
1698                if password:
1699                    conn_str_parts.append(f"PWD={password}")
1700                if authentication:
1701                    conn_str_parts.append(f"Authentication={authentication}")
1702
1703                # Add any additional ODBC properties from the odbc_properties dictionary
1704                if self.odbc_properties:
1705                    for key, value in self.odbc_properties.items():
1706                        # Skip properties that we've already set above
1707                        if key.lower() in (
1708                            "driver",
1709                            "server",
1710                            "database",
1711                            "uid",
1712                            "pwd",
1713                            "encrypt",
1714                            "trustservercertificate",
1715                            "connectretrycount",
1716                            "connectretryinterval",
1717                            "connection timeout",
1718                        ):
1719                            continue
1720
1721                        # Handle boolean values properly
1722                        if isinstance(value, bool):
1723                            conn_str_parts.append(f"{key}={'yes' if value else 'no'}")
1724                        else:
1725                            conn_str_parts.append(f"{key}={value}")
1726
1727                # Create the connection
1728                conn_str = ";".join(conn_str_parts)
1729
1730                conn = mssql_python.connect(
1731                    conn_str,
1732                    autocommit=kwargs.get("autocommit", False),
1733                    timeout=timeout,
1734                )
1735
1736                # TODO: Remove this output converter as DATETIMEOFFSET
1737                # should be handled natively by `mssql-python`.
1738                # see "https://github.com/microsoft/mssql-python/issues/213"
1739
1740                def handle_datetimeoffset_mssql_python(dto_value: t.Any) -> t.Any:
1741                    import struct
1742                    from datetime import datetime, timedelta, timezone
1743
1744                    # Unpack the DATETIMEOFFSET binary format:
1745                    # Format: <6hI2h = (year, month, day, hour, minute, second, nanoseconds, tz_hour_offset, tz_minute_offset)
1746                    tup = struct.unpack("<6hI2h", dto_value)
1747                    return datetime(
1748                        tup[0],
1749                        tup[1],
1750                        tup[2],
1751                        tup[3],
1752                        tup[4],
1753                        tup[5],
1754                        tup[6] // 1000,
1755                        timezone(timedelta(hours=tup[7], minutes=tup[8])),
1756                    )
1757
1758                conn.add_output_converter(-155, handle_datetimeoffset_mssql_python)
1759
1760                return t.cast(t.Any, conn)
1761
1762            return connect_mssql_python
1763
1764        if self.driver == "pyodbc":
1765
1766            def connect_pyodbc(**kwargs: t.Any) -> t.Callable:
1767                # Extract parameters for connection string
1768                host = kwargs.pop("host")
1769                port = kwargs.pop("port", 1433)
1770                database = kwargs.pop("database", "")
1771                user = kwargs.pop("user", None)
1772                password = kwargs.pop("password", None)
1773                driver_name = kwargs.pop("driver_name", "ODBC Driver 18 for SQL Server")
1774                trust_server_certificate = kwargs.pop("trust_server_certificate", False)
1775                encrypt = kwargs.pop("encrypt", True)
1776                login_timeout = kwargs.pop("login_timeout", 60)
1777
1778                # Build connection string
1779                conn_str_parts = [
1780                    f"DRIVER={{{driver_name}}}",
1781                    f"SERVER={host},{port}",
1782                ]
1783
1784                if database:
1785                    conn_str_parts.append(f"DATABASE={database}")
1786
1787                # Add security options
1788                conn_str_parts.append(f"Encrypt={'YES' if encrypt else 'NO'}")
1789                if trust_server_certificate:
1790                    conn_str_parts.append("TrustServerCertificate=YES")
1791
1792                conn_str_parts.append(f"Connection Timeout={login_timeout}")
1793
1794                # Standard SQL Server authentication
1795                if user:
1796                    conn_str_parts.append(f"UID={user}")
1797                if password:
1798                    conn_str_parts.append(f"PWD={password}")
1799
1800                # Add any additional ODBC properties from the odbc_properties dictionary
1801                if self.odbc_properties:
1802                    for key, value in self.odbc_properties.items():
1803                        # Skip properties that we've already set above
1804                        if key.lower() in (
1805                            "driver",
1806                            "server",
1807                            "database",
1808                            "uid",
1809                            "pwd",
1810                            "encrypt",
1811                            "trustservercertificate",
1812                            "connection timeout",
1813                        ):
1814                            continue
1815
1816                        # Handle boolean values properly
1817                        if isinstance(value, bool):
1818                            conn_str_parts.append(f"{key}={'YES' if value else 'NO'}")
1819                        else:
1820                            conn_str_parts.append(f"{key}={value}")
1821
1822                # Create the connection
1823                conn_str = ";".join(conn_str_parts)
1824
1825                import pyodbc
1826
1827                conn = pyodbc.connect(conn_str, autocommit=kwargs.get("autocommit", False))
1828
1829                # Set up output converters for MSSQL-specific data types
1830                # Handle SQL type -155 (DATETIMEOFFSET) which is not yet supported by pyodbc
1831                # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
1832                def handle_datetimeoffset_pyodbc(dto_value: t.Any) -> t.Any:
1833                    import struct
1834                    from datetime import datetime, timedelta, timezone
1835
1836                    # Unpack the DATETIMEOFFSET binary format:
1837                    # Format: <6hI2h = (year, month, day, hour, minute, second, nanoseconds, tz_hour_offset, tz_minute_offset)
1838                    tup = struct.unpack("<6hI2h", dto_value)
1839                    return datetime(
1840                        tup[0],
1841                        tup[1],
1842                        tup[2],
1843                        tup[3],
1844                        tup[4],
1845                        tup[5],
1846                        tup[6] // 1000,
1847                        timezone(timedelta(hours=tup[7], minutes=tup[8])),
1848                    )
1849
1850                conn.add_output_converter(-155, handle_datetimeoffset_pyodbc)
1851
1852                return t.cast(t.Any, conn)
1853
1854            return connect_pyodbc
1855
1856        raise ValueError(f"Unsupported driver: {self.driver}")
1857
1858    @property
1859    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1860        return {"catalog_support": CatalogSupport.REQUIRES_SET_CATALOG}
1861
1862
1863class AzureSQLConnectionConfig(MSSQLConnectionConfig):
1864    type_: t.Literal["azuresql"] = Field(alias="type", default="azuresql")  # type: ignore
1865    DISPLAY_NAME: t.ClassVar[t.Literal["Azure SQL"]] = "Azure SQL"  # type: ignore
1866    DISPLAY_ORDER: t.ClassVar[t.Literal[10]] = 10  # type: ignore
1867
1868    @property
1869    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1870        return {"catalog_support": CatalogSupport.SINGLE_CATALOG_ONLY}
1871
1872
1873class FabricConnectionConfig(MSSQLConnectionConfig):
1874    """
1875    Fabric Connection Configuration.
1876    Inherits most settings from MSSQLConnectionConfig and sets the type to 'fabric'.
1877    It is recommended to use the 'pyodbc' driver for Fabric.
1878    """
1879
1880    type_: t.Literal["fabric"] = Field(alias="type", default="fabric")  # type: ignore
1881    DIALECT: t.ClassVar[t.Literal["fabric"]] = "fabric"  # type: ignore
1882    DISPLAY_NAME: t.ClassVar[t.Literal["Fabric"]] = "Fabric"  # type: ignore
1883    DISPLAY_ORDER: t.ClassVar[t.Literal[17]] = 17  # type: ignore
1884    driver: t.Literal["pyodbc", "mssql-python"] = "pyodbc"
1885    workspace_id: str
1886    tenant_id: str
1887    autocommit: t.Optional[bool] = True
1888
1889    @property
1890    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1891        from sqlmesh.core.engine_adapter.fabric import FabricEngineAdapter
1892
1893        return FabricEngineAdapter
1894
1895    @property
1896    def _connection_factory(self) -> t.Callable:
1897        # Override to support catalog switching for Fabric
1898        base_factory = super()._connection_factory
1899
1900        def create_fabric_connection(
1901            target_catalog: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any
1902        ) -> t.Callable:
1903            kwargs["database"] = target_catalog or self.database
1904            return base_factory(*args, **kwargs)
1905
1906        return create_fabric_connection
1907
1908    @property
1909    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1910        return {
1911            "database": self.database,
1912            # more operations than not require a specific catalog to be already active
1913            # in particular, create/drop view, create/drop schema and querying information_schema
1914            "catalog_support": CatalogSupport.REQUIRES_SET_CATALOG,
1915            "workspace_id": self.workspace_id,
1916            "tenant_id": self.tenant_id,
1917            "user": self.user,
1918            "password": self.password,
1919        }
1920
1921
1922class SparkConnectionConfig(ConnectionConfig):
1923    """
1924    Vanilla Spark Connection Configuration. Use `DatabricksConnectionConfig` for Databricks.
1925    """
1926
1927    config_dir: t.Optional[str] = None
1928    catalog: t.Optional[str] = None
1929    config: t.Dict[str, t.Any] = {}
1930    wap_enabled: bool = False
1931
1932    concurrent_tasks: int = 4
1933    register_comments: bool = True
1934    pre_ping: t.Literal[False] = False
1935
1936    type_: t.Literal["spark"] = Field(alias="type", default="spark")
1937    DIALECT: t.ClassVar[t.Literal["spark"]] = "spark"
1938    DISPLAY_NAME: t.ClassVar[t.Literal["Spark"]] = "Spark"
1939    DISPLAY_ORDER: t.ClassVar[t.Literal[8]] = 8
1940
1941    _engine_import_validator = _get_engine_import_validator("pyspark", "spark")
1942
1943    @property
1944    def _connection_kwargs_keys(self) -> t.Set[str]:
1945        return {
1946            "catalog",
1947        }
1948
1949    @property
1950    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1951        return engine_adapter.SparkEngineAdapter
1952
1953    @property
1954    def _connection_factory(self) -> t.Callable:
1955        from sqlmesh.engines.spark.db_api.spark_session import connection
1956
1957        return connection
1958
1959    @property
1960    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
1961        from pyspark.conf import SparkConf
1962        from pyspark.sql import SparkSession
1963
1964        spark_config = SparkConf()
1965        if self.config:
1966            for k, v in self.config.items():
1967                spark_config.set(k, v)
1968
1969        if self.config_dir:
1970            os.environ["SPARK_CONF_DIR"] = self.config_dir
1971        return {
1972            "spark": SparkSession.builder.config(conf=spark_config)
1973            .enableHiveSupport()
1974            .getOrCreate(),
1975        }
1976
1977    @property
1978    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1979        return {"wap_enabled": self.wap_enabled}
1980
1981
1982class TrinoAuthenticationMethod(str, Enum):
1983    NO_AUTH = "no-auth"
1984    BASIC = "basic"
1985    LDAP = "ldap"
1986    KERBEROS = "kerberos"
1987    JWT = "jwt"
1988    CERTIFICATE = "certificate"
1989    OAUTH = "oauth"
1990
1991    @property
1992    def is_no_auth(self) -> bool:
1993        return self == self.NO_AUTH
1994
1995    @property
1996    def is_basic(self) -> bool:
1997        return self == self.BASIC
1998
1999    @property
2000    def is_ldap(self) -> bool:
2001        return self == self.LDAP
2002
2003    @property
2004    def is_kerberos(self) -> bool:
2005        return self == self.KERBEROS
2006
2007    @property
2008    def is_jwt(self) -> bool:
2009        return self == self.JWT
2010
2011    @property
2012    def is_certificate(self) -> bool:
2013        return self == self.CERTIFICATE
2014
2015    @property
2016    def is_oauth(self) -> bool:
2017        return self == self.OAUTH
2018
2019
2020class TrinoConnectionConfig(ConnectionConfig):
2021    method: TrinoAuthenticationMethod = TrinoAuthenticationMethod.NO_AUTH
2022    host: str
2023    user: str
2024    catalog: str
2025    port: t.Optional[int] = None
2026    http_scheme: t.Literal["http", "https"] = "https"
2027    # General Optional
2028    roles: t.Optional[t.Dict[str, str]] = None
2029    http_headers: t.Optional[t.Dict[str, str]] = None
2030    session_properties: t.Optional[t.Dict[str, str]] = None
2031    retries: int = 3
2032    timezone: t.Optional[str] = None
2033    # Basic/LDAP
2034    password: t.Optional[str] = None
2035    verify: t.Optional[bool] = None  # disable SSL verification (ignored if `cert` is provided)
2036    # LDAP
2037    impersonation_user: t.Optional[str] = None
2038    # Kerberos
2039    keytab: t.Optional[str] = None
2040    krb5_config: t.Optional[str] = None
2041    principal: t.Optional[str] = None
2042    service_name: str = "trino"
2043    hostname_override: t.Optional[str] = None
2044    mutual_authentication: bool = False
2045    force_preemptive: bool = False
2046    sanitize_mutual_error_response: bool = True
2047    delegate: bool = False
2048    # JWT
2049    jwt_token: t.Optional[str] = None
2050    # Certificate
2051    client_certificate: t.Optional[str] = None
2052    client_private_key: t.Optional[str] = None
2053    cert: t.Optional[str] = None
2054    source: str = "sqlmesh"
2055
2056    # SQLMesh options
2057    schema_location_mapping: t.Optional[dict[re.Pattern, str]] = None
2058    timestamp_mapping: t.Optional[dict[exp.DataType, exp.DataType]] = None
2059    concurrent_tasks: int = 4
2060    register_comments: bool = True
2061    pre_ping: t.Literal[False] = False
2062
2063    type_: t.Literal["trino"] = Field(alias="type", default="trino")
2064    DIALECT: t.ClassVar[t.Literal["trino"]] = "trino"
2065    DISPLAY_NAME: t.ClassVar[t.Literal["Trino"]] = "Trino"
2066    DISPLAY_ORDER: t.ClassVar[t.Literal[9]] = 9
2067
2068    _engine_import_validator = _get_engine_import_validator("trino", "trino")
2069
2070    @field_validator("schema_location_mapping", mode="before")
2071    @classmethod
2072    def _validate_regex_keys(
2073        cls, value: t.Dict[str | re.Pattern, str]
2074    ) -> t.Dict[re.Pattern, t.Any]:
2075        compiled = compile_regex_mapping(value)
2076        for replacement in compiled.values():
2077            if "@{schema_name}" not in replacement:
2078                raise ConfigError(
2079                    "schema_location_mapping needs to include the '@{schema_name}' placeholder in the value so SQLMesh knows where to substitute the schema name"
2080                )
2081        return compiled
2082
2083    @field_validator("timestamp_mapping", mode="before")
2084    @classmethod
2085    def _validate_timestamp_mapping(
2086        cls, value: t.Optional[dict[str, str]]
2087    ) -> t.Optional[dict[exp.DataType, exp.DataType]]:
2088        if value is None:
2089            return value
2090
2091        result: dict[exp.DataType, exp.DataType] = {}
2092        for source_type, target_type in value.items():
2093            try:
2094                source_datatype = exp.DataType.build(source_type)
2095            except ParseError:
2096                raise ConfigError(
2097                    f"Invalid SQL type string in timestamp_mapping: "
2098                    f"'{source_type}' is not a valid SQL data type."
2099                )
2100            try:
2101                target_datatype = exp.DataType.build(target_type)
2102            except ParseError:
2103                raise ConfigError(
2104                    f"Invalid SQL type string in timestamp_mapping: "
2105                    f"'{target_type}' is not a valid SQL data type."
2106                )
2107            result[source_datatype] = target_datatype
2108
2109        return result
2110
2111    @model_validator(mode="after")
2112    def _root_validator(self) -> Self:
2113        port = self.port
2114        if self.http_scheme == "http" and not self.method.is_no_auth and not self.method.is_basic:
2115            raise ConfigError("HTTP scheme can only be used with no-auth or basic method")
2116
2117        if port is None:
2118            self.port = 80 if self.http_scheme == "http" else 443
2119
2120        if (self.method.is_ldap or self.method.is_basic) and (not self.password or not self.user):
2121            raise ConfigError(
2122                f"Username and Password must be provided if using {self.method.value} authentication"
2123            )
2124
2125        if self.method.is_kerberos and (
2126            not self.principal or not self.keytab or not self.krb5_config
2127        ):
2128            raise ConfigError(
2129                "Kerberos requires the following fields: principal, keytab, and krb5_config"
2130            )
2131
2132        if self.method.is_jwt and not self.jwt_token:
2133            raise ConfigError("JWT requires `jwt_token` to be set")
2134
2135        if self.method.is_certificate and (
2136            not self.cert or not self.client_certificate or not self.client_private_key
2137        ):
2138            raise ConfigError(
2139                "Certificate requires the following fields: cert, client_certificate, and client_private_key"
2140            )
2141
2142        return self
2143
2144    @property
2145    def _connection_kwargs_keys(self) -> t.Set[str]:
2146        kwargs = {
2147            "host",
2148            "port",
2149            "catalog",
2150            "roles",
2151            "source",
2152            "http_scheme",
2153            "http_headers",
2154            "session_properties",
2155            "timezone",
2156        }
2157        return kwargs
2158
2159    @property
2160    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2161        return engine_adapter.TrinoEngineAdapter
2162
2163    @property
2164    def _connection_factory(self) -> t.Callable:
2165        from trino.dbapi import connect
2166
2167        return connect
2168
2169    @property
2170    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
2171        from trino.auth import (
2172            BasicAuthentication,
2173            CertificateAuthentication,
2174            JWTAuthentication,
2175            KerberosAuthentication,
2176            OAuth2Authentication,
2177        )
2178
2179        auth: t.Optional[
2180            t.Union[
2181                BasicAuthentication,
2182                KerberosAuthentication,
2183                OAuth2Authentication,
2184                JWTAuthentication,
2185                CertificateAuthentication,
2186            ]
2187        ] = None
2188        if self.method.is_basic or self.method.is_ldap:
2189            assert self.password is not None  # for mypy since validator already checks this
2190            auth = BasicAuthentication(self.user, self.password)
2191        elif self.method.is_kerberos:
2192            if self.keytab:
2193                os.environ["KRB5_CLIENT_KTNAME"] = self.keytab
2194            auth = KerberosAuthentication(
2195                config=self.krb5_config,
2196                service_name=self.service_name,
2197                principal=self.principal,
2198                mutual_authentication=self.mutual_authentication,
2199                ca_bundle=self.cert,
2200                force_preemptive=self.force_preemptive,
2201                hostname_override=self.hostname_override,
2202                sanitize_mutual_error_response=self.sanitize_mutual_error_response,
2203                delegate=self.delegate,
2204            )
2205        elif self.method.is_oauth:
2206            auth = OAuth2Authentication()
2207        elif self.method.is_jwt:
2208            assert self.jwt_token is not None
2209            auth = JWTAuthentication(self.jwt_token)
2210        elif self.method.is_certificate:
2211            assert self.client_certificate is not None
2212            assert self.client_private_key is not None
2213            auth = CertificateAuthentication(self.client_certificate, self.client_private_key)
2214
2215        return {
2216            "auth": auth,
2217            "user": self.impersonation_user or self.user,
2218            "max_attempts": self.retries,
2219            "verify": self.cert if self.cert is not None else self.verify,
2220            "source": self.source,
2221        }
2222
2223    @property
2224    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2225        return {
2226            "schema_location_mapping": self.schema_location_mapping,
2227            "timestamp_mapping": self.timestamp_mapping,
2228        }
2229
2230
2231class ClickhouseConnectionConfig(ConnectionConfig):
2232    """
2233    Clickhouse Connection Configuration.
2234
2235    Property reference: https://clickhouse.com/docs/en/integrations/python#client-initialization
2236    """
2237
2238    host: str
2239    username: str
2240    password: t.Optional[str] = None
2241    port: t.Optional[int] = None
2242    cluster: t.Optional[str] = None
2243    virtual_catalog: t.Optional[str] = None
2244    connect_timeout: int = 10
2245    send_receive_timeout: int = 300
2246    query_limit: int = 0
2247    use_compression: bool = True
2248    compression_method: t.Optional[str] = None
2249    connection_settings: t.Optional[t.Dict[str, t.Any]] = None
2250    http_proxy: t.Optional[str] = None
2251    # HTTPS/TLS settings
2252    verify: bool = True
2253    ca_cert: t.Optional[str] = None
2254    client_cert: t.Optional[str] = None
2255    client_cert_key: t.Optional[str] = None
2256    https_proxy: t.Optional[str] = None
2257    server_host_name: t.Optional[str] = None
2258    tls_mode: t.Optional[str] = None
2259    secure: bool = False
2260
2261    concurrent_tasks: int = 1
2262    register_comments: bool = True
2263    pre_ping: bool = False
2264
2265    # This object expects options from urllib3 and also from clickhouse-connect
2266    # See:
2267    # * https://urllib3.readthedocs.io/en/stable/advanced-usage.html
2268    # * https://clickhouse.com/docs/en/integrations/python#customizing-the-http-connection-pool
2269    connection_pool_options: t.Optional[t.Dict[str, t.Any]] = None
2270
2271    type_: t.Literal["clickhouse"] = Field(alias="type", default="clickhouse")
2272    DIALECT: t.ClassVar[t.Literal["clickhouse"]] = "clickhouse"
2273    DISPLAY_NAME: t.ClassVar[t.Literal["ClickHouse"]] = "ClickHouse"
2274    DISPLAY_ORDER: t.ClassVar[t.Literal[6]] = 6
2275
2276    _engine_import_validator = _get_engine_import_validator("clickhouse_connect", "clickhouse")
2277
2278    @field_validator("virtual_catalog")
2279    def validate_virtual_catalog(cls, v: t.Optional[str]) -> t.Optional[str]:
2280        if v is not None and not v.strip():
2281            raise ConfigError(
2282                "virtual_catalog cannot be an empty string. "
2283                "Omit the field to use the default synthetic prefix (__<gateway_name>__)."
2284            )
2285        if v is not None and "." in v:
2286            raise ConfigError(
2287                f"virtual_catalog must be a single identifier with no dots (got: {v!r})"
2288            )
2289        return v
2290
2291    @property
2292    def _connection_kwargs_keys(self) -> t.Set[str]:
2293        kwargs = {
2294            "host",
2295            "username",
2296            "port",
2297            "password",
2298            "connect_timeout",
2299            "send_receive_timeout",
2300            "query_limit",
2301            "http_proxy",
2302            "verify",
2303            "ca_cert",
2304            "client_cert",
2305            "client_cert_key",
2306            "https_proxy",
2307            "server_host_name",
2308            "tls_mode",
2309            "secure",
2310        }
2311        return kwargs
2312
2313    @property
2314    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2315        return engine_adapter.ClickhouseEngineAdapter
2316
2317    @property
2318    def _connection_factory(self) -> t.Callable:
2319        from functools import partial
2320
2321        from clickhouse_connect.dbapi import connect  # type: ignore
2322        from clickhouse_connect.driver import httputil  # type: ignore
2323
2324        pool_manager_options: t.Dict[str, t.Any] = dict(
2325            # Match the maxsize to the number of concurrent tasks
2326            maxsize=self.concurrent_tasks,
2327            # Block if there are no free connections
2328            block=True,
2329            verify=self.verify,
2330            ca_cert=self.ca_cert,
2331            client_cert=self.client_cert,
2332            client_cert_key=self.client_cert_key,
2333            https_proxy=self.https_proxy,
2334        )
2335        # this doesn't happen automatically because we always supply our own pool manager to the connection
2336        # https://github.com/ClickHouse/clickhouse-connect/blob/3a7f4b04cad29c7c2536661b831fb744248e2ec0/clickhouse_connect/driver/httpclient.py#L109
2337        if self.server_host_name:
2338            pool_manager_options["server_hostname"] = self.server_host_name
2339            if self.verify:
2340                pool_manager_options["assert_hostname"] = self.server_host_name
2341        if self.connection_pool_options:
2342            pool_manager_options.update(self.connection_pool_options)
2343        pool_mgr = httputil.get_pool_manager(**pool_manager_options)
2344
2345        return partial(connect, pool_mgr=pool_mgr)
2346
2347    @property
2348    def cloud_mode(self) -> bool:
2349        return "clickhouse.cloud" in self.host
2350
2351    @property
2352    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2353        return {
2354            "cluster": self.cluster,
2355            "cloud_mode": self.cloud_mode,
2356            "virtual_catalog": self.virtual_catalog,
2357        }
2358
2359    @property
2360    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
2361        from sqlmesh import __version__
2362
2363        # False = no compression
2364        # True = Clickhouse default compression method
2365        # string = specific compression method
2366        compress: bool | str = self.use_compression
2367        if compress and self.compression_method:
2368            compress = self.compression_method
2369
2370        # Clickhouse system settings passed to connection
2371        # https://clickhouse.com/docs/en/operations/settings/settings
2372        # - below are set to align with dbt-clickhouse
2373        # - https://github.com/ClickHouse/dbt-clickhouse/blob/44d26308ea6a3c8ead25c280164aa88191f05f47/dbt/adapters/clickhouse/dbclient.py#L77
2374        settings = self.connection_settings or {}
2375        #  mutations_sync = 2: "The query waits for all mutations [ALTER statements] to complete on all replicas (if they exist)"
2376        settings["mutations_sync"] = "2"
2377        #  insert_distributed_sync = 1: "INSERT operation succeeds only after all the data is saved on all shards"
2378        settings["insert_distributed_sync"] = "1"
2379        if self.cluster or self.cloud_mode:
2380            # database_replicated_enforce_synchronous_settings = 1:
2381            #   - "Enforces synchronous waiting for some queries"
2382            #   - https://github.com/ClickHouse/ClickHouse/blob/ccaa8d03a9351efc16625340268b9caffa8a22ba/src/Core/Settings.h#L709
2383            settings["database_replicated_enforce_synchronous_settings"] = "1"
2384            # insert_quorum = auto:
2385            #   - "INSERT succeeds only when ClickHouse manages to correctly write data to the insert_quorum of replicas during
2386            #       the insert_quorum_timeout"
2387            #   - "use majority number (number_of_replicas / 2 + 1) as quorum number"
2388            settings["insert_quorum"] = "auto"
2389
2390        return {
2391            "compress": compress,
2392            "client_name": f"SQLMesh/{__version__}",
2393            **settings,
2394        }
2395
2396
2397class AthenaConnectionConfig(ConnectionConfig):
2398    # PyAthena connection options
2399    aws_access_key_id: t.Optional[str] = None
2400    aws_secret_access_key: t.Optional[str] = None
2401    role_arn: t.Optional[str] = None
2402    role_session_name: t.Optional[str] = None
2403    region_name: t.Optional[str] = None
2404    work_group: t.Optional[str] = None
2405    s3_staging_dir: t.Optional[str] = None
2406    schema_name: t.Optional[str] = None
2407    catalog_name: t.Optional[str] = None
2408
2409    # SQLMesh options
2410    s3_warehouse_location: t.Optional[str] = None
2411    concurrent_tasks: int = 4
2412    register_comments: t.Literal[False] = (
2413        False  # because Athena doesnt support comments in most cases
2414    )
2415    pre_ping: t.Literal[False] = False
2416
2417    type_: t.Literal["athena"] = Field(alias="type", default="athena")
2418    DIALECT: t.ClassVar[t.Literal["athena"]] = "athena"
2419    DISPLAY_NAME: t.ClassVar[t.Literal["Athena"]] = "Athena"
2420    DISPLAY_ORDER: t.ClassVar[t.Literal[15]] = 15
2421
2422    _engine_import_validator = _get_engine_import_validator("pyathena", "athena")
2423
2424    @model_validator(mode="after")
2425    def _root_validator(self) -> Self:
2426        work_group = self.work_group
2427        s3_staging_dir = self.s3_staging_dir
2428        s3_warehouse_location = self.s3_warehouse_location
2429
2430        if not work_group and not s3_staging_dir:
2431            raise ConfigError("At least one of work_group or s3_staging_dir must be set")
2432
2433        if s3_staging_dir:
2434            self.s3_staging_dir = validate_s3_uri(s3_staging_dir, base=True, error_type=ConfigError)
2435
2436        if s3_warehouse_location:
2437            self.s3_warehouse_location = validate_s3_uri(
2438                s3_warehouse_location, base=True, error_type=ConfigError
2439            )
2440
2441        return self
2442
2443    @property
2444    def _connection_kwargs_keys(self) -> t.Set[str]:
2445        return {
2446            "aws_access_key_id",
2447            "aws_secret_access_key",
2448            "role_arn",
2449            "role_session_name",
2450            "region_name",
2451            "work_group",
2452            "s3_staging_dir",
2453            "schema_name",
2454            "catalog_name",
2455        }
2456
2457    @property
2458    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2459        return engine_adapter.AthenaEngineAdapter
2460
2461    @property
2462    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2463        return {"s3_warehouse_location": self.s3_warehouse_location}
2464
2465    @property
2466    def _connection_factory(self) -> t.Callable:
2467        from pyathena import connect  # type: ignore
2468
2469        return connect
2470
2471    def get_catalog(self) -> t.Optional[str]:
2472        return self.catalog_name
2473
2474
2475class RisingwaveConnectionConfig(ConnectionConfig):
2476    host: str
2477    user: str
2478    password: t.Optional[str] = None
2479    port: int
2480    database: str
2481    role: t.Optional[str] = None
2482    sslmode: t.Optional[str] = None
2483
2484    concurrent_tasks: int = 4
2485    register_comments: bool = True
2486    pre_ping: bool = True
2487
2488    type_: t.Literal["risingwave"] = Field(alias="type", default="risingwave")
2489    DIALECT: t.ClassVar[t.Literal["risingwave"]] = "risingwave"
2490    DISPLAY_NAME: t.ClassVar[t.Literal["RisingWave"]] = "RisingWave"
2491    DISPLAY_ORDER: t.ClassVar[t.Literal[16]] = 16
2492
2493    _engine_import_validator = _get_engine_import_validator("psycopg2", "risingwave")
2494
2495    @property
2496    def _connection_kwargs_keys(self) -> t.Set[str]:
2497        return {
2498            "host",
2499            "user",
2500            "password",
2501            "port",
2502            "database",
2503            "role",
2504            "sslmode",
2505        }
2506
2507    @property
2508    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2509        return engine_adapter.RisingwaveEngineAdapter
2510
2511    @property
2512    def _connection_factory(self) -> t.Callable:
2513        from psycopg2 import connect
2514
2515        return connect
2516
2517    @property
2518    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
2519        def init(cursor: t.Any) -> None:
2520            sql = "SET RW_IMPLICIT_FLUSH TO true;"
2521            cursor.execute(sql)
2522
2523        return init
2524
2525
2526class StarRocksConnectionConfig(ConnectionConfig):
2527    """Configuration for the StarRocks connection.
2528
2529    StarRocks uses MySQL network protocol and is compatible with MySQL ecosystem tools,
2530    JDBC/ODBC drivers, and various visualization tools.
2531
2532    Args:
2533        host: The hostname of the StarRocks FE (Frontend) node.
2534        user: The StarRocks username.
2535        password: The StarRocks password.
2536        port: The port number of the StarRocks FE node. Default is 9030.
2537        database: The optional database name.
2538        charset: The optional character set.  TODO: may be not supported yet.
2539        collation: The optional collation.  TODO: may be not supported yet.
2540        ssl_disabled: Whether to disable SSL connection.  TODO: need to check it.
2541        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
2542        register_comments: Whether or not to register model comments with the SQL engine.
2543        local_infile: Whether or not to allow local file access.
2544        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
2545    """
2546
2547    host: str
2548    user: str
2549    password: str
2550    port: t.Optional[int] = 9030
2551    database: t.Optional[str] = None
2552    charset: t.Optional[str] = None
2553    collation: t.Optional[str] = None
2554    ssl_disabled: t.Optional[bool] = None
2555
2556    concurrent_tasks: int = 4
2557    register_comments: bool = True
2558    local_infile: bool = False
2559    pre_ping: bool = True
2560
2561    type_: t.Literal["starrocks"] = Field(alias="type", default="starrocks")
2562    DIALECT: t.ClassVar[t.Literal["starrocks"]] = "starrocks"
2563    DISPLAY_NAME: t.ClassVar[t.Literal["StarRocks"]] = "StarRocks"
2564    DISPLAY_ORDER: t.ClassVar[t.Literal[18]] = 18
2565
2566    _engine_import_validator = _get_engine_import_validator("pymysql", "starrocks")
2567
2568    @property
2569    def _connection_kwargs_keys(self) -> t.Set[str]:
2570        connection_keys = {
2571            "host",
2572            "user",
2573            "password",
2574        }
2575        if self.port is not None:
2576            connection_keys.add("port")
2577        if self.database is not None:
2578            connection_keys.add("database")
2579        if self.charset is not None:
2580            connection_keys.add("charset")
2581        if self.collation is not None:
2582            connection_keys.add("collation")
2583        if self.ssl_disabled is not None:
2584            connection_keys.add("ssl_disabled")
2585        if self.local_infile is not None:
2586            connection_keys.add("local_infile")
2587        return connection_keys
2588
2589    @property
2590    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2591        return engine_adapter.StarRocksEngineAdapter
2592
2593    @property
2594    def _connection_factory(self) -> t.Callable:
2595        from pymysql import connect
2596
2597        return connect
2598
2599
2600_CONNECTION_CONFIG_EXCLUDE: t.Set[t.Type[ConnectionConfig]] = {
2601    ConnectionConfig,  # type: ignore[type-abstract]
2602    BaseDuckDBConnectionConfig,  # type: ignore[type-abstract]
2603}
2604
2605CONNECTION_CONFIG_TO_TYPE = {
2606    # Map all subclasses of ConnectionConfig to the value of their `type_` field.
2607    tpe.all_field_infos()["type_"].default: tpe
2608    for tpe in subclasses(__name__, ConnectionConfig, exclude=_CONNECTION_CONFIG_EXCLUDE)
2609}
2610
2611DIALECT_TO_TYPE = {
2612    tpe.all_field_infos()["type_"].default: tpe.DIALECT
2613    for tpe in subclasses(__name__, ConnectionConfig, exclude=_CONNECTION_CONFIG_EXCLUDE)
2614}
2615
2616INIT_DISPLAY_INFO_TO_TYPE = {
2617    tpe.all_field_infos()["type_"].default: (
2618        tpe.DISPLAY_ORDER,
2619        tpe.DISPLAY_NAME,
2620    )
2621    for tpe in subclasses(__name__, ConnectionConfig, exclude=_CONNECTION_CONFIG_EXCLUDE)
2622}
2623
2624
2625def parse_connection_config(v: t.Dict[str, t.Any]) -> ConnectionConfig:
2626    if "type" not in v:
2627        raise ConfigError("Missing connection type.")
2628
2629    connection_type = v["type"]
2630    if connection_type not in CONNECTION_CONFIG_TO_TYPE:
2631        raise ConfigError(f"Unknown connection type '{connection_type}'.")
2632
2633    return CONNECTION_CONFIG_TO_TYPE[connection_type](**v)
2634
2635
2636def _connection_config_validator(
2637    cls: t.Type, v: ConnectionConfig | t.Dict[str, t.Any] | None
2638) -> ConnectionConfig | None:
2639    if v is None or isinstance(v, ConnectionConfig):
2640        return v
2641
2642    check_config_and_vars_msg = "\n\nVerify your config.yaml and environment variables."
2643
2644    try:
2645        return parse_connection_config(v)
2646    except pydantic.ValidationError as e:
2647        raise ConfigError(
2648            validation_error_message(e, f"Invalid '{v['type']}' connection config:")
2649            + check_config_and_vars_msg
2650        )
2651    except ConfigError as e:
2652        raise ConfigError(str(e) + check_config_and_vars_msg)
2653
2654
2655connection_config_validator: t.Callable = field_validator(
2656    "connection",
2657    "state_connection",
2658    "test_connection",
2659    "default_connection",
2660    "default_test_connection",
2661    mode="before",
2662    check_fields=False,
2663)(_connection_config_validator)
2664
2665
2666if t.TYPE_CHECKING:
2667    # TypeAlias hasn't been introduced until Python 3.10 which means that we can't use it
2668    # outside the TYPE_CHECKING guard.
2669    SerializableConnectionConfig: t.TypeAlias = ConnectionConfig  # type: ignore
2670else:
2671    import pydantic
2672
2673    # Workaround for https://docs.pydantic.dev/latest/concepts/serialization/#serializing-with-duck-typing
2674    SerializableConnectionConfig = pydantic.SerializeAsAny[ConnectionConfig]  # type: ignore
logger = <Logger sqlmesh.core.config.connection (WARNING)>
FORBIDDEN_STATE_SYNC_ENGINES = {'clickhouse', 'trino', 'starrocks', 'spark'}
MOTHERDUCK_TOKEN_REGEX = re.compile('(\\?|\\&)(motherduck_token=)(\\S*)')
PASSWORD_REGEX = re.compile('(password=)(\\S+)')
SUPPORTS_MSSQL_PYTHON_DRIVER = True
class ConnectionConfig(abc.ABC, sqlmesh.core.config.base.BaseConfig):
101class ConnectionConfig(abc.ABC, BaseConfig):
102    type_: str
103    DIALECT: t.ClassVar[str]
104    DISPLAY_NAME: t.ClassVar[str]
105    DISPLAY_ORDER: t.ClassVar[int]
106    concurrent_tasks: int
107    register_comments: bool
108    pre_ping: bool
109    pretty_sql: bool = False
110    schema_differ_overrides: t.Optional[t.Dict[str, t.Any]] = None
111    catalog_type_overrides: t.Optional[t.Dict[str, str]] = None
112
113    # Whether to share a  single connection across threads or create a new connection per thread.
114    #
115    # MyPy throws a "Decorators on top of @property are not supported" error despite this being a
116    # valid decoration, and Pydantic recommend disabling the MyPy hint for this reason - see:
117    # https://pydantic.dev/docs/validation/2.0/usage/computed_fields/
118    @computed_field  # type: ignore[prop-decorator]
119    @property
120    def shared_connection(self) -> bool:
121        return False
122
123    @property
124    @abc.abstractmethod
125    def _connection_kwargs_keys(self) -> t.Set[str]:
126        """keywords that should be passed into the connection"""
127
128    @property
129    @abc.abstractmethod
130    def _engine_adapter(self) -> t.Type[EngineAdapter]:
131        """The engine adapter for this connection"""
132
133    @property
134    @abc.abstractmethod
135    def _connection_factory(self) -> t.Callable:
136        """A function that is called to return a connection object for the given Engine Adapter"""
137
138    @property
139    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
140        """The static connection kwargs for this connection"""
141        return {}
142
143    @property
144    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
145        """kwargs that are for execution config only"""
146        return {}
147
148    @property
149    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
150        """A function that is called to initialize the cursor"""
151        return None
152
153    @property
154    def is_recommended_for_state_sync(self) -> bool:
155        """Whether this engine is recommended for being used as a state sync for production state syncs"""
156        return self.type_ in RECOMMENDED_STATE_SYNC_ENGINES
157
158    @property
159    def is_forbidden_for_state_sync(self) -> bool:
160        """Whether this engine is forbidden from being used as a state sync"""
161        return self.type_ in FORBIDDEN_STATE_SYNC_ENGINES
162
163    @property
164    def _connection_factory_with_kwargs(self) -> t.Callable[[], t.Any]:
165        """A function that is called to return a connection object for the given Engine Adapter"""
166        return partial(
167            self._connection_factory,
168            **{
169                **self._static_connection_kwargs,
170                **{k: v for k, v in self.dict().items() if k in self._connection_kwargs_keys},
171            },
172        )
173
174    def connection_validator(self) -> t.Callable[[], None]:
175        """A function that validates the connection configuration"""
176        return self.create_engine_adapter().ping
177
178    def create_engine_adapter(
179        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
180    ) -> EngineAdapter:
181        """Returns a new instance of the Engine Adapter."""
182
183        concurrent_tasks = concurrent_tasks or self.concurrent_tasks
184        return self._engine_adapter(
185            self._connection_factory_with_kwargs,
186            multithreaded=concurrent_tasks > 1,
187            default_catalog=self.get_catalog(),
188            cursor_init=self._cursor_init,
189            register_comments=register_comments_override or self.register_comments,
190            pre_ping=self.pre_ping,
191            pretty_sql=self.pretty_sql,
192            shared_connection=self.shared_connection,
193            schema_differ_overrides=self.schema_differ_overrides,
194            catalog_type_overrides=self.catalog_type_overrides,
195            **self._extra_engine_config,
196        )
197
198    def get_catalog(self) -> t.Optional[str]:
199        """The catalog for this connection"""
200        if hasattr(self, "catalog"):
201            return self.catalog
202        if hasattr(self, "database"):
203            return self.database
204        if hasattr(self, "db"):
205            return self.db
206        return None
207
208    @model_validator(mode="before")
209    @classmethod
210    def _expand_json_strings_to_concrete_types(cls, data: t.Any) -> t.Any:
211        """
212        There are situations where a connection config class has a field that is some kind of complex type
213        (eg a list of strings or a dict) but the value is being supplied from a source such as an environment variable
214
215        When this happens, the value is supplied as a string rather than a Python object. We need some way
216        of turning this string into the corresponding Python list or dict.
217
218        Rather than doing this piecemeal on every config subclass, this provides a generic implementatation
219        to identify fields that may be be supplied as JSON strings and handle them transparently
220        """
221        if data and isinstance(data, dict):
222            for maybe_json_field_name in cls._get_list_and_dict_field_names():
223                if (value := data.get(maybe_json_field_name)) and isinstance(value, str):
224                    # crude JSON check as we dont want to try and parse every string we get
225                    value = value.strip()
226                    if value.startswith("{") or value.startswith("["):
227                        data[maybe_json_field_name] = from_json(value)
228
229        return data
230
231    @classmethod
232    def _get_list_and_dict_field_names(cls) -> t.Set[str]:
233        field_names = set()
234        for name, field in cls.model_fields.items():
235            if field.annotation:
236                field_types = get_concrete_types_from_typehint(field.annotation)
237
238                # check if the field type is something that could concievably be supplied as a json string
239                if any(ft is t for t in (list, tuple, set, dict) for ft in field_types):
240                    field_names.add(name)
241
242        return field_names

Helper class that provides a standard way to create an ABC using inheritance.

type_: str
DIALECT: ClassVar[str]
DISPLAY_NAME: ClassVar[str]
DISPLAY_ORDER: ClassVar[int]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
pretty_sql: bool
schema_differ_overrides: Optional[Dict[str, Any]]
catalog_type_overrides: Optional[Dict[str, str]]
shared_connection: bool
118    @computed_field  # type: ignore[prop-decorator]
119    @property
120    def shared_connection(self) -> bool:
121        return False
is_forbidden_for_state_sync: bool
158    @property
159    def is_forbidden_for_state_sync(self) -> bool:
160        """Whether this engine is forbidden from being used as a state sync"""
161        return self.type_ in FORBIDDEN_STATE_SYNC_ENGINES

Whether this engine is forbidden from being used as a state sync

def connection_validator(self) -> Callable[[], NoneType]:
174    def connection_validator(self) -> t.Callable[[], None]:
175        """A function that validates the connection configuration"""
176        return self.create_engine_adapter().ping

A function that validates the connection configuration

def create_engine_adapter( self, register_comments_override: bool = False, concurrent_tasks: Optional[int] = None) -> sqlmesh.core.engine_adapter.base.EngineAdapter:
178    def create_engine_adapter(
179        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
180    ) -> EngineAdapter:
181        """Returns a new instance of the Engine Adapter."""
182
183        concurrent_tasks = concurrent_tasks or self.concurrent_tasks
184        return self._engine_adapter(
185            self._connection_factory_with_kwargs,
186            multithreaded=concurrent_tasks > 1,
187            default_catalog=self.get_catalog(),
188            cursor_init=self._cursor_init,
189            register_comments=register_comments_override or self.register_comments,
190            pre_ping=self.pre_ping,
191            pretty_sql=self.pretty_sql,
192            shared_connection=self.shared_connection,
193            schema_differ_overrides=self.schema_differ_overrides,
194            catalog_type_overrides=self.catalog_type_overrides,
195            **self._extra_engine_config,
196        )

Returns a new instance of the Engine Adapter.

def get_catalog(self) -> Optional[str]:
198    def get_catalog(self) -> t.Optional[str]:
199        """The catalog for this connection"""
200        if hasattr(self, "catalog"):
201            return self.catalog
202        if hasattr(self, "database"):
203            return self.database
204        if hasattr(self, "db"):
205            return self.db
206        return None

The catalog for this connection

model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class DuckDBAttachOptions(sqlmesh.core.config.base.BaseConfig):
245class DuckDBAttachOptions(BaseConfig):
246    type: str
247    path: str
248    read_only: bool = False
249
250    # DuckLake specific options
251    data_path: t.Optional[str] = None
252    override_data_path: t.Optional[bool] = False
253    encrypted: bool = False
254    data_inlining_row_limit: t.Optional[int] = None
255    metadata_schema: t.Optional[str] = None
256
257    def to_sql(self, alias: str) -> str:
258        options = []
259        # 'duckdb' is actually not a supported type, but we'd like to allow it for
260        # fully qualified attach options or integration testing, similar to duckdb-dbt
261        if self.type not in ("duckdb", "ducklake", "motherduck"):
262            options.append(f"TYPE {self.type.upper()}")
263        if self.read_only:
264            options.append("READ_ONLY")
265
266        # DuckLake specific options
267        path = self.path
268        if self.type == "ducklake":
269            if not path.startswith("ducklake:"):
270                path = f"ducklake:{path}"
271            if self.data_path is not None:
272                options.append(f"DATA_PATH '{self.data_path}'")
273                if self.override_data_path:
274                    options.append("OVERRIDE_DATA_PATH true")
275            if self.encrypted:
276                options.append("ENCRYPTED")
277            if self.data_inlining_row_limit is not None:
278                options.append(f"DATA_INLINING_ROW_LIMIT {self.data_inlining_row_limit}")
279            if self.metadata_schema is not None:
280                options.append(f"METADATA_SCHEMA '{self.metadata_schema}'")
281
282        options_sql = f" ({', '.join(options)})" if options else ""
283        alias_sql = ""
284        # TODO: Add support for Postgres schema. Currently adding it blocks access to the information_schema
285
286        # MotherDuck does not support aliasing
287        alias_sql = (
288            f" AS {alias}" if not (self.type == "motherduck" or self.path.startswith("md:")) else ""
289        )
290        return f"ATTACH IF NOT EXISTS '{path}'{alias_sql}{options_sql}"

Base configuration functionality for configuration classes.

type: str
path: str
read_only: bool
data_path: Optional[str]
override_data_path: Optional[bool]
encrypted: bool
data_inlining_row_limit: Optional[int]
metadata_schema: Optional[str]
def to_sql(self, alias: str) -> str:
257    def to_sql(self, alias: str) -> str:
258        options = []
259        # 'duckdb' is actually not a supported type, but we'd like to allow it for
260        # fully qualified attach options or integration testing, similar to duckdb-dbt
261        if self.type not in ("duckdb", "ducklake", "motherduck"):
262            options.append(f"TYPE {self.type.upper()}")
263        if self.read_only:
264            options.append("READ_ONLY")
265
266        # DuckLake specific options
267        path = self.path
268        if self.type == "ducklake":
269            if not path.startswith("ducklake:"):
270                path = f"ducklake:{path}"
271            if self.data_path is not None:
272                options.append(f"DATA_PATH '{self.data_path}'")
273                if self.override_data_path:
274                    options.append("OVERRIDE_DATA_PATH true")
275            if self.encrypted:
276                options.append("ENCRYPTED")
277            if self.data_inlining_row_limit is not None:
278                options.append(f"DATA_INLINING_ROW_LIMIT {self.data_inlining_row_limit}")
279            if self.metadata_schema is not None:
280                options.append(f"METADATA_SCHEMA '{self.metadata_schema}'")
281
282        options_sql = f" ({', '.join(options)})" if options else ""
283        alias_sql = ""
284        # TODO: Add support for Postgres schema. Currently adding it blocks access to the information_schema
285
286        # MotherDuck does not support aliasing
287        alias_sql = (
288            f" AS {alias}" if not (self.type == "motherduck" or self.path.startswith("md:")) else ""
289        )
290        return f"ATTACH IF NOT EXISTS '{path}'{alias_sql}{options_sql}"
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class BaseDuckDBConnectionConfig(ConnectionConfig):
293class BaseDuckDBConnectionConfig(ConnectionConfig):
294    """Common configuration for the DuckDB-based connections.
295
296    Args:
297        database: The optional database name. If not specified, the in-memory database will be used.
298        catalogs: Key is the name of the catalog and value is the path.
299        extensions: A list of autoloadable extensions to load.
300        connector_config: A dictionary of configuration to pass into the duckdb connector.
301        secrets: A list of dictionaries used to generate DuckDB secrets for authenticating with external services (e.g. S3).
302        filesystems: A list of dictionaries used to register `fsspec` filesystems to the DuckDB cursor.
303        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
304        register_comments: Whether or not to register model comments with the SQL engine.
305        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
306        token: The optional MotherDuck token. If not specified and a MotherDuck path is in the catalog, the user will be prompted to login with their web browser.
307    """
308
309    database: t.Optional[str] = None
310    catalogs: t.Optional[t.Dict[str, t.Union[str, DuckDBAttachOptions]]] = None
311    extensions: t.List[t.Union[str, t.Dict[str, t.Any]]] = []
312    connector_config: t.Dict[str, t.Any] = {}
313    secrets: t.Union[t.List[t.Dict[str, t.Any]], t.Dict[str, t.Dict[str, t.Any]]] = []
314    filesystems: t.List[t.Dict[str, t.Any]] = []
315
316    concurrent_tasks: int = 1
317    register_comments: bool = True
318    pre_ping: t.Literal[False] = False
319
320    token: t.Optional[str] = None
321
322    @computed_field  # type: ignore[prop-decorator]
323    @property
324    def shared_connection(self) -> bool:
325        return True
326
327    _data_file_to_adapter: t.ClassVar[t.Dict[str, EngineAdapter]] = {}
328
329    @model_validator(mode="before")
330    def _validate_database_catalogs(cls, data: t.Any) -> t.Any:
331        if not isinstance(data, dict):
332            return data
333
334        db_path = data.get("database")
335        if db_path and data.get("catalogs"):
336            raise ConfigError(
337                "Cannot specify both `database` and `catalogs`. Define all your catalogs in `catalogs` and have the first entry be the default catalog"
338            )
339        if isinstance(db_path, str) and db_path.startswith("md:"):
340            raise ConfigError(
341                "Please use connection type 'motherduck' without the `md:` prefix if you want to use a MotherDuck database as the single `database`."
342            )
343
344        return data
345
346    @property
347    def _engine_adapter(self) -> t.Type[EngineAdapter]:
348        return engine_adapter.DuckDBEngineAdapter
349
350    @property
351    def _connection_kwargs_keys(self) -> t.Set[str]:
352        return {"database"}
353
354    @property
355    def _connection_factory(self) -> t.Callable:
356        import duckdb
357
358        return duckdb.connect
359
360    @property
361    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
362        """A function that is called to initialize the cursor"""
363        import duckdb
364        from duckdb import BinderException
365
366        def init(cursor: duckdb.DuckDBPyConnection) -> None:
367            for extension in self.extensions:
368                extension = extension if isinstance(extension, dict) else {"name": extension}
369
370                install_command = f"INSTALL {extension['name']}"
371
372                if extension.get("repository"):
373                    install_command = f"{install_command} FROM {extension['repository']}"
374
375                if extension.get("force_install"):
376                    install_command = f"FORCE {install_command}"
377
378                try:
379                    cursor.execute(install_command)
380                    cursor.execute(f"LOAD {extension['name']}")
381                except Exception as e:
382                    raise ConfigError(f"Failed to load extension {extension['name']}: {e}")
383
384            if self.connector_config:
385                option_names = list(self.connector_config)
386                in_part = ",".join("?" for _ in range(len(option_names)))
387
388                cursor.execute(
389                    f"SELECT name, value FROM duckdb_settings() WHERE name IN ({in_part})",
390                    option_names,
391                )
392
393                existing_values = {field: setting for field, setting in cursor.fetchall()}
394
395                # only set connector_config items if the values differ from what is already set
396                # trying to set options like 'temp_directory' even to the same value can throw errors like:
397                # Not implemented Error: Cannot switch temporary directory after the current one has been used
398                for field, setting in self.connector_config.items():
399                    if existing_values.get(field) != setting:
400                        try:
401                            cursor.execute(f"SET {field} = '{setting}'")
402                        except Exception as e:
403                            raise ConfigError(
404                                f"Failed to set connector config {field} to {setting}: {e}"
405                            )
406
407            if self.secrets:
408                duckdb_version = duckdb.__version__
409                if version.parse(duckdb_version) < version.parse("0.10.0"):
410                    from sqlmesh.core.console import get_console
411
412                    get_console().log_warning(
413                        f"DuckDB version {duckdb_version} does not support secrets-based authentication (requires 0.10.0 or later).\n"
414                        "To use secrets, please upgrade DuckDB. For older versions, configure legacy authentication via `connector_config`.\n"
415                        "More info: https://duckdb.org/docs/stable/extensions/httpfs/s3api_legacy_authentication.html"
416                    )
417                else:
418                    if isinstance(self.secrets, list):
419                        secrets_items = [(secret_dict, "") for secret_dict in self.secrets]
420                    else:
421                        secrets_items = [
422                            (secret_dict, secret_name)
423                            for secret_name, secret_dict in self.secrets.items()
424                        ]
425
426                    for secret_dict, secret_name in secrets_items:
427                        secret_settings: t.List[str] = []
428                        for field, setting in secret_dict.items():
429                            secret_settings.append(f"{field} '{setting}'")
430                        if secret_settings:
431                            secret_clause = ", ".join(secret_settings)
432                            try:
433                                cursor.execute(
434                                    f"CREATE OR REPLACE SECRET {secret_name} ({secret_clause});"
435                                )
436                            except Exception as e:
437                                raise ConfigError(f"Failed to create secret: {e}")
438
439            if self.filesystems:
440                from fsspec import filesystem  # type: ignore
441
442                for file_system in self.filesystems:
443                    options = file_system.copy()
444                    fs = options.pop("fs")
445                    fs = filesystem(fs, **options)
446                    cursor.register_filesystem(fs)
447
448            for i, (alias, path_options) in enumerate(
449                (getattr(self, "catalogs", None) or {}).items()
450            ):
451                # we parse_identifier and generate to ensure that `alias` has exactly one set of quotes
452                # regardless of whether it comes in quoted or not
453                alias = exp.parse_identifier(alias, dialect="duckdb").sql(
454                    identify=True, dialect="duckdb"
455                )
456                try:
457                    if isinstance(path_options, DuckDBAttachOptions):
458                        query = path_options.to_sql(alias)
459                    else:
460                        query = f"ATTACH IF NOT EXISTS '{path_options}'"
461                        if not path_options.startswith("md:"):
462                            query += f" AS {alias}"
463                    cursor.execute(query)
464                except BinderException as e:
465                    # If a user tries to create a catalog pointing at `:memory:` and with the name `memory`
466                    # then we don't want to raise since this happens by default. They are just doing this to
467                    # set it as the default catalog.
468                    # If a user tried to attach a MotherDuck database/share which has already by attached via
469                    # `ATTACH 'md:'`, then we don't want to raise since this is expected.
470                    if (
471                        not (
472                            'database with name "memory" already exists' in str(e)
473                            and path_options == ":memory:"
474                        )
475                        and f"""database with name "{path_options.path.replace("md:", "")}" already exists"""
476                        not in str(e)
477                    ):
478                        raise e
479                if i == 0 and not getattr(self, "database", None):
480                    cursor.execute(f"USE {alias}")
481
482        return init
483
484    def create_engine_adapter(
485        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
486    ) -> EngineAdapter:
487        """Checks if another engine adapter has already been created that shares a catalog that points to the same data
488        file. If so, it uses that same adapter instead of creating a new one. As a result, any additional configuration
489        associated with the new adapter will be ignored."""
490        data_files = set((self.catalogs or {}).values())
491        if self.database:
492            if isinstance(self, MotherDuckConnectionConfig):
493                data_files.add(
494                    f"md:{self.database}"
495                    + (f"?motherduck_token={self.token}" if self.token else "")
496                )
497            else:
498                data_files.add(self.database)
499        data_files.discard(":memory:")
500        for data_file in data_files:
501            key = data_file if isinstance(data_file, str) else data_file.path
502            adapter = BaseDuckDBConnectionConfig._data_file_to_adapter.get(key)
503            if adapter is not None:
504                logger.info(
505                    f"Using existing DuckDB adapter due to overlapping data file: {self._mask_sensitive_data(key)}"
506                )
507                return adapter
508
509        if data_files:
510            masked_files = {
511                self._mask_sensitive_data(file if isinstance(file, str) else file.path)
512                for file in data_files
513            }
514            logger.info(f"Creating new DuckDB adapter for data files: {masked_files}")
515        else:
516            logger.info("Creating new DuckDB adapter for in-memory database")
517        adapter = super().create_engine_adapter(
518            register_comments_override, concurrent_tasks=concurrent_tasks
519        )
520        for data_file in data_files:
521            key = data_file if isinstance(data_file, str) else data_file.path
522            BaseDuckDBConnectionConfig._data_file_to_adapter[key] = adapter
523        return adapter
524
525    def get_catalog(self) -> t.Optional[str]:
526        if self.database:
527            # Remove `:` from the database name in order to handle if `:memory:` is passed in
528            return pathlib.Path(self.database.replace(":memory:", "memory")).stem
529        if self.catalogs:
530            return list(self.catalogs)[0]
531        return None
532
533    def _mask_sensitive_data(self, string: str) -> str:
534        # Mask MotherDuck tokens with fixed number of asterisks
535        result = MOTHERDUCK_TOKEN_REGEX.sub(
536            lambda m: f"{m.group(1)}{m.group(2)}{'*' * 8 if m.group(3) else ''}", string
537        )
538        # Mask PostgreSQL/MySQL passwords with fixed number of asterisks
539        result = PASSWORD_REGEX.sub(lambda m: f"{m.group(1)}{'*' * 8}", result)
540        return result

Common configuration for the DuckDB-based connections.

Arguments:
  • database: The optional database name. If not specified, the in-memory database will be used.
  • catalogs: Key is the name of the catalog and value is the path.
  • extensions: A list of autoloadable extensions to load.
  • connector_config: A dictionary of configuration to pass into the duckdb connector.
  • secrets: A list of dictionaries used to generate DuckDB secrets for authenticating with external services (e.g. S3).
  • filesystems: A list of dictionaries used to register fsspec filesystems to the DuckDB cursor.
  • concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
  • register_comments: Whether or not to register model comments with the SQL engine.
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
  • token: The optional MotherDuck token. If not specified and a MotherDuck path is in the catalog, the user will be prompted to login with their web browser.
database: Optional[str]
catalogs: Optional[Dict[str, Union[str, DuckDBAttachOptions]]]
extensions: List[Union[str, Dict[str, Any]]]
connector_config: Dict[str, Any]
secrets: Union[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]
filesystems: List[Dict[str, Any]]
concurrent_tasks: int
register_comments: bool
pre_ping: Literal[False]
token: Optional[str]
shared_connection: bool
322    @computed_field  # type: ignore[prop-decorator]
323    @property
324    def shared_connection(self) -> bool:
325        return True
def create_engine_adapter( self, register_comments_override: bool = False, concurrent_tasks: Optional[int] = None) -> sqlmesh.core.engine_adapter.base.EngineAdapter:
484    def create_engine_adapter(
485        self, register_comments_override: bool = False, concurrent_tasks: t.Optional[int] = None
486    ) -> EngineAdapter:
487        """Checks if another engine adapter has already been created that shares a catalog that points to the same data
488        file. If so, it uses that same adapter instead of creating a new one. As a result, any additional configuration
489        associated with the new adapter will be ignored."""
490        data_files = set((self.catalogs or {}).values())
491        if self.database:
492            if isinstance(self, MotherDuckConnectionConfig):
493                data_files.add(
494                    f"md:{self.database}"
495                    + (f"?motherduck_token={self.token}" if self.token else "")
496                )
497            else:
498                data_files.add(self.database)
499        data_files.discard(":memory:")
500        for data_file in data_files:
501            key = data_file if isinstance(data_file, str) else data_file.path
502            adapter = BaseDuckDBConnectionConfig._data_file_to_adapter.get(key)
503            if adapter is not None:
504                logger.info(
505                    f"Using existing DuckDB adapter due to overlapping data file: {self._mask_sensitive_data(key)}"
506                )
507                return adapter
508
509        if data_files:
510            masked_files = {
511                self._mask_sensitive_data(file if isinstance(file, str) else file.path)
512                for file in data_files
513            }
514            logger.info(f"Creating new DuckDB adapter for data files: {masked_files}")
515        else:
516            logger.info("Creating new DuckDB adapter for in-memory database")
517        adapter = super().create_engine_adapter(
518            register_comments_override, concurrent_tasks=concurrent_tasks
519        )
520        for data_file in data_files:
521            key = data_file if isinstance(data_file, str) else data_file.path
522            BaseDuckDBConnectionConfig._data_file_to_adapter[key] = adapter
523        return adapter

Checks if another engine adapter has already been created that shares a catalog that points to the same data file. If so, it uses that same adapter instead of creating a new one. As a result, any additional configuration associated with the new adapter will be ignored.

def get_catalog(self) -> Optional[str]:
525    def get_catalog(self) -> t.Optional[str]:
526        if self.database:
527            # Remove `:` from the database name in order to handle if `:memory:` is passed in
528            return pathlib.Path(self.database.replace(":memory:", "memory")).stem
529        if self.catalogs:
530            return list(self.catalogs)[0]
531        return None

The catalog for this connection

model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
type_
DIALECT
DISPLAY_NAME
DISPLAY_ORDER
pretty_sql
schema_differ_overrides
catalog_type_overrides
is_forbidden_for_state_sync
connection_validator
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class MotherDuckConnectionConfig(BaseDuckDBConnectionConfig):
543class MotherDuckConnectionConfig(BaseDuckDBConnectionConfig):
544    """Configuration for the MotherDuck connection."""
545
546    type_: t.Literal["motherduck"] = Field(alias="type", default="motherduck")
547    DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb"
548    DISPLAY_NAME: t.ClassVar[t.Literal["MotherDuck"]] = "MotherDuck"
549    DISPLAY_ORDER: t.ClassVar[t.Literal[5]] = 5
550
551    @property
552    def _connection_kwargs_keys(self) -> t.Set[str]:
553        return set()
554
555    @property
556    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
557        """kwargs that are for execution config only"""
558        from sqlmesh import __version__
559
560        custom_user_agent_config = {"custom_user_agent": f"SQLMesh/{__version__}"}
561        connection_str = "md:"
562        if self.database:
563            # Attach single MD database instead of all databases on the account
564            connection_str += f"{self.database}?attach_mode=single"
565        if self.token:
566            connection_str += f"{'&' if self.database else '?'}motherduck_token={self.token}"
567        return {"database": connection_str, "config": custom_user_agent_config}
568
569    @property
570    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
571        return {"is_motherduck": True}

Configuration for the MotherDuck connection.

type_: Literal['motherduck']
DIALECT: ClassVar[Literal['duckdb']] = 'duckdb'
DISPLAY_NAME: ClassVar[Literal['MotherDuck']] = 'MotherDuck'
DISPLAY_ORDER: ClassVar[Literal[5]] = 5
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
BaseDuckDBConnectionConfig
database
catalogs
extensions
connector_config
secrets
filesystems
concurrent_tasks
register_comments
pre_ping
token
shared_connection
create_engine_adapter
get_catalog
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
is_forbidden_for_state_sync
connection_validator
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class DuckDBConnectionConfig(BaseDuckDBConnectionConfig):
574class DuckDBConnectionConfig(BaseDuckDBConnectionConfig):
575    """Configuration for the DuckDB connection."""
576
577    type_: t.Literal["duckdb"] = Field(alias="type", default="duckdb")
578    DIALECT: t.ClassVar[t.Literal["duckdb"]] = "duckdb"
579    DISPLAY_NAME: t.ClassVar[t.Literal["DuckDB"]] = "DuckDB"
580    DISPLAY_ORDER: t.ClassVar[t.Literal[1]] = 1

Configuration for the DuckDB connection.

type_: Literal['duckdb']
DIALECT: ClassVar[Literal['duckdb']] = 'duckdb'
DISPLAY_NAME: ClassVar[Literal['DuckDB']] = 'DuckDB'
DISPLAY_ORDER: ClassVar[Literal[1]] = 1
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
BaseDuckDBConnectionConfig
database
catalogs
extensions
connector_config
secrets
filesystems
concurrent_tasks
register_comments
pre_ping
token
shared_connection
create_engine_adapter
get_catalog
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
is_forbidden_for_state_sync
connection_validator
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class SnowflakeConnectionConfig(ConnectionConfig):
583class SnowflakeConnectionConfig(ConnectionConfig):
584    """Configuration for the Snowflake connection.
585
586    Args:
587        account: The Snowflake account name.
588        user: The Snowflake username.
589        password: The Snowflake password.
590        warehouse: The optional warehouse name.
591        database: The optional database name.
592        role: The optional role name.
593        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
594        authenticator: The optional authenticator name. Defaults to username/password authentication ("snowflake").
595                       Options: https://github.com/snowflakedb/snowflake-connector-python/blob/e937591356c067a77f34a0a42328907fda792c23/src/snowflake/connector/network.py#L178-L183
596        token: The optional oauth access token to use for authentication when authenticator is set to "oauth".
597        private_key: The optional private key to use for authentication. Key can be Base64-encoded DER format (representing the key bytes), a plain-text PEM format, or bytes (Python config only). https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#using-key-pair-authentication-key-pair-rotation
598        private_key_path: The optional path to the private key to use for authentication. This would be used instead of `private_key`.
599        private_key_passphrase: The optional passphrase to use to decrypt `private_key` or `private_key_path`. Keys can be created without encryption so only provide this if needed.
600        register_comments: Whether or not to register model comments with the SQL engine.
601        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
602        session_parameters: The optional session parameters to set for the connection.
603        host: Host address for the connection.
604        port: Port for the connection.
605    """
606
607    account: str
608    user: t.Optional[str] = None
609    password: t.Optional[str] = None
610    warehouse: t.Optional[str] = None
611    database: t.Optional[str] = None
612    role: t.Optional[str] = None
613    authenticator: t.Optional[str] = None
614    token: t.Optional[str] = None
615    host: t.Optional[str] = None
616    port: t.Optional[int] = None
617    application: t.Literal["Tobiko_SQLMesh"] = "Tobiko_SQLMesh"
618
619    # Private Key Auth
620    private_key: t.Optional[t.Union[str, bytes]] = None
621    private_key_path: t.Optional[str] = None
622    private_key_passphrase: t.Optional[str] = None
623
624    concurrent_tasks: int = 4
625    register_comments: bool = True
626    pre_ping: bool = False
627
628    session_parameters: t.Optional[dict] = None
629
630    type_: t.Literal["snowflake"] = Field(alias="type", default="snowflake")
631    DIALECT: t.ClassVar[t.Literal["snowflake"]] = "snowflake"
632    DISPLAY_NAME: t.ClassVar[t.Literal["Snowflake"]] = "Snowflake"
633    DISPLAY_ORDER: t.ClassVar[t.Literal[2]] = 2
634
635    _concurrent_tasks_validator = concurrent_tasks_validator
636
637    @model_validator(mode="before")
638    def _validate_authenticator(cls, data: t.Any) -> t.Any:
639        if not isinstance(data, dict):
640            return data
641
642        from snowflake.connector.network import DEFAULT_AUTHENTICATOR, OAUTH_AUTHENTICATOR
643
644        auth = data.get("authenticator")
645        auth = auth.upper() if auth else DEFAULT_AUTHENTICATOR
646        user = data.get("user")
647        password = data.get("password")
648        data["private_key"] = cls._get_private_key(data, auth)  # type: ignore
649
650        if (
651            auth == DEFAULT_AUTHENTICATOR
652            and not data.get("private_key")
653            and (not user or not password)
654        ):
655            raise ConfigError("User and password must be provided if using default authentication")
656
657        if auth == OAUTH_AUTHENTICATOR and not data.get("token"):
658            raise ConfigError("Token must be provided if using oauth authentication")
659
660        return data
661
662    _engine_import_validator = _get_engine_import_validator(
663        "snowflake.connector.network", "snowflake"
664    )
665
666    @classmethod
667    def _get_private_key(cls, values: t.Dict[str, t.Optional[str]], auth: str) -> t.Optional[bytes]:
668        """
669        source: https://github.com/dbt-labs/dbt-snowflake/blob/0374b4ec948982f2ac8ec0c95d53d672ad19e09c/dbt/adapters/snowflake/connections.py#L247C5-L285C1
670
671        Overall code change: Use local variables instead of class attributes + Validation
672        """
673        # Start custom code
674        from cryptography.hazmat.backends import default_backend
675        from cryptography.hazmat.primitives import serialization
676        from snowflake.connector.network import (
677            DEFAULT_AUTHENTICATOR,
678            KEY_PAIR_AUTHENTICATOR,
679        )
680
681        private_key = values.get("private_key")
682        private_key_path = values.get("private_key_path")
683        private_key_passphrase = values.get("private_key_passphrase")
684        user = values.get("user")
685        password = values.get("password")
686        auth = auth if auth and auth != DEFAULT_AUTHENTICATOR else KEY_PAIR_AUTHENTICATOR
687
688        if not private_key and not private_key_path:
689            return None
690        if private_key and private_key_path:
691            raise ConfigError("Cannot specify both `private_key` and `private_key_path`")
692        if auth != KEY_PAIR_AUTHENTICATOR:
693            raise ConfigError(
694                f"Private key or private key path can only be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
695            )
696        if not user:
697            raise ConfigError(
698                f"User must be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
699            )
700        if password:
701            raise ConfigError(
702                f"Password cannot be provided when using {KEY_PAIR_AUTHENTICATOR} authentication"
703            )
704
705        if isinstance(private_key, bytes):
706            return private_key
707        # End Custom Code
708
709        if private_key_passphrase:
710            encoded_passphrase = private_key_passphrase.encode()
711        else:
712            encoded_passphrase = None
713
714        if private_key:
715            if private_key.startswith("-"):
716                p_key = serialization.load_pem_private_key(
717                    data=bytes(private_key, "utf-8"),
718                    password=encoded_passphrase,
719                    backend=default_backend(),
720                )
721
722            else:
723                p_key = serialization.load_der_private_key(
724                    data=base64.b64decode(private_key),
725                    password=encoded_passphrase,
726                    backend=default_backend(),
727                )
728
729        elif private_key_path:
730            with open(private_key_path, "rb") as key:
731                p_key = serialization.load_pem_private_key(
732                    key.read(), password=encoded_passphrase, backend=default_backend()
733                )
734        else:
735            return None
736
737        return p_key.private_bytes(
738            encoding=serialization.Encoding.DER,
739            format=serialization.PrivateFormat.PKCS8,
740            encryption_algorithm=serialization.NoEncryption(),
741        )
742
743    @property
744    def _connection_kwargs_keys(self) -> t.Set[str]:
745        return {
746            "user",
747            "password",
748            "account",
749            "warehouse",
750            "database",
751            "role",
752            "authenticator",
753            "token",
754            "private_key",
755            "session_parameters",
756            "application",
757            "host",
758            "port",
759        }
760
761    @property
762    def _engine_adapter(self) -> t.Type[EngineAdapter]:
763        return engine_adapter.SnowflakeEngineAdapter
764
765    @property
766    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
767        return {"autocommit": False}
768
769    @property
770    def _connection_factory(self) -> t.Callable:
771        from snowflake import connector
772
773        return connector.connect

Configuration for the Snowflake connection.

Arguments:
  • account: The Snowflake account name.
  • user: The Snowflake username.
  • password: The Snowflake password.
  • warehouse: The optional warehouse name.
  • database: The optional database name.
  • role: The optional role name.
  • concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
  • authenticator: The optional authenticator name. Defaults to username/password authentication ("snowflake"). Options: https://github.com/snowflakedb/snowflake-connector-python/blob/e937591356c067a77f34a0a42328907fda792c23/src/snowflake/connector/network.py#L178-L183
  • token: The optional oauth access token to use for authentication when authenticator is set to "oauth".
  • private_key: The optional private key to use for authentication. Key can be Base64-encoded DER format (representing the key bytes), a plain-text PEM format, or bytes (Python config only). https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#using-key-pair-authentication-key-pair-rotation
  • private_key_path: The optional path to the private key to use for authentication. This would be used instead of private_key.
  • private_key_passphrase: The optional passphrase to use to decrypt private_key or private_key_path. Keys can be created without encryption so only provide this if needed.
  • register_comments: Whether or not to register model comments with the SQL engine.
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
  • session_parameters: The optional session parameters to set for the connection.
  • host: Host address for the connection.
  • port: Port for the connection.
account: str
user: Optional[str]
password: Optional[str]
warehouse: Optional[str]
database: Optional[str]
role: Optional[str]
authenticator: Optional[str]
token: Optional[str]
host: Optional[str]
port: Optional[int]
application: Literal['Tobiko_SQLMesh']
private_key: Union[str, bytes, NoneType]
private_key_path: Optional[str]
private_key_passphrase: Optional[str]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
session_parameters: Optional[dict]
type_: Literal['snowflake']
DIALECT: ClassVar[Literal['snowflake']] = 'snowflake'
DISPLAY_NAME: ClassVar[Literal['Snowflake']] = 'Snowflake'
DISPLAY_ORDER: ClassVar[Literal[2]] = 2
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class DatabricksConnectionConfig(ConnectionConfig):
 776class DatabricksConnectionConfig(ConnectionConfig):
 777    """
 778    Databricks connection that uses the SQL connector for SQL models and then Databricks Connect for Dataframe operations
 779
 780    Arg Source: https://github.com/databricks/databricks-sql-python/blob/main/src/databricks/sql/client.py#L39
 781    OAuth ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-machine-to-machine-m2m-authentication
 782
 783    Args:
 784        server_hostname: Databricks instance host name.
 785        http_path: Http path either to a DBSQL endpoint (e.g. /sql/1.0/endpoints/1234567890abcdef)
 786            or to a DBR interactive cluster (e.g. /sql/protocolv1/o/1234567890123456/1234-123456-slid123)
 787        access_token: Http Bearer access token, e.g. Databricks Personal Access Token.
 788        auth_type: Set to 'databricks-oauth' or 'azure-oauth' to trigger OAuth (or dont set at all to use `access_token`)
 789        oauth_client_id: Client ID to use when auth_type is set to one of the 'oauth' types
 790        oauth_client_secret: Client Secret to use when auth_type is set to one of the 'oauth' types
 791        catalog: Default catalog to use for SQL models. Defaults to None which means it will use the default set in
 792            the Databricks cluster (most likely `hive_metastore`).
 793        http_headers: An optional list of (k, v) pairs that will be set as Http headers on every request
 794        session_configuration: An optional dictionary of Spark session parameters.
 795            Execute the SQL command `SET -v` to get a full list of available commands.
 796        databricks_connect_server_hostname: The hostname to use when establishing a connecting using Databricks Connect.
 797            Defaults to the `server_hostname` value.
 798        databricks_connect_access_token: The access token to use when establishing a connecting using Databricks Connect.
 799            Defaults to the `access_token` value.
 800        databricks_connect_cluster_id: The cluster id to use when establishing a connecting using Databricks Connect.
 801            Defaults to deriving the cluster id from the `http_path` value.
 802        force_databricks_connect: Force all queries to run using Databricks Connect instead of the SQL connector.
 803        disable_databricks_connect: Even if databricks connect is installed, do not use it.
 804        disable_spark_session: Do not use SparkSession if it is available (like when running in a notebook).
 805        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
 806    """
 807
 808    server_hostname: t.Optional[str] = None
 809    http_path: t.Optional[str] = None
 810    access_token: t.Optional[str] = None
 811    auth_type: t.Optional[str] = None
 812    oauth_client_id: t.Optional[str] = None
 813    oauth_client_secret: t.Optional[str] = None
 814    catalog: t.Optional[str] = None
 815    http_headers: t.Optional[t.List[t.Tuple[str, str]]] = None
 816    session_configuration: t.Optional[t.Dict[str, t.Any]] = None
 817    databricks_connect_server_hostname: t.Optional[str] = None
 818    databricks_connect_access_token: t.Optional[str] = None
 819    databricks_connect_cluster_id: t.Optional[str] = None
 820    databricks_connect_use_serverless: bool = False
 821    force_databricks_connect: bool = False
 822    disable_databricks_connect: bool = False
 823    disable_spark_session: bool = False
 824
 825    concurrent_tasks: int = 1
 826    register_comments: bool = True
 827    pre_ping: t.Literal[False] = False
 828
 829    type_: t.Literal["databricks"] = Field(alias="type", default="databricks")
 830    DIALECT: t.ClassVar[t.Literal["databricks"]] = "databricks"
 831    DISPLAY_NAME: t.ClassVar[t.Literal["Databricks"]] = "Databricks"
 832    DISPLAY_ORDER: t.ClassVar[t.Literal[3]] = 3
 833
 834    _concurrent_tasks_validator = concurrent_tasks_validator
 835    _http_headers_validator = http_headers_validator
 836
 837    @computed_field  # type: ignore[prop-decorator]
 838    @property
 839    def shared_connection(self) -> bool:
 840        """The connection should only be shared if U2M OAuth is being used"""
 841        return self.auth_type is not None and self.oauth_client_secret is None
 842
 843    @model_validator(mode="before")
 844    def _databricks_connect_validator(cls, data: t.Any) -> t.Any:
 845        # SQLQueryContextLogger will output any error SQL queries even if they are in a try/except block.
 846        # Disabling this allows SQLMesh to determine what should be shown to the user.
 847        # Ex: We describe a table to see if it exists and therefore that execution can fail but we don't need to show
 848        # the user since it is expected if the table doesn't exist. Without this change the user would see the error.
 849        logging.getLogger("SQLQueryContextLogger").setLevel(logging.CRITICAL)
 850
 851        if not isinstance(data, dict):
 852            return data
 853
 854        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 855
 856        if DatabricksEngineAdapter.can_access_spark_session(
 857            bool(data.get("disable_spark_session"))
 858        ):
 859            return data
 860
 861        databricks_connect_use_serverless = data.get("databricks_connect_use_serverless")
 862        server_hostname, http_path, access_token, auth_type = (
 863            data.get("server_hostname"),
 864            data.get("http_path"),
 865            data.get("access_token"),
 866            data.get("auth_type"),
 867        )
 868
 869        if (not server_hostname or not http_path or not access_token) and (
 870            not databricks_connect_use_serverless and not auth_type
 871        ):
 872            raise ValueError(
 873                "`server_hostname`, `http_path`, and `access_token` are required for Databricks connections when not running in a notebook"
 874            )
 875        if (
 876            databricks_connect_use_serverless
 877            and not server_hostname
 878            and not data.get("databricks_connect_server_hostname")
 879        ):
 880            raise ValueError(
 881                "`server_hostname` or `databricks_connect_server_hostname` is required when `databricks_connect_use_serverless` is set"
 882            )
 883        if DatabricksEngineAdapter.can_access_databricks_connect(
 884            bool(data.get("disable_databricks_connect"))
 885        ):
 886            if not data.get("databricks_connect_access_token"):
 887                data["databricks_connect_access_token"] = access_token
 888            if not data.get("databricks_connect_server_hostname"):
 889                data["databricks_connect_server_hostname"] = f"https://{server_hostname}"
 890            if not databricks_connect_use_serverless and not data.get(
 891                "databricks_connect_cluster_id"
 892            ):
 893                if t.TYPE_CHECKING:
 894                    assert http_path is not None
 895                data["databricks_connect_cluster_id"] = http_path.split("/")[-1]
 896
 897        if auth_type:
 898            from databricks.sql.auth.auth import AuthType
 899
 900            all_data = [m.value for m in AuthType]
 901            if auth_type not in all_data:
 902                raise ValueError(
 903                    f"`auth_type` {auth_type} does not match a valid option: {all_data}"
 904                )
 905
 906            client_id = data.get("oauth_client_id")
 907            client_secret = data.get("oauth_client_secret")
 908
 909            if client_secret and not client_id:
 910                raise ValueError(
 911                    "`oauth_client_id` is required when `oauth_client_secret` is specified"
 912                )
 913
 914            if not http_path:
 915                raise ValueError("`http_path` is still required when using `auth_type`")
 916
 917        return data
 918
 919    _engine_import_validator = _get_engine_import_validator("databricks", "databricks")
 920
 921    @property
 922    def _connection_kwargs_keys(self) -> t.Set[str]:
 923        if self.use_spark_session_only:
 924            return set()
 925        return {
 926            "server_hostname",
 927            "http_path",
 928            "access_token",
 929            "http_headers",
 930            "session_configuration",
 931            "catalog",
 932        }
 933
 934    @property
 935    def _engine_adapter(self) -> t.Type[engine_adapter.DatabricksEngineAdapter]:
 936        return engine_adapter.DatabricksEngineAdapter
 937
 938    @property
 939    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
 940        return {
 941            k: v
 942            for k, v in self.dict().items()
 943            if k.startswith("databricks_connect_")
 944            or k in ("catalog", "disable_databricks_connect", "disable_spark_session")
 945        }
 946
 947    @property
 948    def use_spark_session_only(self) -> bool:
 949        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 950
 951        return (
 952            DatabricksEngineAdapter.can_access_spark_session(self.disable_spark_session)
 953            or self.force_databricks_connect
 954        )
 955
 956    @property
 957    def _connection_factory(self) -> t.Callable:
 958        if self.use_spark_session_only:
 959            from sqlmesh.engines.spark.db_api.spark_session import connection
 960
 961            return connection
 962
 963        from databricks import sql  # type: ignore
 964
 965        return sql.connect
 966
 967    @property
 968    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
 969        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
 970
 971        if not self.use_spark_session_only:
 972            conn_kwargs: t.Dict[str, t.Any] = {
 973                "_user_agent_entry": "sqlmesh",
 974            }
 975
 976            if self.auth_type and "oauth" in self.auth_type:
 977                # there are two types of oauth: User-to-Machine (U2M) and Machine-to-Machine (M2M)
 978                if self.oauth_client_secret:
 979                    # if a client_secret exists, then a client_id also exists and we are using M2M
 980                    # ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-machine-to-machine-m2m-authentication
 981                    # ref: https://github.com/databricks/databricks-sql-python/blob/main/examples/m2m_oauth.py
 982                    from databricks.sdk.core import Config, oauth_service_principal
 983
 984                    config = Config(
 985                        host=f"https://{self.server_hostname}",
 986                        client_id=self.oauth_client_id,
 987                        client_secret=self.oauth_client_secret,
 988                    )
 989                    conn_kwargs["credentials_provider"] = lambda: oauth_service_principal(config)
 990                else:
 991                    # if auth_type is set to an 'oauth' type but no client_id/secret are set, then we are using U2M
 992                    # ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-user-to-machine-u2m-authentication
 993                    conn_kwargs["auth_type"] = self.auth_type
 994
 995            return conn_kwargs
 996
 997        if DatabricksEngineAdapter.can_access_spark_session(self.disable_spark_session):
 998            from pyspark.sql import SparkSession
 999
1000            return dict(
1001                spark=SparkSession.getActiveSession(),
1002                catalog=self.catalog,
1003            )
1004
1005        from databricks.connect import DatabricksSession
1006
1007        if t.TYPE_CHECKING:
1008            assert self.databricks_connect_server_hostname is not None
1009            assert self.databricks_connect_access_token is not None
1010
1011        if self.databricks_connect_use_serverless:
1012            builder = DatabricksSession.builder.remote(
1013                host=self.databricks_connect_server_hostname,
1014                token=self.databricks_connect_access_token,
1015                serverless=True,
1016            )
1017        else:
1018            if t.TYPE_CHECKING:
1019                assert self.databricks_connect_cluster_id is not None
1020            builder = DatabricksSession.builder.remote(
1021                host=self.databricks_connect_server_hostname,
1022                token=self.databricks_connect_access_token,
1023                cluster_id=self.databricks_connect_cluster_id,
1024            )
1025
1026        return dict(
1027            spark=builder.userAgent("sqlmesh").getOrCreate(),
1028            catalog=self.catalog,
1029        )

Databricks connection that uses the SQL connector for SQL models and then Databricks Connect for Dataframe operations

Arg Source: https://github.com/databricks/databricks-sql-python/blob/main/src/databricks/sql/client.py#L39 OAuth ref: https://docs.databricks.com/en/dev-tools/python-sql-connector.html#oauth-machine-to-machine-m2m-authentication

Arguments:
  • server_hostname: Databricks instance host name.
  • http_path: Http path either to a DBSQL endpoint (e.g. /sql/1.0/endpoints/1234567890abcdef) or to a DBR interactive cluster (e.g. /sql/protocolv1/o/1234567890123456/1234-123456-slid123)
  • access_token: Http Bearer access token, e.g. Databricks Personal Access Token.
  • auth_type: Set to 'databricks-oauth' or 'azure-oauth' to trigger OAuth (or dont set at all to use access_token)
  • oauth_client_id: Client ID to use when auth_type is set to one of the 'oauth' types
  • oauth_client_secret: Client Secret to use when auth_type is set to one of the 'oauth' types
  • catalog: Default catalog to use for SQL models. Defaults to None which means it will use the default set in the Databricks cluster (most likely hive_metastore).
  • http_headers: An optional list of (k, v) pairs that will be set as Http headers on every request
  • session_configuration: An optional dictionary of Spark session parameters. Execute the SQL command SET -v to get a full list of available commands.
  • databricks_connect_server_hostname: The hostname to use when establishing a connecting using Databricks Connect. Defaults to the server_hostname value.
  • databricks_connect_access_token: The access token to use when establishing a connecting using Databricks Connect. Defaults to the access_token value.
  • databricks_connect_cluster_id: The cluster id to use when establishing a connecting using Databricks Connect. Defaults to deriving the cluster id from the http_path value.
  • force_databricks_connect: Force all queries to run using Databricks Connect instead of the SQL connector.
  • disable_databricks_connect: Even if databricks connect is installed, do not use it.
  • disable_spark_session: Do not use SparkSession if it is available (like when running in a notebook).
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
server_hostname: Optional[str]
http_path: Optional[str]
access_token: Optional[str]
auth_type: Optional[str]
oauth_client_id: Optional[str]
oauth_client_secret: Optional[str]
catalog: Optional[str]
http_headers: Optional[List[Tuple[str, str]]]
session_configuration: Optional[Dict[str, Any]]
databricks_connect_server_hostname: Optional[str]
databricks_connect_access_token: Optional[str]
databricks_connect_cluster_id: Optional[str]
databricks_connect_use_serverless: bool
force_databricks_connect: bool
disable_databricks_connect: bool
disable_spark_session: bool
concurrent_tasks: int
register_comments: bool
pre_ping: Literal[False]
type_: Literal['databricks']
DIALECT: ClassVar[Literal['databricks']] = 'databricks'
DISPLAY_NAME: ClassVar[Literal['Databricks']] = 'Databricks'
DISPLAY_ORDER: ClassVar[Literal[3]] = 3
shared_connection: bool
837    @computed_field  # type: ignore[prop-decorator]
838    @property
839    def shared_connection(self) -> bool:
840        """The connection should only be shared if U2M OAuth is being used"""
841        return self.auth_type is not None and self.oauth_client_secret is None

The connection should only be shared if U2M OAuth is being used

use_spark_session_only: bool
947    @property
948    def use_spark_session_only(self) -> bool:
949        from sqlmesh.core.engine_adapter.databricks import DatabricksEngineAdapter
950
951        return (
952            DatabricksEngineAdapter.can_access_spark_session(self.disable_spark_session)
953            or self.force_databricks_connect
954        )
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class BigQueryConnectionMethod(builtins.str, enum.Enum):
1032class BigQueryConnectionMethod(str, Enum):
1033    OAUTH = "oauth"
1034    OAUTH_SECRETS = "oauth-secrets"
1035    SERVICE_ACCOUNT = "service-account"
1036    SERVICE_ACCOUNT_JSON = "service-account-json"

An enumeration.

OAUTH = <BigQueryConnectionMethod.OAUTH: 'oauth'>
OAUTH_SECRETS = <BigQueryConnectionMethod.OAUTH_SECRETS: 'oauth-secrets'>
SERVICE_ACCOUNT = <BigQueryConnectionMethod.SERVICE_ACCOUNT: 'service-account'>
SERVICE_ACCOUNT_JSON = <BigQueryConnectionMethod.SERVICE_ACCOUNT_JSON: 'service-account-json'>
Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
class BigQueryPriority(builtins.str, enum.Enum):
1039class BigQueryPriority(str, Enum):
1040    BATCH = "batch"
1041    INTERACTIVE = "interactive"
1042
1043    @property
1044    def is_batch(self) -> bool:
1045        return self == self.BATCH
1046
1047    @property
1048    def is_interactive(self) -> bool:
1049        return self == self.INTERACTIVE
1050
1051    @property
1052    def bigquery_constant(self) -> str:
1053        from google.cloud.bigquery import QueryPriority
1054
1055        if self.is_batch:
1056            return QueryPriority.BATCH
1057        return QueryPriority.INTERACTIVE

An enumeration.

BATCH = <BigQueryPriority.BATCH: 'batch'>
INTERACTIVE = <BigQueryPriority.INTERACTIVE: 'interactive'>
is_batch: bool
1043    @property
1044    def is_batch(self) -> bool:
1045        return self == self.BATCH
is_interactive: bool
1047    @property
1048    def is_interactive(self) -> bool:
1049        return self == self.INTERACTIVE
bigquery_constant: str
1051    @property
1052    def bigquery_constant(self) -> str:
1053        from google.cloud.bigquery import QueryPriority
1054
1055        if self.is_batch:
1056            return QueryPriority.BATCH
1057        return QueryPriority.INTERACTIVE
Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
class BigQueryConnectionConfig(ConnectionConfig):
1060class BigQueryConnectionConfig(ConnectionConfig):
1061    """
1062    BigQuery Connection Configuration.
1063    """
1064
1065    method: BigQueryConnectionMethod = BigQueryConnectionMethod.OAUTH
1066
1067    project: t.Optional[str] = None
1068    execution_project: t.Optional[str] = None
1069    quota_project: t.Optional[str] = None
1070    location: t.Optional[str] = None
1071    # Keyfile Auth
1072    keyfile: t.Optional[str] = None
1073    keyfile_json: t.Optional[t.Dict[str, t.Any]] = None
1074    # Oath Secret Auth
1075    token: t.Optional[str] = None
1076    refresh_token: t.Optional[str] = None
1077    client_id: t.Optional[str] = None
1078    client_secret: t.Optional[str] = None
1079    token_uri: t.Optional[str] = None
1080    scopes: t.Tuple[str, ...] = ("https://www.googleapis.com/auth/bigquery",)
1081    impersonated_service_account: t.Optional[str] = None
1082    # Extra Engine Config
1083    job_creation_timeout_seconds: t.Optional[int] = None
1084    job_execution_timeout_seconds: t.Optional[int] = None
1085    job_retries: t.Optional[int] = 1
1086    job_retry_deadline_seconds: t.Optional[int] = None
1087    priority: t.Optional[BigQueryPriority] = None
1088    maximum_bytes_billed: t.Optional[int] = None
1089    reservation: t.Optional[str] = None
1090
1091    concurrent_tasks: int = 1
1092    register_comments: bool = True
1093    pre_ping: t.Literal[False] = False
1094
1095    type_: t.Literal["bigquery"] = Field(alias="type", default="bigquery")
1096    DIALECT: t.ClassVar[t.Literal["bigquery"]] = "bigquery"
1097    DISPLAY_NAME: t.ClassVar[t.Literal["BigQuery"]] = "BigQuery"
1098    DISPLAY_ORDER: t.ClassVar[t.Literal[4]] = 4
1099
1100    _engine_import_validator = _get_engine_import_validator("google.cloud.bigquery", "bigquery")
1101
1102    @field_validator("execution_project")
1103    def validate_execution_project(
1104        cls,
1105        v: t.Optional[str],
1106        info: ValidationInfo,
1107    ) -> t.Optional[str]:
1108        if v and not validation_data(info).get("project"):
1109            raise ConfigError(
1110                "If the `execution_project` field is specified, you must also specify the `project` field to provide a default object location."
1111            )
1112        return v
1113
1114    @field_validator("quota_project")
1115    def validate_quota_project(
1116        cls,
1117        v: t.Optional[str],
1118        info: ValidationInfo,
1119    ) -> t.Optional[str]:
1120        if v and not validation_data(info).get("project"):
1121            raise ConfigError(
1122                "If the `quota_project` field is specified, you must also specify the `project` field to provide a default object location."
1123            )
1124        return v
1125
1126    @property
1127    def _connection_kwargs_keys(self) -> t.Set[str]:
1128        return set()
1129
1130    @property
1131    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1132        return engine_adapter.BigQueryEngineAdapter
1133
1134    @property
1135    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
1136        """The static connection kwargs for this connection"""
1137        import google.auth
1138        from google.api_core import client_info, client_options
1139        from google.auth import impersonated_credentials
1140        from google.oauth2 import credentials, service_account
1141
1142        if self.method == BigQueryConnectionMethod.OAUTH:
1143            creds, _ = google.auth.default(scopes=self.scopes)
1144        elif self.method == BigQueryConnectionMethod.SERVICE_ACCOUNT:
1145            creds = service_account.Credentials.from_service_account_file(
1146                self.keyfile, scopes=self.scopes
1147            )
1148        elif self.method == BigQueryConnectionMethod.SERVICE_ACCOUNT_JSON:
1149            creds = service_account.Credentials.from_service_account_info(
1150                self.keyfile_json, scopes=self.scopes
1151            )
1152        elif self.method == BigQueryConnectionMethod.OAUTH_SECRETS:
1153            creds = credentials.Credentials(
1154                token=self.token,
1155                refresh_token=self.refresh_token,
1156                client_id=self.client_id,
1157                client_secret=self.client_secret,
1158                token_uri=self.token_uri,
1159                scopes=self.scopes,
1160            )
1161        else:
1162            raise ConfigError("Invalid BigQuery Connection Method")
1163
1164        if self.impersonated_service_account:
1165            creds = impersonated_credentials.Credentials(
1166                source_credentials=creds,
1167                target_principal=self.impersonated_service_account,
1168                target_scopes=self.scopes,
1169            )
1170
1171        options = client_options.ClientOptions(quota_project_id=self.quota_project)
1172        project = self.execution_project or self.project or None
1173
1174        client = google.cloud.bigquery.Client(
1175            project=project and exp.parse_identifier(project, dialect="bigquery").name,
1176            credentials=creds,
1177            location=self.location,
1178            client_info=client_info.ClientInfo(user_agent="sqlmesh"),
1179            client_options=options,
1180        )
1181
1182        return {
1183            "client": client,
1184        }
1185
1186    @property
1187    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1188        return {
1189            k: v
1190            for k, v in self.dict().items()
1191            if k
1192            in {
1193                "job_creation_timeout_seconds",
1194                "job_execution_timeout_seconds",
1195                "job_retries",
1196                "job_retry_deadline_seconds",
1197                "priority",
1198                "maximum_bytes_billed",
1199                "reservation",
1200            }
1201        }
1202
1203    @property
1204    def _connection_factory(self) -> t.Callable:
1205        from google.cloud.bigquery.dbapi import connect
1206
1207        return connect
1208
1209    def get_catalog(self) -> t.Optional[str]:
1210        return self.project

BigQuery Connection Configuration.

project: Optional[str]
execution_project: Optional[str]
quota_project: Optional[str]
location: Optional[str]
keyfile: Optional[str]
keyfile_json: Optional[Dict[str, Any]]
token: Optional[str]
refresh_token: Optional[str]
client_id: Optional[str]
client_secret: Optional[str]
token_uri: Optional[str]
scopes: Tuple[str, ...]
impersonated_service_account: Optional[str]
job_creation_timeout_seconds: Optional[int]
job_execution_timeout_seconds: Optional[int]
job_retries: Optional[int]
job_retry_deadline_seconds: Optional[int]
priority: Optional[BigQueryPriority]
maximum_bytes_billed: Optional[int]
reservation: Optional[str]
concurrent_tasks: int
register_comments: bool
pre_ping: Literal[False]
type_: Literal['bigquery']
DIALECT: ClassVar[Literal['bigquery']] = 'bigquery'
DISPLAY_NAME: ClassVar[Literal['BigQuery']] = 'BigQuery'
DISPLAY_ORDER: ClassVar[Literal[4]] = 4
@field_validator('execution_project')
def validate_execution_project( cls, v: Optional[str], info: pydantic_core.core_schema.ValidationInfo) -> Optional[str]:
1102    @field_validator("execution_project")
1103    def validate_execution_project(
1104        cls,
1105        v: t.Optional[str],
1106        info: ValidationInfo,
1107    ) -> t.Optional[str]:
1108        if v and not validation_data(info).get("project"):
1109            raise ConfigError(
1110                "If the `execution_project` field is specified, you must also specify the `project` field to provide a default object location."
1111            )
1112        return v
@field_validator('quota_project')
def validate_quota_project( cls, v: Optional[str], info: pydantic_core.core_schema.ValidationInfo) -> Optional[str]:
1114    @field_validator("quota_project")
1115    def validate_quota_project(
1116        cls,
1117        v: t.Optional[str],
1118        info: ValidationInfo,
1119    ) -> t.Optional[str]:
1120        if v and not validation_data(info).get("project"):
1121            raise ConfigError(
1122                "If the `quota_project` field is specified, you must also specify the `project` field to provide a default object location."
1123            )
1124        return v
def get_catalog(self) -> Optional[str]:
1209    def get_catalog(self) -> t.Optional[str]:
1210        return self.project

The catalog for this connection

model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class GCPPostgresConnectionConfig(ConnectionConfig):
1213class GCPPostgresConnectionConfig(ConnectionConfig):
1214    """
1215    Postgres Connection Configuration for GCP.
1216
1217    Args:
1218        instance_connection_string: Connection name for the postgres instance.
1219        user: Postgres or IAM user's name
1220        password: The postgres user's password. Only needed when the user is a postgres user.
1221        enable_iam_auth: Set to True when user is an IAM user.
1222        db: Name of the db to connect to.
1223        keyfile: string path to json service account credentials file
1224        keyfile_json: dict service account credentials info
1225        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
1226    """
1227
1228    instance_connection_string: str
1229    user: str
1230    password: t.Optional[str] = None
1231    enable_iam_auth: t.Optional[bool] = None
1232    db: str
1233    ip_type: t.Union[t.Literal["public"], t.Literal["private"], t.Literal["psc"]] = "public"
1234    # Keyfile Auth
1235    keyfile: t.Optional[str] = None
1236    keyfile_json: t.Optional[t.Dict[str, t.Any]] = None
1237    timeout: t.Optional[int] = None
1238    scopes: t.Tuple[str, ...] = ("https://www.googleapis.com/auth/sqlservice.admin",)
1239    driver: str = "pg8000"
1240
1241    type_: t.Literal["gcp_postgres"] = Field(alias="type", default="gcp_postgres")
1242    DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres"
1243    DISPLAY_NAME: t.ClassVar[t.Literal["GCP Postgres"]] = "GCP Postgres"
1244    DISPLAY_ORDER: t.ClassVar[t.Literal[13]] = 13
1245
1246    concurrent_tasks: int = 4
1247    register_comments: bool = True
1248    pre_ping: bool = True
1249
1250    _engine_import_validator = _get_engine_import_validator(
1251        "google.cloud.sql", "gcp_postgres", "gcppostgres"
1252    )
1253
1254    @model_validator(mode="before")
1255    def _validate_auth_method(cls, data: t.Any) -> t.Any:
1256        if not isinstance(data, dict):
1257            return data
1258
1259        password = data.get("password")
1260        enable_iam_auth = data.get("enable_iam_auth")
1261
1262        if not password and not enable_iam_auth:
1263            raise ConfigError(
1264                "GCP Postgres connection configuration requires either password set"
1265                " for a postgres user account or enable_iam_auth set to 'True'"
1266                " for an IAM user account."
1267            )
1268
1269        return data
1270
1271    @property
1272    def _connection_kwargs_keys(self) -> t.Set[str]:
1273        return {
1274            "instance_connection_string",
1275            "driver",
1276            "user",
1277            "password",
1278            "db",
1279            "enable_iam_auth",
1280            "timeout",
1281        }
1282
1283    @property
1284    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1285        return engine_adapter.PostgresEngineAdapter
1286
1287    @property
1288    def _connection_factory(self) -> t.Callable:
1289        from google.cloud.sql.connector import Connector
1290        from google.oauth2 import service_account
1291
1292        creds = None
1293        if self.keyfile:
1294            creds = service_account.Credentials.from_service_account_file(
1295                self.keyfile, scopes=self.scopes
1296            )
1297        elif self.keyfile_json:
1298            creds = service_account.Credentials.from_service_account_info(
1299                self.keyfile_json, scopes=self.scopes
1300            )
1301
1302        kwargs = {
1303            "credentials": creds,
1304            "ip_type": self.ip_type,
1305        }
1306
1307        if self.timeout:
1308            kwargs["timeout"] = self.timeout
1309
1310        return Connector(**kwargs).connect  # type: ignore

Postgres Connection Configuration for GCP.

Arguments:
  • instance_connection_string: Connection name for the postgres instance.
  • user: Postgres or IAM user's name
  • password: The postgres user's password. Only needed when the user is a postgres user.
  • enable_iam_auth: Set to True when user is an IAM user.
  • db: Name of the db to connect to.
  • keyfile: string path to json service account credentials file
  • keyfile_json: dict service account credentials info
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
instance_connection_string: str
user: str
password: Optional[str]
enable_iam_auth: Optional[bool]
db: str
ip_type: Union[Literal['public'], Literal['private'], Literal['psc']]
keyfile: Optional[str]
keyfile_json: Optional[Dict[str, Any]]
timeout: Optional[int]
scopes: Tuple[str, ...]
driver: str
type_: Literal['gcp_postgres']
DIALECT: ClassVar[Literal['postgres']] = 'postgres'
DISPLAY_NAME: ClassVar[Literal['GCP Postgres']] = 'GCP Postgres'
DISPLAY_ORDER: ClassVar[Literal[13]] = 13
concurrent_tasks: int
register_comments: bool
pre_ping: bool
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class RedshiftConnectionConfig(ConnectionConfig):
1313class RedshiftConnectionConfig(ConnectionConfig):
1314    """
1315    Redshift Connection Configuration.
1316
1317    Arg Source: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/__init__.py#L146
1318    Note: A subset of properties were selected. Please open an issue/PR if you want to see more supported.
1319
1320    Args:
1321        user: The username to use for authentication with the Amazon Redshift cluster.
1322        password: The password to use for authentication with the Amazon Redshift cluster.
1323        database: The name of the database instance to connect to.
1324        host: The hostname of the Amazon Redshift cluster.
1325        port: The port number of the Amazon Redshift cluster. Default value is 5439.
1326        source_address: No description provided
1327        unix_sock: No description provided
1328        ssl: Is SSL enabled. Default value is ``True``. SSL must be enabled when authenticating using IAM.
1329        sslmode: The security of the connection to the Amazon Redshift cluster. 'verify-ca' and 'verify-full' are supported.
1330        timeout: The number of seconds before the connection to the server will timeout. By default there is no timeout.
1331        tcp_keepalive: Is `TCP keepalive <https://en.wikipedia.org/wiki/Keepalive#TCP_keepalive>`_ used. The default value is ``True``.
1332        application_name: Sets the application name. The default value is None.
1333        preferred_role: The IAM role preferred for the current connection.
1334        principal_arn: The ARN of the IAM entity (user or role) for which you are generating a policy.
1335        credentials_provider: The class name of the IdP that will be used for authenticating with the Amazon Redshift cluster.
1336        region: The AWS region where the Amazon Redshift cluster is located.
1337        cluster_identifier: The cluster identifier of the Amazon Redshift cluster.
1338        iam: If IAM authentication is enabled. Default value is False. IAM must be True when authenticating using an IdP.
1339        db_user: The database user to authenticate as. Required when using IAM authentication.
1340        is_serverless: Redshift end-point is serverless or provisional. Default value false.
1341        serverless_acct_id: The account ID of the serverless. Default value None
1342        serverless_work_group: The name of work group for serverless end point. Default value None.
1343        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
1344        enable_merge: Whether to use the Redshift merge operation instead of the SQLMesh logical merge.
1345    """
1346
1347    user: t.Optional[str] = None
1348    password: t.Optional[str] = None
1349    database: t.Optional[str] = None
1350    host: t.Optional[str] = None
1351    port: t.Optional[int] = None
1352    source_address: t.Optional[str] = None
1353    unix_sock: t.Optional[str] = None
1354    ssl: t.Optional[bool] = None
1355    sslmode: t.Optional[str] = None
1356    timeout: t.Optional[int] = None
1357    tcp_keepalive: t.Optional[bool] = None
1358    application_name: t.Optional[str] = None
1359    preferred_role: t.Optional[str] = None
1360    principal_arn: t.Optional[str] = None
1361    credentials_provider: t.Optional[str] = None
1362    region: t.Optional[str] = None
1363    cluster_identifier: t.Optional[str] = None
1364    iam: t.Optional[bool] = None
1365    db_user: t.Optional[str] = None
1366    is_serverless: t.Optional[bool] = None
1367    serverless_acct_id: t.Optional[str] = None
1368    serverless_work_group: t.Optional[str] = None
1369    enable_merge: t.Optional[bool] = None
1370
1371    concurrent_tasks: int = 4
1372    register_comments: bool = True
1373    pre_ping: bool = False
1374
1375    type_: t.Literal["redshift"] = Field(alias="type", default="redshift")
1376    DIALECT: t.ClassVar[t.Literal["redshift"]] = "redshift"
1377    DISPLAY_NAME: t.ClassVar[t.Literal["Redshift"]] = "Redshift"
1378    DISPLAY_ORDER: t.ClassVar[t.Literal[7]] = 7
1379
1380    _engine_import_validator = _get_engine_import_validator("redshift_connector", "redshift")
1381
1382    @property
1383    def _connection_kwargs_keys(self) -> t.Set[str]:
1384        return {
1385            "user",
1386            "password",
1387            "database",
1388            "host",
1389            "port",
1390            "source_address",
1391            "unix_sock",
1392            "ssl",
1393            "sslmode",
1394            "timeout",
1395            "tcp_keepalive",
1396            "application_name",
1397            "preferred_role",
1398            "principal_arn",
1399            "credentials_provider",
1400            "region",
1401            "cluster_identifier",
1402            "iam",
1403            "db_user",
1404            "is_serverless",
1405            "serverless_acct_id",
1406            "serverless_work_group",
1407        }
1408
1409    @property
1410    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1411        return engine_adapter.RedshiftEngineAdapter
1412
1413    @property
1414    def _connection_factory(self) -> t.Callable:
1415        from redshift_connector import connect
1416
1417        return connect
1418
1419    @property
1420    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1421        return {"enable_merge": self.enable_merge}

Redshift Connection Configuration.

Arg Source: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/__init__.py#L146 Note: A subset of properties were selected. Please open an issue/PR if you want to see more supported.

Arguments:
  • user: The username to use for authentication with the Amazon Redshift cluster.
  • password: The password to use for authentication with the Amazon Redshift cluster.
  • database: The name of the database instance to connect to.
  • host: The hostname of the Amazon Redshift cluster.
  • port: The port number of the Amazon Redshift cluster. Default value is 5439.
  • source_address: No description provided
  • unix_sock: No description provided
  • ssl: Is SSL enabled. Default value is True. SSL must be enabled when authenticating using IAM.
  • sslmode: The security of the connection to the Amazon Redshift cluster. 'verify-ca' and 'verify-full' are supported.
  • timeout: The number of seconds before the connection to the server will timeout. By default there is no timeout.
  • tcp_keepalive: Is TCP keepalive used. The default value is True.
  • application_name: Sets the application name. The default value is None.
  • preferred_role: The IAM role preferred for the current connection.
  • principal_arn: The ARN of the IAM entity (user or role) for which you are generating a policy.
  • credentials_provider: The class name of the IdP that will be used for authenticating with the Amazon Redshift cluster.
  • region: The AWS region where the Amazon Redshift cluster is located.
  • cluster_identifier: The cluster identifier of the Amazon Redshift cluster.
  • iam: If IAM authentication is enabled. Default value is False. IAM must be True when authenticating using an IdP.
  • db_user: The database user to authenticate as. Required when using IAM authentication.
  • is_serverless: Redshift end-point is serverless or provisional. Default value false.
  • serverless_acct_id: The account ID of the serverless. Default value None
  • serverless_work_group: The name of work group for serverless end point. Default value None.
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
  • enable_merge: Whether to use the Redshift merge operation instead of the SQLMesh logical merge.
user: Optional[str]
password: Optional[str]
database: Optional[str]
host: Optional[str]
port: Optional[int]
source_address: Optional[str]
unix_sock: Optional[str]
ssl: Optional[bool]
sslmode: Optional[str]
timeout: Optional[int]
tcp_keepalive: Optional[bool]
application_name: Optional[str]
preferred_role: Optional[str]
principal_arn: Optional[str]
credentials_provider: Optional[str]
region: Optional[str]
cluster_identifier: Optional[str]
iam: Optional[bool]
db_user: Optional[str]
is_serverless: Optional[bool]
serverless_acct_id: Optional[str]
serverless_work_group: Optional[str]
enable_merge: Optional[bool]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
type_: Literal['redshift']
DIALECT: ClassVar[Literal['redshift']] = 'redshift'
DISPLAY_NAME: ClassVar[Literal['Redshift']] = 'Redshift'
DISPLAY_ORDER: ClassVar[Literal[7]] = 7
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class PostgresConnectionConfig(ConnectionConfig):
1424class PostgresConnectionConfig(ConnectionConfig):
1425    host: str
1426    user: str
1427    password: str
1428    port: int
1429    database: str
1430    keepalives_idle: t.Optional[int] = None
1431    connect_timeout: int = 10
1432    role: t.Optional[str] = None
1433    sslmode: t.Optional[str] = None
1434    application_name: t.Optional[str] = None
1435
1436    concurrent_tasks: int = 4
1437    register_comments: bool = True
1438    pre_ping: bool = True
1439
1440    type_: t.Literal["postgres"] = Field(alias="type", default="postgres")
1441    DIALECT: t.ClassVar[t.Literal["postgres"]] = "postgres"
1442    DISPLAY_NAME: t.ClassVar[t.Literal["Postgres"]] = "Postgres"
1443    DISPLAY_ORDER: t.ClassVar[t.Literal[12]] = 12
1444
1445    _engine_import_validator = _get_engine_import_validator("psycopg2", "postgres")
1446
1447    @property
1448    def _connection_kwargs_keys(self) -> t.Set[str]:
1449        return {
1450            "host",
1451            "user",
1452            "password",
1453            "port",
1454            "database",
1455            "keepalives_idle",
1456            "connect_timeout",
1457            "sslmode",
1458            "application_name",
1459        }
1460
1461    @property
1462    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1463        return engine_adapter.PostgresEngineAdapter
1464
1465    @property
1466    def _connection_factory(self) -> t.Callable:
1467        from psycopg2 import connect
1468
1469        return connect
1470
1471    @property
1472    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
1473        if not self.role:
1474            return None
1475
1476        def init(cursor: t.Any) -> None:
1477            cursor.execute(f"SET ROLE {self.role}")
1478
1479        return init

Helper class that provides a standard way to create an ABC using inheritance.

host: str
user: str
password: str
port: int
database: str
keepalives_idle: Optional[int]
connect_timeout: int
role: Optional[str]
sslmode: Optional[str]
application_name: Optional[str]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
type_: Literal['postgres']
DIALECT: ClassVar[Literal['postgres']] = 'postgres'
DISPLAY_NAME: ClassVar[Literal['Postgres']] = 'Postgres'
DISPLAY_ORDER: ClassVar[Literal[12]] = 12
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class MySQLConnectionConfig(ConnectionConfig):
1482class MySQLConnectionConfig(ConnectionConfig):
1483    host: str
1484    user: str
1485    password: str
1486    port: t.Optional[int] = None
1487    database: t.Optional[str] = None
1488    charset: t.Optional[str] = None
1489    collation: t.Optional[str] = None
1490    ssl_disabled: t.Optional[bool] = None
1491
1492    concurrent_tasks: int = 4
1493    register_comments: bool = True
1494    pre_ping: bool = True
1495
1496    type_: t.Literal["mysql"] = Field(alias="type", default="mysql")
1497    DIALECT: t.ClassVar[t.Literal["mysql"]] = "mysql"
1498    DISPLAY_NAME: t.ClassVar[t.Literal["MySQL"]] = "MySQL"
1499    DISPLAY_ORDER: t.ClassVar[t.Literal[14]] = 14
1500
1501    _engine_import_validator = _get_engine_import_validator("pymysql", "mysql")
1502
1503    @property
1504    def _connection_kwargs_keys(self) -> t.Set[str]:
1505        connection_keys = {
1506            "host",
1507            "user",
1508            "password",
1509        }
1510        if self.port is not None:
1511            connection_keys.add("port")
1512        if self.database is not None:
1513            connection_keys.add("database")
1514        if self.charset is not None:
1515            connection_keys.add("charset")
1516        if self.collation is not None:
1517            connection_keys.add("collation")
1518        if self.ssl_disabled is not None:
1519            connection_keys.add("ssl_disabled")
1520        return connection_keys
1521
1522    @property
1523    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1524        return engine_adapter.MySQLEngineAdapter
1525
1526    @property
1527    def _connection_factory(self) -> t.Callable:
1528        from pymysql import connect
1529
1530        return connect

Helper class that provides a standard way to create an ABC using inheritance.

host: str
user: str
password: str
port: Optional[int]
database: Optional[str]
charset: Optional[str]
collation: Optional[str]
ssl_disabled: Optional[bool]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
type_: Literal['mysql']
DIALECT: ClassVar[Literal['mysql']] = 'mysql'
DISPLAY_NAME: ClassVar[Literal['MySQL']] = 'MySQL'
DISPLAY_ORDER: ClassVar[Literal[14]] = 14
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class MSSQLConnectionConfig(ConnectionConfig):
1533class MSSQLConnectionConfig(ConnectionConfig):
1534    host: str
1535    user: t.Optional[str] = None
1536    password: t.Optional[str] = None
1537    database: t.Optional[str] = ""
1538    timeout: t.Optional[int] = 0
1539    login_timeout: t.Optional[int] = 60
1540    login_attempts: t.Optional[int] = 1
1541    charset: t.Optional[str] = "UTF-8"
1542    appname: t.Optional[str] = None
1543    port: t.Optional[int] = 1433
1544    conn_properties: t.Optional[t.Union[t.List[str], str]] = None
1545    autocommit: t.Optional[bool] = False
1546    tds_version: t.Optional[str] = None
1547
1548    # Driver options
1549    driver: t.Literal["pymssql", "pyodbc", "mssql-python"] = "pymssql"
1550    # PyODBC specific options
1551    driver_name: t.Optional[str] = None  # e.g. "ODBC Driver 18 for SQL Server"
1552    trust_server_certificate: t.Optional[bool] = None
1553    encrypt: t.Optional[bool] = None
1554    # Dictionary of arbitrary ODBC connection properties
1555    # See: https://learn.microsoft.com/en-us/sql/connect/odbc/dsn-connection-string-attribute
1556    odbc_properties: t.Optional[t.Dict[str, t.Any]] = None
1557
1558    concurrent_tasks: int = 4
1559    register_comments: bool = True
1560    pre_ping: bool = True
1561
1562    type_: t.Literal["mssql"] = Field(alias="type", default="mssql")
1563    DIALECT: t.ClassVar[t.Literal["tsql"]] = "tsql"
1564    DISPLAY_NAME: t.ClassVar[t.Literal["MSSQL"]] = "MSSQL"
1565    DISPLAY_ORDER: t.ClassVar[t.Literal[11]] = 11
1566
1567    @model_validator(mode="before")
1568    @classmethod
1569    def _mssql_engine_import_validator(cls, data: t.Any) -> t.Any:
1570        if not isinstance(data, dict):
1571            return data
1572
1573        driver = data.get("driver", "pymssql")
1574
1575        # Define the mapping of driver to import module and extra name
1576        driver_configs = {
1577            "pymssql": ("pymssql", "mssql"),
1578            "pyodbc": ("pyodbc", "mssql-odbc"),
1579            "mssql-python": ("mssql_python", "mssql-python"),
1580        }
1581
1582        if driver not in driver_configs:
1583            raise ValueError(f"Unsupported driver: {driver}")
1584
1585        import_module, extra_name = driver_configs[driver]
1586
1587        # Use _get_engine_import_validator with decorate=False to get the raw validation function
1588        # This avoids the __wrapped__ issue in Python 3.9
1589        validator_func = _get_engine_import_validator(
1590            import_module, driver, extra_name, decorate=False
1591        )
1592
1593        # Call the raw validation function directly
1594        return validator_func(cls, data)
1595
1596    @property
1597    def _connection_kwargs_keys(self) -> t.Set[str]:
1598        base_keys = {
1599            "host",
1600            "user",
1601            "password",
1602            "database",
1603            "timeout",
1604            "login_timeout",
1605            "charset",
1606            "appname",
1607            "port",
1608            "conn_properties",
1609            "autocommit",
1610            "tds_version",
1611        }
1612
1613        if self.driver == "pyodbc":
1614            base_keys.update(
1615                {
1616                    "driver_name",
1617                    "trust_server_certificate",
1618                    "encrypt",
1619                    "odbc_properties",
1620                }
1621            )
1622            # Remove pymssql-specific parameters
1623            base_keys.discard("tds_version")
1624            base_keys.discard("conn_properties")
1625
1626        elif self.driver == "mssql-python":
1627            base_keys.update(
1628                {
1629                    "trust_server_certificate",
1630                    "encrypt",
1631                    "odbc_properties",
1632                    "login_attempts",
1633                }
1634            )
1635            # Remove pymssql-specific parameters
1636            base_keys.discard("tds_version")
1637            base_keys.discard("conn_properties")
1638
1639        return base_keys
1640
1641    @property
1642    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1643        return engine_adapter.MSSQLEngineAdapter
1644
1645    @property
1646    def _connection_factory(self) -> t.Callable:
1647        if self.driver == "pymssql":
1648            import pymssql
1649
1650            return pymssql.connect
1651
1652        if self.driver == "mssql-python":
1653            # The `mssql-python` implementation is API-compatible with
1654            # with the `pyodbc` equivalent for documented parameters.
1655
1656            if not SUPPORTS_MSSQL_PYTHON_DRIVER:
1657                raise ConfigError("The `mssql-python` driver requires Python 3.10 or higher.")
1658
1659            import mssql_python
1660
1661            def connect_mssql_python(**kwargs: t.Any) -> t.Callable:
1662                # Extract parameters for connection string
1663                host = kwargs.pop("host")
1664                port = kwargs.pop("port", 1433)
1665                database = kwargs.pop("database", "")
1666                user = kwargs.pop("user", None)
1667                password = kwargs.pop("password", None)
1668                authentication = kwargs.pop("authentication", None)
1669                trust_server_certificate = kwargs.pop("trust_server_certificate", False)
1670                encrypt = kwargs.pop("encrypt", True)
1671                timeout = kwargs.pop("timeout", 0)
1672                login_timeout = kwargs.pop("login_timeout", 59)
1673                login_attempts = kwargs.pop("login_attempts", 1)
1674
1675                # Build connection string
1676                conn_str_parts = [
1677                    f"Server={host},{port}",
1678                ]
1679
1680                if database:
1681                    conn_str_parts.append(f"Database={database}")
1682
1683                # Add security options
1684                conn_str_parts.append(f"Encrypt={'yes' if encrypt else 'no'}")
1685                if trust_server_certificate:
1686                    conn_str_parts.append("TrustServerCertificate=yes")
1687
1688                # `Connection Timeout=` is not a valid option so we leverage `ConnectRetry*`.
1689                # See the following:
1690                # - https://github.com/microsoft/mssql-python/issues/339
1691                # - https://github.com/microsoft/mssql-python/wiki/Connection-to-SQL-Database
1692                # - https://github.com/microsoft/mssql-python/wiki/Connection#timeout
1693                conn_str_parts.append(f"ConnectRetryCount={login_attempts}")
1694                conn_str_parts.append(f"ConnectRetryInterval={min(int(login_timeout), 60)}")
1695
1696                # Standard SQL Server authentication
1697                if user:
1698                    conn_str_parts.append(f"UID={user}")
1699                if password:
1700                    conn_str_parts.append(f"PWD={password}")
1701                if authentication:
1702                    conn_str_parts.append(f"Authentication={authentication}")
1703
1704                # Add any additional ODBC properties from the odbc_properties dictionary
1705                if self.odbc_properties:
1706                    for key, value in self.odbc_properties.items():
1707                        # Skip properties that we've already set above
1708                        if key.lower() in (
1709                            "driver",
1710                            "server",
1711                            "database",
1712                            "uid",
1713                            "pwd",
1714                            "encrypt",
1715                            "trustservercertificate",
1716                            "connectretrycount",
1717                            "connectretryinterval",
1718                            "connection timeout",
1719                        ):
1720                            continue
1721
1722                        # Handle boolean values properly
1723                        if isinstance(value, bool):
1724                            conn_str_parts.append(f"{key}={'yes' if value else 'no'}")
1725                        else:
1726                            conn_str_parts.append(f"{key}={value}")
1727
1728                # Create the connection
1729                conn_str = ";".join(conn_str_parts)
1730
1731                conn = mssql_python.connect(
1732                    conn_str,
1733                    autocommit=kwargs.get("autocommit", False),
1734                    timeout=timeout,
1735                )
1736
1737                # TODO: Remove this output converter as DATETIMEOFFSET
1738                # should be handled natively by `mssql-python`.
1739                # see "https://github.com/microsoft/mssql-python/issues/213"
1740
1741                def handle_datetimeoffset_mssql_python(dto_value: t.Any) -> t.Any:
1742                    import struct
1743                    from datetime import datetime, timedelta, timezone
1744
1745                    # Unpack the DATETIMEOFFSET binary format:
1746                    # Format: <6hI2h = (year, month, day, hour, minute, second, nanoseconds, tz_hour_offset, tz_minute_offset)
1747                    tup = struct.unpack("<6hI2h", dto_value)
1748                    return datetime(
1749                        tup[0],
1750                        tup[1],
1751                        tup[2],
1752                        tup[3],
1753                        tup[4],
1754                        tup[5],
1755                        tup[6] // 1000,
1756                        timezone(timedelta(hours=tup[7], minutes=tup[8])),
1757                    )
1758
1759                conn.add_output_converter(-155, handle_datetimeoffset_mssql_python)
1760
1761                return t.cast(t.Any, conn)
1762
1763            return connect_mssql_python
1764
1765        if self.driver == "pyodbc":
1766
1767            def connect_pyodbc(**kwargs: t.Any) -> t.Callable:
1768                # Extract parameters for connection string
1769                host = kwargs.pop("host")
1770                port = kwargs.pop("port", 1433)
1771                database = kwargs.pop("database", "")
1772                user = kwargs.pop("user", None)
1773                password = kwargs.pop("password", None)
1774                driver_name = kwargs.pop("driver_name", "ODBC Driver 18 for SQL Server")
1775                trust_server_certificate = kwargs.pop("trust_server_certificate", False)
1776                encrypt = kwargs.pop("encrypt", True)
1777                login_timeout = kwargs.pop("login_timeout", 60)
1778
1779                # Build connection string
1780                conn_str_parts = [
1781                    f"DRIVER={{{driver_name}}}",
1782                    f"SERVER={host},{port}",
1783                ]
1784
1785                if database:
1786                    conn_str_parts.append(f"DATABASE={database}")
1787
1788                # Add security options
1789                conn_str_parts.append(f"Encrypt={'YES' if encrypt else 'NO'}")
1790                if trust_server_certificate:
1791                    conn_str_parts.append("TrustServerCertificate=YES")
1792
1793                conn_str_parts.append(f"Connection Timeout={login_timeout}")
1794
1795                # Standard SQL Server authentication
1796                if user:
1797                    conn_str_parts.append(f"UID={user}")
1798                if password:
1799                    conn_str_parts.append(f"PWD={password}")
1800
1801                # Add any additional ODBC properties from the odbc_properties dictionary
1802                if self.odbc_properties:
1803                    for key, value in self.odbc_properties.items():
1804                        # Skip properties that we've already set above
1805                        if key.lower() in (
1806                            "driver",
1807                            "server",
1808                            "database",
1809                            "uid",
1810                            "pwd",
1811                            "encrypt",
1812                            "trustservercertificate",
1813                            "connection timeout",
1814                        ):
1815                            continue
1816
1817                        # Handle boolean values properly
1818                        if isinstance(value, bool):
1819                            conn_str_parts.append(f"{key}={'YES' if value else 'NO'}")
1820                        else:
1821                            conn_str_parts.append(f"{key}={value}")
1822
1823                # Create the connection
1824                conn_str = ";".join(conn_str_parts)
1825
1826                import pyodbc
1827
1828                conn = pyodbc.connect(conn_str, autocommit=kwargs.get("autocommit", False))
1829
1830                # Set up output converters for MSSQL-specific data types
1831                # Handle SQL type -155 (DATETIMEOFFSET) which is not yet supported by pyodbc
1832                # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
1833                def handle_datetimeoffset_pyodbc(dto_value: t.Any) -> t.Any:
1834                    import struct
1835                    from datetime import datetime, timedelta, timezone
1836
1837                    # Unpack the DATETIMEOFFSET binary format:
1838                    # Format: <6hI2h = (year, month, day, hour, minute, second, nanoseconds, tz_hour_offset, tz_minute_offset)
1839                    tup = struct.unpack("<6hI2h", dto_value)
1840                    return datetime(
1841                        tup[0],
1842                        tup[1],
1843                        tup[2],
1844                        tup[3],
1845                        tup[4],
1846                        tup[5],
1847                        tup[6] // 1000,
1848                        timezone(timedelta(hours=tup[7], minutes=tup[8])),
1849                    )
1850
1851                conn.add_output_converter(-155, handle_datetimeoffset_pyodbc)
1852
1853                return t.cast(t.Any, conn)
1854
1855            return connect_pyodbc
1856
1857        raise ValueError(f"Unsupported driver: {self.driver}")
1858
1859    @property
1860    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1861        return {"catalog_support": CatalogSupport.REQUIRES_SET_CATALOG}

Helper class that provides a standard way to create an ABC using inheritance.

host: str
user: Optional[str]
password: Optional[str]
database: Optional[str]
timeout: Optional[int]
login_timeout: Optional[int]
login_attempts: Optional[int]
charset: Optional[str]
appname: Optional[str]
port: Optional[int]
conn_properties: Union[List[str], str, NoneType]
autocommit: Optional[bool]
tds_version: Optional[str]
driver: Literal['pymssql', 'pyodbc', 'mssql-python']
driver_name: Optional[str]
trust_server_certificate: Optional[bool]
encrypt: Optional[bool]
odbc_properties: Optional[Dict[str, Any]]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
type_: Literal['mssql']
DIALECT: ClassVar[Literal['tsql']] = 'tsql'
DISPLAY_NAME: ClassVar[Literal['MSSQL']] = 'MSSQL'
DISPLAY_ORDER: ClassVar[Literal[11]] = 11
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class AzureSQLConnectionConfig(MSSQLConnectionConfig):
1864class AzureSQLConnectionConfig(MSSQLConnectionConfig):
1865    type_: t.Literal["azuresql"] = Field(alias="type", default="azuresql")  # type: ignore
1866    DISPLAY_NAME: t.ClassVar[t.Literal["Azure SQL"]] = "Azure SQL"  # type: ignore
1867    DISPLAY_ORDER: t.ClassVar[t.Literal[10]] = 10  # type: ignore
1868
1869    @property
1870    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1871        return {"catalog_support": CatalogSupport.SINGLE_CATALOG_ONLY}

Helper class that provides a standard way to create an ABC using inheritance.

type_: Literal['azuresql']
DISPLAY_NAME: ClassVar[Literal['Azure SQL']] = 'Azure SQL'
DISPLAY_ORDER: ClassVar[Literal[10]] = 10
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
MSSQLConnectionConfig
host
user
password
database
timeout
login_timeout
login_attempts
charset
appname
port
conn_properties
autocommit
tds_version
driver
driver_name
trust_server_certificate
encrypt
odbc_properties
concurrent_tasks
register_comments
pre_ping
DIALECT
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class FabricConnectionConfig(MSSQLConnectionConfig):
1874class FabricConnectionConfig(MSSQLConnectionConfig):
1875    """
1876    Fabric Connection Configuration.
1877    Inherits most settings from MSSQLConnectionConfig and sets the type to 'fabric'.
1878    It is recommended to use the 'pyodbc' driver for Fabric.
1879    """
1880
1881    type_: t.Literal["fabric"] = Field(alias="type", default="fabric")  # type: ignore
1882    DIALECT: t.ClassVar[t.Literal["fabric"]] = "fabric"  # type: ignore
1883    DISPLAY_NAME: t.ClassVar[t.Literal["Fabric"]] = "Fabric"  # type: ignore
1884    DISPLAY_ORDER: t.ClassVar[t.Literal[17]] = 17  # type: ignore
1885    driver: t.Literal["pyodbc", "mssql-python"] = "pyodbc"
1886    workspace_id: str
1887    tenant_id: str
1888    autocommit: t.Optional[bool] = True
1889
1890    @property
1891    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1892        from sqlmesh.core.engine_adapter.fabric import FabricEngineAdapter
1893
1894        return FabricEngineAdapter
1895
1896    @property
1897    def _connection_factory(self) -> t.Callable:
1898        # Override to support catalog switching for Fabric
1899        base_factory = super()._connection_factory
1900
1901        def create_fabric_connection(
1902            target_catalog: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any
1903        ) -> t.Callable:
1904            kwargs["database"] = target_catalog or self.database
1905            return base_factory(*args, **kwargs)
1906
1907        return create_fabric_connection
1908
1909    @property
1910    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1911        return {
1912            "database": self.database,
1913            # more operations than not require a specific catalog to be already active
1914            # in particular, create/drop view, create/drop schema and querying information_schema
1915            "catalog_support": CatalogSupport.REQUIRES_SET_CATALOG,
1916            "workspace_id": self.workspace_id,
1917            "tenant_id": self.tenant_id,
1918            "user": self.user,
1919            "password": self.password,
1920        }

Fabric Connection Configuration. Inherits most settings from MSSQLConnectionConfig and sets the type to 'fabric'. It is recommended to use the 'pyodbc' driver for Fabric.

type_: Literal['fabric']
DIALECT: ClassVar[Literal['fabric']] = 'fabric'
DISPLAY_NAME: ClassVar[Literal['Fabric']] = 'Fabric'
DISPLAY_ORDER: ClassVar[Literal[17]] = 17
driver: Literal['pyodbc', 'mssql-python']
workspace_id: str
tenant_id: str
autocommit: Optional[bool]
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
MSSQLConnectionConfig
host
user
password
database
timeout
login_timeout
login_attempts
charset
appname
port
conn_properties
tds_version
driver_name
trust_server_certificate
encrypt
odbc_properties
concurrent_tasks
register_comments
pre_ping
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class SparkConnectionConfig(ConnectionConfig):
1923class SparkConnectionConfig(ConnectionConfig):
1924    """
1925    Vanilla Spark Connection Configuration. Use `DatabricksConnectionConfig` for Databricks.
1926    """
1927
1928    config_dir: t.Optional[str] = None
1929    catalog: t.Optional[str] = None
1930    config: t.Dict[str, t.Any] = {}
1931    wap_enabled: bool = False
1932
1933    concurrent_tasks: int = 4
1934    register_comments: bool = True
1935    pre_ping: t.Literal[False] = False
1936
1937    type_: t.Literal["spark"] = Field(alias="type", default="spark")
1938    DIALECT: t.ClassVar[t.Literal["spark"]] = "spark"
1939    DISPLAY_NAME: t.ClassVar[t.Literal["Spark"]] = "Spark"
1940    DISPLAY_ORDER: t.ClassVar[t.Literal[8]] = 8
1941
1942    _engine_import_validator = _get_engine_import_validator("pyspark", "spark")
1943
1944    @property
1945    def _connection_kwargs_keys(self) -> t.Set[str]:
1946        return {
1947            "catalog",
1948        }
1949
1950    @property
1951    def _engine_adapter(self) -> t.Type[EngineAdapter]:
1952        return engine_adapter.SparkEngineAdapter
1953
1954    @property
1955    def _connection_factory(self) -> t.Callable:
1956        from sqlmesh.engines.spark.db_api.spark_session import connection
1957
1958        return connection
1959
1960    @property
1961    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
1962        from pyspark.conf import SparkConf
1963        from pyspark.sql import SparkSession
1964
1965        spark_config = SparkConf()
1966        if self.config:
1967            for k, v in self.config.items():
1968                spark_config.set(k, v)
1969
1970        if self.config_dir:
1971            os.environ["SPARK_CONF_DIR"] = self.config_dir
1972        return {
1973            "spark": SparkSession.builder.config(conf=spark_config)
1974            .enableHiveSupport()
1975            .getOrCreate(),
1976        }
1977
1978    @property
1979    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
1980        return {"wap_enabled": self.wap_enabled}

Vanilla Spark Connection Configuration. Use DatabricksConnectionConfig for Databricks.

config_dir: Optional[str]
catalog: Optional[str]
config: Dict[str, Any]
wap_enabled: bool
concurrent_tasks: int
register_comments: bool
pre_ping: Literal[False]
type_: Literal['spark']
DIALECT: ClassVar[Literal['spark']] = 'spark'
DISPLAY_NAME: ClassVar[Literal['Spark']] = 'Spark'
DISPLAY_ORDER: ClassVar[Literal[8]] = 8
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class TrinoAuthenticationMethod(builtins.str, enum.Enum):
1983class TrinoAuthenticationMethod(str, Enum):
1984    NO_AUTH = "no-auth"
1985    BASIC = "basic"
1986    LDAP = "ldap"
1987    KERBEROS = "kerberos"
1988    JWT = "jwt"
1989    CERTIFICATE = "certificate"
1990    OAUTH = "oauth"
1991
1992    @property
1993    def is_no_auth(self) -> bool:
1994        return self == self.NO_AUTH
1995
1996    @property
1997    def is_basic(self) -> bool:
1998        return self == self.BASIC
1999
2000    @property
2001    def is_ldap(self) -> bool:
2002        return self == self.LDAP
2003
2004    @property
2005    def is_kerberos(self) -> bool:
2006        return self == self.KERBEROS
2007
2008    @property
2009    def is_jwt(self) -> bool:
2010        return self == self.JWT
2011
2012    @property
2013    def is_certificate(self) -> bool:
2014        return self == self.CERTIFICATE
2015
2016    @property
2017    def is_oauth(self) -> bool:
2018        return self == self.OAUTH

An enumeration.

NO_AUTH = <TrinoAuthenticationMethod.NO_AUTH: 'no-auth'>
BASIC = <TrinoAuthenticationMethod.BASIC: 'basic'>
KERBEROS = <TrinoAuthenticationMethod.KERBEROS: 'kerberos'>
CERTIFICATE = <TrinoAuthenticationMethod.CERTIFICATE: 'certificate'>
OAUTH = <TrinoAuthenticationMethod.OAUTH: 'oauth'>
is_no_auth: bool
1992    @property
1993    def is_no_auth(self) -> bool:
1994        return self == self.NO_AUTH
is_basic: bool
1996    @property
1997    def is_basic(self) -> bool:
1998        return self == self.BASIC
is_ldap: bool
2000    @property
2001    def is_ldap(self) -> bool:
2002        return self == self.LDAP
is_kerberos: bool
2004    @property
2005    def is_kerberos(self) -> bool:
2006        return self == self.KERBEROS
is_jwt: bool
2008    @property
2009    def is_jwt(self) -> bool:
2010        return self == self.JWT
is_certificate: bool
2012    @property
2013    def is_certificate(self) -> bool:
2014        return self == self.CERTIFICATE
is_oauth: bool
2016    @property
2017    def is_oauth(self) -> bool:
2018        return self == self.OAUTH
Inherited Members
enum.Enum
name
value
builtins.str
encode
replace
split
rsplit
join
capitalize
casefold
title
center
count
expandtabs
find
partition
index
ljust
lower
lstrip
rfind
rindex
rjust
rstrip
rpartition
splitlines
strip
swapcase
translate
upper
startswith
endswith
removeprefix
removesuffix
isascii
islower
isupper
istitle
isspace
isdecimal
isdigit
isnumeric
isalpha
isalnum
isidentifier
isprintable
zfill
format
format_map
maketrans
class TrinoConnectionConfig(ConnectionConfig):
2021class TrinoConnectionConfig(ConnectionConfig):
2022    method: TrinoAuthenticationMethod = TrinoAuthenticationMethod.NO_AUTH
2023    host: str
2024    user: str
2025    catalog: str
2026    port: t.Optional[int] = None
2027    http_scheme: t.Literal["http", "https"] = "https"
2028    # General Optional
2029    roles: t.Optional[t.Dict[str, str]] = None
2030    http_headers: t.Optional[t.Dict[str, str]] = None
2031    session_properties: t.Optional[t.Dict[str, str]] = None
2032    retries: int = 3
2033    timezone: t.Optional[str] = None
2034    # Basic/LDAP
2035    password: t.Optional[str] = None
2036    verify: t.Optional[bool] = None  # disable SSL verification (ignored if `cert` is provided)
2037    # LDAP
2038    impersonation_user: t.Optional[str] = None
2039    # Kerberos
2040    keytab: t.Optional[str] = None
2041    krb5_config: t.Optional[str] = None
2042    principal: t.Optional[str] = None
2043    service_name: str = "trino"
2044    hostname_override: t.Optional[str] = None
2045    mutual_authentication: bool = False
2046    force_preemptive: bool = False
2047    sanitize_mutual_error_response: bool = True
2048    delegate: bool = False
2049    # JWT
2050    jwt_token: t.Optional[str] = None
2051    # Certificate
2052    client_certificate: t.Optional[str] = None
2053    client_private_key: t.Optional[str] = None
2054    cert: t.Optional[str] = None
2055    source: str = "sqlmesh"
2056
2057    # SQLMesh options
2058    schema_location_mapping: t.Optional[dict[re.Pattern, str]] = None
2059    timestamp_mapping: t.Optional[dict[exp.DataType, exp.DataType]] = None
2060    concurrent_tasks: int = 4
2061    register_comments: bool = True
2062    pre_ping: t.Literal[False] = False
2063
2064    type_: t.Literal["trino"] = Field(alias="type", default="trino")
2065    DIALECT: t.ClassVar[t.Literal["trino"]] = "trino"
2066    DISPLAY_NAME: t.ClassVar[t.Literal["Trino"]] = "Trino"
2067    DISPLAY_ORDER: t.ClassVar[t.Literal[9]] = 9
2068
2069    _engine_import_validator = _get_engine_import_validator("trino", "trino")
2070
2071    @field_validator("schema_location_mapping", mode="before")
2072    @classmethod
2073    def _validate_regex_keys(
2074        cls, value: t.Dict[str | re.Pattern, str]
2075    ) -> t.Dict[re.Pattern, t.Any]:
2076        compiled = compile_regex_mapping(value)
2077        for replacement in compiled.values():
2078            if "@{schema_name}" not in replacement:
2079                raise ConfigError(
2080                    "schema_location_mapping needs to include the '@{schema_name}' placeholder in the value so SQLMesh knows where to substitute the schema name"
2081                )
2082        return compiled
2083
2084    @field_validator("timestamp_mapping", mode="before")
2085    @classmethod
2086    def _validate_timestamp_mapping(
2087        cls, value: t.Optional[dict[str, str]]
2088    ) -> t.Optional[dict[exp.DataType, exp.DataType]]:
2089        if value is None:
2090            return value
2091
2092        result: dict[exp.DataType, exp.DataType] = {}
2093        for source_type, target_type in value.items():
2094            try:
2095                source_datatype = exp.DataType.build(source_type)
2096            except ParseError:
2097                raise ConfigError(
2098                    f"Invalid SQL type string in timestamp_mapping: "
2099                    f"'{source_type}' is not a valid SQL data type."
2100                )
2101            try:
2102                target_datatype = exp.DataType.build(target_type)
2103            except ParseError:
2104                raise ConfigError(
2105                    f"Invalid SQL type string in timestamp_mapping: "
2106                    f"'{target_type}' is not a valid SQL data type."
2107                )
2108            result[source_datatype] = target_datatype
2109
2110        return result
2111
2112    @model_validator(mode="after")
2113    def _root_validator(self) -> Self:
2114        port = self.port
2115        if self.http_scheme == "http" and not self.method.is_no_auth and not self.method.is_basic:
2116            raise ConfigError("HTTP scheme can only be used with no-auth or basic method")
2117
2118        if port is None:
2119            self.port = 80 if self.http_scheme == "http" else 443
2120
2121        if (self.method.is_ldap or self.method.is_basic) and (not self.password or not self.user):
2122            raise ConfigError(
2123                f"Username and Password must be provided if using {self.method.value} authentication"
2124            )
2125
2126        if self.method.is_kerberos and (
2127            not self.principal or not self.keytab or not self.krb5_config
2128        ):
2129            raise ConfigError(
2130                "Kerberos requires the following fields: principal, keytab, and krb5_config"
2131            )
2132
2133        if self.method.is_jwt and not self.jwt_token:
2134            raise ConfigError("JWT requires `jwt_token` to be set")
2135
2136        if self.method.is_certificate and (
2137            not self.cert or not self.client_certificate or not self.client_private_key
2138        ):
2139            raise ConfigError(
2140                "Certificate requires the following fields: cert, client_certificate, and client_private_key"
2141            )
2142
2143        return self
2144
2145    @property
2146    def _connection_kwargs_keys(self) -> t.Set[str]:
2147        kwargs = {
2148            "host",
2149            "port",
2150            "catalog",
2151            "roles",
2152            "source",
2153            "http_scheme",
2154            "http_headers",
2155            "session_properties",
2156            "timezone",
2157        }
2158        return kwargs
2159
2160    @property
2161    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2162        return engine_adapter.TrinoEngineAdapter
2163
2164    @property
2165    def _connection_factory(self) -> t.Callable:
2166        from trino.dbapi import connect
2167
2168        return connect
2169
2170    @property
2171    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
2172        from trino.auth import (
2173            BasicAuthentication,
2174            CertificateAuthentication,
2175            JWTAuthentication,
2176            KerberosAuthentication,
2177            OAuth2Authentication,
2178        )
2179
2180        auth: t.Optional[
2181            t.Union[
2182                BasicAuthentication,
2183                KerberosAuthentication,
2184                OAuth2Authentication,
2185                JWTAuthentication,
2186                CertificateAuthentication,
2187            ]
2188        ] = None
2189        if self.method.is_basic or self.method.is_ldap:
2190            assert self.password is not None  # for mypy since validator already checks this
2191            auth = BasicAuthentication(self.user, self.password)
2192        elif self.method.is_kerberos:
2193            if self.keytab:
2194                os.environ["KRB5_CLIENT_KTNAME"] = self.keytab
2195            auth = KerberosAuthentication(
2196                config=self.krb5_config,
2197                service_name=self.service_name,
2198                principal=self.principal,
2199                mutual_authentication=self.mutual_authentication,
2200                ca_bundle=self.cert,
2201                force_preemptive=self.force_preemptive,
2202                hostname_override=self.hostname_override,
2203                sanitize_mutual_error_response=self.sanitize_mutual_error_response,
2204                delegate=self.delegate,
2205            )
2206        elif self.method.is_oauth:
2207            auth = OAuth2Authentication()
2208        elif self.method.is_jwt:
2209            assert self.jwt_token is not None
2210            auth = JWTAuthentication(self.jwt_token)
2211        elif self.method.is_certificate:
2212            assert self.client_certificate is not None
2213            assert self.client_private_key is not None
2214            auth = CertificateAuthentication(self.client_certificate, self.client_private_key)
2215
2216        return {
2217            "auth": auth,
2218            "user": self.impersonation_user or self.user,
2219            "max_attempts": self.retries,
2220            "verify": self.cert if self.cert is not None else self.verify,
2221            "source": self.source,
2222        }
2223
2224    @property
2225    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2226        return {
2227            "schema_location_mapping": self.schema_location_mapping,
2228            "timestamp_mapping": self.timestamp_mapping,
2229        }

Helper class that provides a standard way to create an ABC using inheritance.

host: str
user: str
catalog: str
port: Optional[int]
http_scheme: Literal['http', 'https']
roles: Optional[Dict[str, str]]
http_headers: Optional[Dict[str, str]]
session_properties: Optional[Dict[str, str]]
retries: int
timezone: Optional[str]
password: Optional[str]
verify: Optional[bool]
impersonation_user: Optional[str]
keytab: Optional[str]
krb5_config: Optional[str]
principal: Optional[str]
service_name: str
hostname_override: Optional[str]
mutual_authentication: bool
force_preemptive: bool
sanitize_mutual_error_response: bool
delegate: bool
jwt_token: Optional[str]
client_certificate: Optional[str]
client_private_key: Optional[str]
cert: Optional[str]
source: str
schema_location_mapping: Optional[dict[re.Pattern, str]]
timestamp_mapping: Optional[dict[sqlglot.expressions.datatypes.DataType, sqlglot.expressions.datatypes.DataType]]
concurrent_tasks: int
register_comments: bool
pre_ping: Literal[False]
type_: Literal['trino']
DIALECT: ClassVar[Literal['trino']] = 'trino'
DISPLAY_NAME: ClassVar[Literal['Trino']] = 'Trino'
DISPLAY_ORDER: ClassVar[Literal[9]] = 9
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class ClickhouseConnectionConfig(ConnectionConfig):
2232class ClickhouseConnectionConfig(ConnectionConfig):
2233    """
2234    Clickhouse Connection Configuration.
2235
2236    Property reference: https://clickhouse.com/docs/en/integrations/python#client-initialization
2237    """
2238
2239    host: str
2240    username: str
2241    password: t.Optional[str] = None
2242    port: t.Optional[int] = None
2243    cluster: t.Optional[str] = None
2244    virtual_catalog: t.Optional[str] = None
2245    connect_timeout: int = 10
2246    send_receive_timeout: int = 300
2247    query_limit: int = 0
2248    use_compression: bool = True
2249    compression_method: t.Optional[str] = None
2250    connection_settings: t.Optional[t.Dict[str, t.Any]] = None
2251    http_proxy: t.Optional[str] = None
2252    # HTTPS/TLS settings
2253    verify: bool = True
2254    ca_cert: t.Optional[str] = None
2255    client_cert: t.Optional[str] = None
2256    client_cert_key: t.Optional[str] = None
2257    https_proxy: t.Optional[str] = None
2258    server_host_name: t.Optional[str] = None
2259    tls_mode: t.Optional[str] = None
2260    secure: bool = False
2261
2262    concurrent_tasks: int = 1
2263    register_comments: bool = True
2264    pre_ping: bool = False
2265
2266    # This object expects options from urllib3 and also from clickhouse-connect
2267    # See:
2268    # * https://urllib3.readthedocs.io/en/stable/advanced-usage.html
2269    # * https://clickhouse.com/docs/en/integrations/python#customizing-the-http-connection-pool
2270    connection_pool_options: t.Optional[t.Dict[str, t.Any]] = None
2271
2272    type_: t.Literal["clickhouse"] = Field(alias="type", default="clickhouse")
2273    DIALECT: t.ClassVar[t.Literal["clickhouse"]] = "clickhouse"
2274    DISPLAY_NAME: t.ClassVar[t.Literal["ClickHouse"]] = "ClickHouse"
2275    DISPLAY_ORDER: t.ClassVar[t.Literal[6]] = 6
2276
2277    _engine_import_validator = _get_engine_import_validator("clickhouse_connect", "clickhouse")
2278
2279    @field_validator("virtual_catalog")
2280    def validate_virtual_catalog(cls, v: t.Optional[str]) -> t.Optional[str]:
2281        if v is not None and not v.strip():
2282            raise ConfigError(
2283                "virtual_catalog cannot be an empty string. "
2284                "Omit the field to use the default synthetic prefix (__<gateway_name>__)."
2285            )
2286        if v is not None and "." in v:
2287            raise ConfigError(
2288                f"virtual_catalog must be a single identifier with no dots (got: {v!r})"
2289            )
2290        return v
2291
2292    @property
2293    def _connection_kwargs_keys(self) -> t.Set[str]:
2294        kwargs = {
2295            "host",
2296            "username",
2297            "port",
2298            "password",
2299            "connect_timeout",
2300            "send_receive_timeout",
2301            "query_limit",
2302            "http_proxy",
2303            "verify",
2304            "ca_cert",
2305            "client_cert",
2306            "client_cert_key",
2307            "https_proxy",
2308            "server_host_name",
2309            "tls_mode",
2310            "secure",
2311        }
2312        return kwargs
2313
2314    @property
2315    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2316        return engine_adapter.ClickhouseEngineAdapter
2317
2318    @property
2319    def _connection_factory(self) -> t.Callable:
2320        from functools import partial
2321
2322        from clickhouse_connect.dbapi import connect  # type: ignore
2323        from clickhouse_connect.driver import httputil  # type: ignore
2324
2325        pool_manager_options: t.Dict[str, t.Any] = dict(
2326            # Match the maxsize to the number of concurrent tasks
2327            maxsize=self.concurrent_tasks,
2328            # Block if there are no free connections
2329            block=True,
2330            verify=self.verify,
2331            ca_cert=self.ca_cert,
2332            client_cert=self.client_cert,
2333            client_cert_key=self.client_cert_key,
2334            https_proxy=self.https_proxy,
2335        )
2336        # this doesn't happen automatically because we always supply our own pool manager to the connection
2337        # https://github.com/ClickHouse/clickhouse-connect/blob/3a7f4b04cad29c7c2536661b831fb744248e2ec0/clickhouse_connect/driver/httpclient.py#L109
2338        if self.server_host_name:
2339            pool_manager_options["server_hostname"] = self.server_host_name
2340            if self.verify:
2341                pool_manager_options["assert_hostname"] = self.server_host_name
2342        if self.connection_pool_options:
2343            pool_manager_options.update(self.connection_pool_options)
2344        pool_mgr = httputil.get_pool_manager(**pool_manager_options)
2345
2346        return partial(connect, pool_mgr=pool_mgr)
2347
2348    @property
2349    def cloud_mode(self) -> bool:
2350        return "clickhouse.cloud" in self.host
2351
2352    @property
2353    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2354        return {
2355            "cluster": self.cluster,
2356            "cloud_mode": self.cloud_mode,
2357            "virtual_catalog": self.virtual_catalog,
2358        }
2359
2360    @property
2361    def _static_connection_kwargs(self) -> t.Dict[str, t.Any]:
2362        from sqlmesh import __version__
2363
2364        # False = no compression
2365        # True = Clickhouse default compression method
2366        # string = specific compression method
2367        compress: bool | str = self.use_compression
2368        if compress and self.compression_method:
2369            compress = self.compression_method
2370
2371        # Clickhouse system settings passed to connection
2372        # https://clickhouse.com/docs/en/operations/settings/settings
2373        # - below are set to align with dbt-clickhouse
2374        # - https://github.com/ClickHouse/dbt-clickhouse/blob/44d26308ea6a3c8ead25c280164aa88191f05f47/dbt/adapters/clickhouse/dbclient.py#L77
2375        settings = self.connection_settings or {}
2376        #  mutations_sync = 2: "The query waits for all mutations [ALTER statements] to complete on all replicas (if they exist)"
2377        settings["mutations_sync"] = "2"
2378        #  insert_distributed_sync = 1: "INSERT operation succeeds only after all the data is saved on all shards"
2379        settings["insert_distributed_sync"] = "1"
2380        if self.cluster or self.cloud_mode:
2381            # database_replicated_enforce_synchronous_settings = 1:
2382            #   - "Enforces synchronous waiting for some queries"
2383            #   - https://github.com/ClickHouse/ClickHouse/blob/ccaa8d03a9351efc16625340268b9caffa8a22ba/src/Core/Settings.h#L709
2384            settings["database_replicated_enforce_synchronous_settings"] = "1"
2385            # insert_quorum = auto:
2386            #   - "INSERT succeeds only when ClickHouse manages to correctly write data to the insert_quorum of replicas during
2387            #       the insert_quorum_timeout"
2388            #   - "use majority number (number_of_replicas / 2 + 1) as quorum number"
2389            settings["insert_quorum"] = "auto"
2390
2391        return {
2392            "compress": compress,
2393            "client_name": f"SQLMesh/{__version__}",
2394            **settings,
2395        }

Clickhouse Connection Configuration.

Property reference: https://clickhouse.com/docs/en/integrations/python#client-initialization

host: str
username: str
password: Optional[str]
port: Optional[int]
cluster: Optional[str]
virtual_catalog: Optional[str]
connect_timeout: int
send_receive_timeout: int
query_limit: int
use_compression: bool
compression_method: Optional[str]
connection_settings: Optional[Dict[str, Any]]
http_proxy: Optional[str]
verify: bool
ca_cert: Optional[str]
client_cert: Optional[str]
client_cert_key: Optional[str]
https_proxy: Optional[str]
server_host_name: Optional[str]
tls_mode: Optional[str]
secure: bool
concurrent_tasks: int
register_comments: bool
pre_ping: bool
connection_pool_options: Optional[Dict[str, Any]]
type_: Literal['clickhouse']
DIALECT: ClassVar[Literal['clickhouse']] = 'clickhouse'
DISPLAY_NAME: ClassVar[Literal['ClickHouse']] = 'ClickHouse'
DISPLAY_ORDER: ClassVar[Literal[6]] = 6
@field_validator('virtual_catalog')
def validate_virtual_catalog(cls, v: Optional[str]) -> Optional[str]:
2279    @field_validator("virtual_catalog")
2280    def validate_virtual_catalog(cls, v: t.Optional[str]) -> t.Optional[str]:
2281        if v is not None and not v.strip():
2282            raise ConfigError(
2283                "virtual_catalog cannot be an empty string. "
2284                "Omit the field to use the default synthetic prefix (__<gateway_name>__)."
2285            )
2286        if v is not None and "." in v:
2287            raise ConfigError(
2288                f"virtual_catalog must be a single identifier with no dots (got: {v!r})"
2289            )
2290        return v
cloud_mode: bool
2348    @property
2349    def cloud_mode(self) -> bool:
2350        return "clickhouse.cloud" in self.host
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class AthenaConnectionConfig(ConnectionConfig):
2398class AthenaConnectionConfig(ConnectionConfig):
2399    # PyAthena connection options
2400    aws_access_key_id: t.Optional[str] = None
2401    aws_secret_access_key: t.Optional[str] = None
2402    role_arn: t.Optional[str] = None
2403    role_session_name: t.Optional[str] = None
2404    region_name: t.Optional[str] = None
2405    work_group: t.Optional[str] = None
2406    s3_staging_dir: t.Optional[str] = None
2407    schema_name: t.Optional[str] = None
2408    catalog_name: t.Optional[str] = None
2409
2410    # SQLMesh options
2411    s3_warehouse_location: t.Optional[str] = None
2412    concurrent_tasks: int = 4
2413    register_comments: t.Literal[False] = (
2414        False  # because Athena doesnt support comments in most cases
2415    )
2416    pre_ping: t.Literal[False] = False
2417
2418    type_: t.Literal["athena"] = Field(alias="type", default="athena")
2419    DIALECT: t.ClassVar[t.Literal["athena"]] = "athena"
2420    DISPLAY_NAME: t.ClassVar[t.Literal["Athena"]] = "Athena"
2421    DISPLAY_ORDER: t.ClassVar[t.Literal[15]] = 15
2422
2423    _engine_import_validator = _get_engine_import_validator("pyathena", "athena")
2424
2425    @model_validator(mode="after")
2426    def _root_validator(self) -> Self:
2427        work_group = self.work_group
2428        s3_staging_dir = self.s3_staging_dir
2429        s3_warehouse_location = self.s3_warehouse_location
2430
2431        if not work_group and not s3_staging_dir:
2432            raise ConfigError("At least one of work_group or s3_staging_dir must be set")
2433
2434        if s3_staging_dir:
2435            self.s3_staging_dir = validate_s3_uri(s3_staging_dir, base=True, error_type=ConfigError)
2436
2437        if s3_warehouse_location:
2438            self.s3_warehouse_location = validate_s3_uri(
2439                s3_warehouse_location, base=True, error_type=ConfigError
2440            )
2441
2442        return self
2443
2444    @property
2445    def _connection_kwargs_keys(self) -> t.Set[str]:
2446        return {
2447            "aws_access_key_id",
2448            "aws_secret_access_key",
2449            "role_arn",
2450            "role_session_name",
2451            "region_name",
2452            "work_group",
2453            "s3_staging_dir",
2454            "schema_name",
2455            "catalog_name",
2456        }
2457
2458    @property
2459    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2460        return engine_adapter.AthenaEngineAdapter
2461
2462    @property
2463    def _extra_engine_config(self) -> t.Dict[str, t.Any]:
2464        return {"s3_warehouse_location": self.s3_warehouse_location}
2465
2466    @property
2467    def _connection_factory(self) -> t.Callable:
2468        from pyathena import connect  # type: ignore
2469
2470        return connect
2471
2472    def get_catalog(self) -> t.Optional[str]:
2473        return self.catalog_name

Helper class that provides a standard way to create an ABC using inheritance.

aws_access_key_id: Optional[str]
aws_secret_access_key: Optional[str]
role_arn: Optional[str]
role_session_name: Optional[str]
region_name: Optional[str]
work_group: Optional[str]
s3_staging_dir: Optional[str]
schema_name: Optional[str]
catalog_name: Optional[str]
s3_warehouse_location: Optional[str]
concurrent_tasks: int
register_comments: Literal[False]
pre_ping: Literal[False]
type_: Literal['athena']
DIALECT: ClassVar[Literal['athena']] = 'athena'
DISPLAY_NAME: ClassVar[Literal['Athena']] = 'Athena'
DISPLAY_ORDER: ClassVar[Literal[15]] = 15
def get_catalog(self) -> Optional[str]:
2472    def get_catalog(self) -> t.Optional[str]:
2473        return self.catalog_name

The catalog for this connection

model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class RisingwaveConnectionConfig(ConnectionConfig):
2476class RisingwaveConnectionConfig(ConnectionConfig):
2477    host: str
2478    user: str
2479    password: t.Optional[str] = None
2480    port: int
2481    database: str
2482    role: t.Optional[str] = None
2483    sslmode: t.Optional[str] = None
2484
2485    concurrent_tasks: int = 4
2486    register_comments: bool = True
2487    pre_ping: bool = True
2488
2489    type_: t.Literal["risingwave"] = Field(alias="type", default="risingwave")
2490    DIALECT: t.ClassVar[t.Literal["risingwave"]] = "risingwave"
2491    DISPLAY_NAME: t.ClassVar[t.Literal["RisingWave"]] = "RisingWave"
2492    DISPLAY_ORDER: t.ClassVar[t.Literal[16]] = 16
2493
2494    _engine_import_validator = _get_engine_import_validator("psycopg2", "risingwave")
2495
2496    @property
2497    def _connection_kwargs_keys(self) -> t.Set[str]:
2498        return {
2499            "host",
2500            "user",
2501            "password",
2502            "port",
2503            "database",
2504            "role",
2505            "sslmode",
2506        }
2507
2508    @property
2509    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2510        return engine_adapter.RisingwaveEngineAdapter
2511
2512    @property
2513    def _connection_factory(self) -> t.Callable:
2514        from psycopg2 import connect
2515
2516        return connect
2517
2518    @property
2519    def _cursor_init(self) -> t.Optional[t.Callable[[t.Any], None]]:
2520        def init(cursor: t.Any) -> None:
2521            sql = "SET RW_IMPLICIT_FLUSH TO true;"
2522            cursor.execute(sql)
2523
2524        return init

Helper class that provides a standard way to create an ABC using inheritance.

host: str
user: str
password: Optional[str]
port: int
database: str
role: Optional[str]
sslmode: Optional[str]
concurrent_tasks: int
register_comments: bool
pre_ping: bool
type_: Literal['risingwave']
DIALECT: ClassVar[Literal['risingwave']] = 'risingwave'
DISPLAY_NAME: ClassVar[Literal['RisingWave']] = 'RisingWave'
DISPLAY_ORDER: ClassVar[Literal[16]] = 16
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
class StarRocksConnectionConfig(ConnectionConfig):
2527class StarRocksConnectionConfig(ConnectionConfig):
2528    """Configuration for the StarRocks connection.
2529
2530    StarRocks uses MySQL network protocol and is compatible with MySQL ecosystem tools,
2531    JDBC/ODBC drivers, and various visualization tools.
2532
2533    Args:
2534        host: The hostname of the StarRocks FE (Frontend) node.
2535        user: The StarRocks username.
2536        password: The StarRocks password.
2537        port: The port number of the StarRocks FE node. Default is 9030.
2538        database: The optional database name.
2539        charset: The optional character set.  TODO: may be not supported yet.
2540        collation: The optional collation.  TODO: may be not supported yet.
2541        ssl_disabled: Whether to disable SSL connection.  TODO: need to check it.
2542        concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
2543        register_comments: Whether or not to register model comments with the SQL engine.
2544        local_infile: Whether or not to allow local file access.
2545        pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
2546    """
2547
2548    host: str
2549    user: str
2550    password: str
2551    port: t.Optional[int] = 9030
2552    database: t.Optional[str] = None
2553    charset: t.Optional[str] = None
2554    collation: t.Optional[str] = None
2555    ssl_disabled: t.Optional[bool] = None
2556
2557    concurrent_tasks: int = 4
2558    register_comments: bool = True
2559    local_infile: bool = False
2560    pre_ping: bool = True
2561
2562    type_: t.Literal["starrocks"] = Field(alias="type", default="starrocks")
2563    DIALECT: t.ClassVar[t.Literal["starrocks"]] = "starrocks"
2564    DISPLAY_NAME: t.ClassVar[t.Literal["StarRocks"]] = "StarRocks"
2565    DISPLAY_ORDER: t.ClassVar[t.Literal[18]] = 18
2566
2567    _engine_import_validator = _get_engine_import_validator("pymysql", "starrocks")
2568
2569    @property
2570    def _connection_kwargs_keys(self) -> t.Set[str]:
2571        connection_keys = {
2572            "host",
2573            "user",
2574            "password",
2575        }
2576        if self.port is not None:
2577            connection_keys.add("port")
2578        if self.database is not None:
2579            connection_keys.add("database")
2580        if self.charset is not None:
2581            connection_keys.add("charset")
2582        if self.collation is not None:
2583            connection_keys.add("collation")
2584        if self.ssl_disabled is not None:
2585            connection_keys.add("ssl_disabled")
2586        if self.local_infile is not None:
2587            connection_keys.add("local_infile")
2588        return connection_keys
2589
2590    @property
2591    def _engine_adapter(self) -> t.Type[EngineAdapter]:
2592        return engine_adapter.StarRocksEngineAdapter
2593
2594    @property
2595    def _connection_factory(self) -> t.Callable:
2596        from pymysql import connect
2597
2598        return connect

Configuration for the StarRocks connection.

StarRocks uses MySQL network protocol and is compatible with MySQL ecosystem tools, JDBC/ODBC drivers, and various visualization tools.

Arguments:
  • host: The hostname of the StarRocks FE (Frontend) node.
  • user: The StarRocks username.
  • password: The StarRocks password.
  • port: The port number of the StarRocks FE node. Default is 9030.
  • database: The optional database name.
  • charset: The optional character set. TODO: may be not supported yet.
  • collation: The optional collation. TODO: may be not supported yet.
  • ssl_disabled: Whether to disable SSL connection. TODO: need to check it.
  • concurrent_tasks: The maximum number of tasks that can use this connection concurrently.
  • register_comments: Whether or not to register model comments with the SQL engine.
  • local_infile: Whether or not to allow local file access.
  • pre_ping: Whether or not to pre-ping the connection before starting a new transaction to ensure it is still alive.
host: str
user: str
password: str
port: Optional[int]
database: Optional[str]
charset: Optional[str]
collation: Optional[str]
ssl_disabled: Optional[bool]
concurrent_tasks: int
register_comments: bool
local_infile: bool
pre_ping: bool
type_: Literal['starrocks']
DIALECT: ClassVar[Literal['starrocks']] = 'starrocks'
DISPLAY_NAME: ClassVar[Literal['StarRocks']] = 'StarRocks'
DISPLAY_ORDER: ClassVar[Literal[18]] = 18
model_config = {'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
pydantic.main.BaseModel
BaseModel
model_fields
model_computed_fields
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
parse_file
from_orm
construct
schema
schema_json
validate
update_forward_refs
ConnectionConfig
pretty_sql
schema_differ_overrides
catalog_type_overrides
shared_connection
is_forbidden_for_state_sync
connection_validator
create_engine_adapter
get_catalog
sqlmesh.core.config.base.BaseConfig
update_with
sqlmesh.utils.pydantic.PydanticModel
dict
json
copy
fields_set
parse_obj
parse_raw
missing_required_fields
extra_fields
all_fields
all_field_infos
required_fields
CONNECTION_CONFIG_TO_TYPE = {'athena': <class 'AthenaConnectionConfig'>, 'azuresql': <class 'AzureSQLConnectionConfig'>, 'bigquery': <class 'BigQueryConnectionConfig'>, 'clickhouse': <class 'ClickhouseConnectionConfig'>, 'databricks': <class 'DatabricksConnectionConfig'>, 'duckdb': <class 'DuckDBConnectionConfig'>, 'fabric': <class 'FabricConnectionConfig'>, 'gcp_postgres': <class 'GCPPostgresConnectionConfig'>, 'mssql': <class 'MSSQLConnectionConfig'>, 'motherduck': <class 'MotherDuckConnectionConfig'>, 'mysql': <class 'MySQLConnectionConfig'>, 'postgres': <class 'PostgresConnectionConfig'>, 'redshift': <class 'RedshiftConnectionConfig'>, 'risingwave': <class 'RisingwaveConnectionConfig'>, 'snowflake': <class 'SnowflakeConnectionConfig'>, 'spark': <class 'SparkConnectionConfig'>, 'starrocks': <class 'StarRocksConnectionConfig'>, 'trino': <class 'TrinoConnectionConfig'>}
DIALECT_TO_TYPE = {'athena': 'athena', 'azuresql': 'tsql', 'bigquery': 'bigquery', 'clickhouse': 'clickhouse', 'databricks': 'databricks', 'duckdb': 'duckdb', 'fabric': 'fabric', 'gcp_postgres': 'postgres', 'mssql': 'tsql', 'motherduck': 'duckdb', 'mysql': 'mysql', 'postgres': 'postgres', 'redshift': 'redshift', 'risingwave': 'risingwave', 'snowflake': 'snowflake', 'spark': 'spark', 'starrocks': 'starrocks', 'trino': 'trino'}
INIT_DISPLAY_INFO_TO_TYPE = {'athena': (15, 'Athena'), 'azuresql': (10, 'Azure SQL'), 'bigquery': (4, 'BigQuery'), 'clickhouse': (6, 'ClickHouse'), 'databricks': (3, 'Databricks'), 'duckdb': (1, 'DuckDB'), 'fabric': (17, 'Fabric'), 'gcp_postgres': (13, 'GCP Postgres'), 'mssql': (11, 'MSSQL'), 'motherduck': (5, 'MotherDuck'), 'mysql': (14, 'MySQL'), 'postgres': (12, 'Postgres'), 'redshift': (7, 'Redshift'), 'risingwave': (16, 'RisingWave'), 'snowflake': (2, 'Snowflake'), 'spark': (8, 'Spark'), 'starrocks': (18, 'StarRocks'), 'trino': (9, 'Trino')}
def parse_connection_config(v: Dict[str, Any]) -> ConnectionConfig:
2626def parse_connection_config(v: t.Dict[str, t.Any]) -> ConnectionConfig:
2627    if "type" not in v:
2628        raise ConfigError("Missing connection type.")
2629
2630    connection_type = v["type"]
2631    if connection_type not in CONNECTION_CONFIG_TO_TYPE:
2632        raise ConfigError(f"Unknown connection type '{connection_type}'.")
2633
2634    return CONNECTION_CONFIG_TO_TYPE[connection_type](**v)
def connection_config_validator( cls: Type, v: Union[ConnectionConfig, Dict[str, Any], NoneType]) -> ConnectionConfig | None:
2637def _connection_config_validator(
2638    cls: t.Type, v: ConnectionConfig | t.Dict[str, t.Any] | None
2639) -> ConnectionConfig | None:
2640    if v is None or isinstance(v, ConnectionConfig):
2641        return v
2642
2643    check_config_and_vars_msg = "\n\nVerify your config.yaml and environment variables."
2644
2645    try:
2646        return parse_connection_config(v)
2647    except pydantic.ValidationError as e:
2648        raise ConfigError(
2649            validation_error_message(e, f"Invalid '{v['type']}' connection config:")
2650            + check_config_and_vars_msg
2651        )
2652    except ConfigError as e:
2653        raise ConfigError(str(e) + check_config_and_vars_msg)

Wrap a classmethod, staticmethod, property or unbound function and act as a descriptor that allows us to detect decorated items from the class' attributes.

This class' __get__ returns the wrapped item's __get__ result, which makes it transparent for classmethods and staticmethods.

Attributes:
  • wrapped: The decorator that has to be wrapped.
  • decorator_info: The decorator info.
  • shim: A wrapper function to wrap V1 style function.
SerializableConnectionConfig = typing.Annotated[ConnectionConfig, SerializeAsAny()]