Edit on GitHub

sqlmesh.core.schema_loader

  1from __future__ import annotations
  2
  3import typing as t
  4from concurrent.futures import ThreadPoolExecutor
  5from pathlib import Path
  6
  7from sqlglot import exp
  8from sqlglot.dialects.dialect import DialectType
  9
 10from sqlmesh.core.console import get_console
 11from sqlmesh.core.engine_adapter import EngineAdapter
 12from sqlmesh.core.model.definition import Model
 13from sqlmesh.core.state_sync import StateReader
 14from sqlmesh.utils import UniqueKeyDict, yaml
 15from sqlmesh.utils.errors import SQLMeshError
 16
 17
 18def create_external_models_file(
 19    path: Path,
 20    models: UniqueKeyDict[str, Model],
 21    adapter: EngineAdapter,
 22    state_reader: StateReader,
 23    dialect: DialectType,
 24    gateway: t.Optional[str] = None,
 25    max_workers: int = 1,
 26    strict: bool = False,
 27    all_models: t.Optional[t.Dict[str, Model]] = None,
 28) -> None:
 29    """Create or replace a YAML file with column and types of all columns in all external models.
 30
 31    Args:
 32        path: The path to store the YAML file.
 33        models: FQN to model for the current repo/config being processed.
 34        adapter: The engine adapter.
 35        state_reader: The state reader.
 36        dialect: The dialect to serialize the schema as.
 37        gateway: If the model should be associated with a specific gateway; the gateway key
 38        max_workers: The max concurrent workers to fetch columns.
 39        strict: If True, raise an error if the external model is missing in the database.
 40        all_models: FQN to model across all loaded repos. When provided, a dependency is only
 41            classified as external if it is absent from this full set. This prevents cross-repo
 42            internal models from being misclassified as external in multi-repo setups.
 43    """
 44    known_models: t.Dict[str, Model] = all_models if all_models is not None else models
 45    external_model_fqns = set()
 46
 47    for fqn, model in models.items():
 48        if model.kind.is_external:
 49            external_model_fqns.add(fqn)
 50        for dep in model.depends_on:
 51            if dep not in known_models:
 52                external_model_fqns.add(dep)
 53
 54    # Make sure we don't convert internal models into external ones.
 55    existing_model_fqns = state_reader.nodes_exist(external_model_fqns, exclude_external=True)
 56    if existing_model_fqns:
 57        existing_model_fqns_str = ", ".join(existing_model_fqns)
 58        get_console().log_warning(
 59            f"The following models already exist and can't be converted to external: {existing_model_fqns_str}. "
 60            "Perhaps these models have been removed, while downstream models that reference them weren't updated accordingly."
 61        )
 62        external_model_fqns -= existing_model_fqns
 63
 64    with ThreadPoolExecutor(max_workers=max_workers) as pool:
 65        gateway_part = {"gateway": gateway} if gateway else {}
 66
 67        schemas = [
 68            {
 69                "name": exp.to_table(table).sql(dialect=dialect),
 70                "columns": columns,
 71                **gateway_part,
 72            }
 73            for table, columns in sorted(
 74                pool.map(
 75                    lambda table: (table, get_columns(adapter, dialect, table, strict)),
 76                    external_model_fqns,
 77                )
 78            )
 79            if columns
 80        ]
 81
 82        # dont clobber existing entries from other gateways
 83        entries_to_keep = (
 84            [e for e in yaml.load(path) if e.get("gateway", None) != gateway]
 85            if path.exists()
 86            else []
 87        )
 88
 89        with open(path, "w", encoding="utf-8") as file:
 90            yaml.dump(entries_to_keep + schemas, file)
 91
 92
 93def get_columns(
 94    adapter: EngineAdapter, dialect: DialectType, table: str, strict: bool
 95) -> t.Optional[t.Dict[str, t.Any]]:
 96    """
 97    Return the column and their types in a dictionary
 98    """
 99    try:
100        columns = adapter.columns(table, include_pseudo_columns=True)
101        return {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()}
102    except Exception as e:
103        msg = f"Unable to get schema for '{table}': '{e}'."
104        if strict:
105            raise SQLMeshError(msg) from e
106        get_console().log_warning(msg)
107        return None
def create_external_models_file( path: pathlib.Path, models: sqlmesh.utils.UniqueKeyDict[str, typing.Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel]], adapter: sqlmesh.core.engine_adapter.base.EngineAdapter, state_reader: sqlmesh.core.state_sync.base.StateReader, dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType], gateway: Optional[str] = None, max_workers: int = 1, strict: bool = False, all_models: Optional[Dict[str, Union[sqlmesh.core.model.definition.SqlModel, sqlmesh.core.model.definition.SeedModel, sqlmesh.core.model.definition.PythonModel, sqlmesh.core.model.definition.ExternalModel]]] = None) -> None:
19def create_external_models_file(
20    path: Path,
21    models: UniqueKeyDict[str, Model],
22    adapter: EngineAdapter,
23    state_reader: StateReader,
24    dialect: DialectType,
25    gateway: t.Optional[str] = None,
26    max_workers: int = 1,
27    strict: bool = False,
28    all_models: t.Optional[t.Dict[str, Model]] = None,
29) -> None:
30    """Create or replace a YAML file with column and types of all columns in all external models.
31
32    Args:
33        path: The path to store the YAML file.
34        models: FQN to model for the current repo/config being processed.
35        adapter: The engine adapter.
36        state_reader: The state reader.
37        dialect: The dialect to serialize the schema as.
38        gateway: If the model should be associated with a specific gateway; the gateway key
39        max_workers: The max concurrent workers to fetch columns.
40        strict: If True, raise an error if the external model is missing in the database.
41        all_models: FQN to model across all loaded repos. When provided, a dependency is only
42            classified as external if it is absent from this full set. This prevents cross-repo
43            internal models from being misclassified as external in multi-repo setups.
44    """
45    known_models: t.Dict[str, Model] = all_models if all_models is not None else models
46    external_model_fqns = set()
47
48    for fqn, model in models.items():
49        if model.kind.is_external:
50            external_model_fqns.add(fqn)
51        for dep in model.depends_on:
52            if dep not in known_models:
53                external_model_fqns.add(dep)
54
55    # Make sure we don't convert internal models into external ones.
56    existing_model_fqns = state_reader.nodes_exist(external_model_fqns, exclude_external=True)
57    if existing_model_fqns:
58        existing_model_fqns_str = ", ".join(existing_model_fqns)
59        get_console().log_warning(
60            f"The following models already exist and can't be converted to external: {existing_model_fqns_str}. "
61            "Perhaps these models have been removed, while downstream models that reference them weren't updated accordingly."
62        )
63        external_model_fqns -= existing_model_fqns
64
65    with ThreadPoolExecutor(max_workers=max_workers) as pool:
66        gateway_part = {"gateway": gateway} if gateway else {}
67
68        schemas = [
69            {
70                "name": exp.to_table(table).sql(dialect=dialect),
71                "columns": columns,
72                **gateway_part,
73            }
74            for table, columns in sorted(
75                pool.map(
76                    lambda table: (table, get_columns(adapter, dialect, table, strict)),
77                    external_model_fqns,
78                )
79            )
80            if columns
81        ]
82
83        # dont clobber existing entries from other gateways
84        entries_to_keep = (
85            [e for e in yaml.load(path) if e.get("gateway", None) != gateway]
86            if path.exists()
87            else []
88        )
89
90        with open(path, "w", encoding="utf-8") as file:
91            yaml.dump(entries_to_keep + schemas, file)

Create or replace a YAML file with column and types of all columns in all external models.

Arguments:
  • path: The path to store the YAML file.
  • models: FQN to model for the current repo/config being processed.
  • adapter: The engine adapter.
  • state_reader: The state reader.
  • dialect: The dialect to serialize the schema as.
  • gateway: If the model should be associated with a specific gateway; the gateway key
  • max_workers: The max concurrent workers to fetch columns.
  • strict: If True, raise an error if the external model is missing in the database.
  • all_models: FQN to model across all loaded repos. When provided, a dependency is only classified as external if it is absent from this full set. This prevents cross-repo internal models from being misclassified as external in multi-repo setups.
def get_columns( adapter: sqlmesh.core.engine_adapter.base.EngineAdapter, dialect: Union[str, sqlglot.dialects.dialect.Dialect, type[sqlglot.dialects.dialect.Dialect], NoneType], table: str, strict: bool) -> Optional[Dict[str, Any]]:
 94def get_columns(
 95    adapter: EngineAdapter, dialect: DialectType, table: str, strict: bool
 96) -> t.Optional[t.Dict[str, t.Any]]:
 97    """
 98    Return the column and their types in a dictionary
 99    """
100    try:
101        columns = adapter.columns(table, include_pseudo_columns=True)
102        return {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()}
103    except Exception as e:
104        msg = f"Unable to get schema for '{table}': '{e}'."
105        if strict:
106            raise SQLMeshError(msg) from e
107        get_console().log_warning(msg)
108        return None

Return the column and their types in a dictionary