Edit on GitHub

sqlglot.schema

  1from __future__ import annotations
  2
  3import abc
  4import typing as t
  5
  6from sqlglot import expressions as exp
  7from sqlglot.dialects.dialect import Dialect
  8from sqlglot.errors import SchemaError
  9from sqlglot.helper import dict_depth, first
 10from sqlglot.trie import TrieResult, in_trie, new_trie
 11
 12
 13if t.TYPE_CHECKING:
 14    from sqlglot._typing import SchemaArgs
 15    from sqlglot.dialects.dialect import DialectType
 16    from collections.abc import Sequence
 17    from typing_extensions import Unpack
 18
 19    ColumnMapping = t.Union[dict[str, t.Any], str, list[str]]
 20
 21
 22class Schema(abc.ABC):
 23    """Abstract base class for database schemas"""
 24
 25    @property
 26    def dialect(self) -> Dialect | None:
 27        """
 28        Returns None by default. Subclasses that require dialect-specific
 29        behavior should override this property.
 30        """
 31        return None
 32
 33    @abc.abstractmethod
 34    def add_table(
 35        self,
 36        table: exp.Table | str,
 37        column_mapping: ColumnMapping | None = None,
 38        dialect: DialectType = None,
 39        normalize: bool | None = None,
 40        match_depth: bool = True,
 41    ) -> None:
 42        """
 43        Register or update a table. Some implementing classes may require column information to also be provided.
 44        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
 45
 46        Args:
 47            table: the `Table` expression instance or string representing the table.
 48            column_mapping: a column mapping that describes the structure of the table.
 49            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 50            normalize: whether to normalize identifiers according to the dialect of interest.
 51            match_depth: whether to enforce that the table must match the schema's depth or not.
 52        """
 53
 54    @abc.abstractmethod
 55    def column_names(
 56        self,
 57        table: exp.Table | str,
 58        only_visible: bool = False,
 59        dialect: DialectType = None,
 60        normalize: bool | None = None,
 61    ) -> Sequence[str]:
 62        """
 63        Get the column names for a table.
 64
 65        Args:
 66            table: the `Table` expression instance.
 67            only_visible: whether to include invisible columns.
 68            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 69            normalize: whether to normalize identifiers according to the dialect of interest.
 70
 71        Returns:
 72            The sequence of column names.
 73        """
 74
 75    @abc.abstractmethod
 76    def get_column_type(
 77        self,
 78        table: exp.Table | str,
 79        column: exp.Column | str,
 80        dialect: DialectType = None,
 81        normalize: bool | None = None,
 82    ) -> exp.DataType:
 83        """
 84        Get the `sqlglot.exp.DataType` type of a column in the schema.
 85
 86        Args:
 87            table: the source table.
 88            column: the target column.
 89            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 90            normalize: whether to normalize identifiers according to the dialect of interest.
 91
 92        Returns:
 93            The resulting column type.
 94        """
 95
 96    def has_column(
 97        self,
 98        table: exp.Table | str,
 99        column: exp.Column | str,
100        dialect: DialectType = None,
101        normalize: bool | None = None,
102    ) -> bool:
103        """
104        Returns whether `column` appears in `table`'s schema.
105
106        Args:
107            table: the source table.
108            column: the target column.
109            dialect: the SQL dialect that will be used to parse `table` if it's a string.
110            normalize: whether to normalize identifiers according to the dialect of interest.
111
112        Returns:
113            True if the column appears in the schema, False otherwise.
114        """
115        name = column if isinstance(column, str) else column.name
116        return name in self.column_names(table, dialect=dialect, normalize=normalize)
117
118    def get_udf_type(
119        self,
120        udf: exp.Anonymous | str,
121        dialect: DialectType = None,
122        normalize: bool | None = None,
123    ) -> exp.DataType:
124        """
125        Get the return type of a UDF.
126
127        Args:
128            udf: the UDF expression or string.
129            dialect: the SQL dialect for parsing string arguments.
130            normalize: whether to normalize identifiers.
131
132        Returns:
133            The return type as a DataType, or UNKNOWN if not found.
134        """
135        return exp.DType.UNKNOWN.into_expr()
136
137    @property
138    @abc.abstractmethod
139    def supported_table_args(self) -> tuple[str, ...]:
140        """
141        Table arguments this schema support, e.g. `("this", "db", "catalog")`
142        """
143
144    @property
145    def empty(self) -> bool:
146        """Returns whether the schema is empty."""
147        return True
148
149
150class AbstractMappingSchema:
151    def __init__(
152        self,
153        mapping: dict[str, object] | None = None,
154        udf_mapping: dict[str, object] | None = None,
155    ) -> None:
156        self.mapping: dict[str, object] = mapping or {}
157        self.mapping_trie: dict[str, object] = new_trie(
158            tuple(reversed(t)) for t in flatten_schema(self.mapping, depth=self.depth())
159        )
160        self.udf_mapping: dict[str, object] = udf_mapping or {}
161        self.udf_trie: dict[str, object] = new_trie(
162            tuple(reversed(t)) for t in flatten_schema(self.udf_mapping, depth=self.udf_depth())
163        )
164
165        self._supported_table_args: tuple[str, ...] = tuple()
166
167    @property
168    def empty(self) -> bool:
169        return not self.mapping
170
171    def depth(self) -> int:
172        return dict_depth(self.mapping)
173
174    def udf_depth(self) -> int:
175        return dict_depth(self.udf_mapping)
176
177    @property
178    def supported_table_args(self) -> tuple[str, ...]:
179        if not self._supported_table_args and self.mapping:
180            depth = self.depth()
181
182            if not depth:  # None
183                self._supported_table_args = tuple()
184            elif 1 <= depth <= 3:
185                self._supported_table_args = exp.TABLE_PARTS[:depth]
186            else:
187                raise SchemaError(f"Invalid mapping shape. Depth: {depth}")
188
189        return self._supported_table_args
190
191    def table_parts(self, table: exp.Table) -> list[str]:
192        return [p.name for p in reversed(table.parts)]
193
194    def udf_parts(self, udf: exp.Anonymous) -> list[str]:
195        # a.b.c(...) is represented as Dot(Dot(a, b), Anonymous(c, ...))
196        parent = udf.parent
197        parts = [p.name for p in parent.flatten()] if isinstance(parent, exp.Dot) else [udf.name]
198        return list(reversed(parts))[0 : self.udf_depth()]
199
200    def _find_in_trie(
201        self,
202        parts: list[str],
203        trie: dict[str, object],
204        raise_on_missing: bool,
205    ) -> list[str] | None:
206        value, trie = in_trie(trie, parts)
207
208        if value == TrieResult.FAILED:
209            return None
210
211        if value == TrieResult.PREFIX:
212            possibilities = flatten_schema(trie)
213
214            if len(possibilities) == 1:
215                parts.extend(possibilities[0])
216            else:
217                if raise_on_missing:
218                    joined_parts = ".".join(parts)
219                    message = ", ".join(".".join(p) for p in possibilities)
220                    raise SchemaError(f"Ambiguous mapping for {joined_parts}: {message}.")
221
222                return None
223
224        return parts
225
226    def find(
227        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
228    ) -> t.Any | None:
229        """
230        Returns the schema of a given table.
231
232        Args:
233            table: the target table.
234            raise_on_missing: whether to raise in case the schema is not found.
235            ensure_data_types: whether to convert `str` types to their `DataType` equivalents.
236
237        Returns:
238            The schema of the target table.
239        """
240        parts = self.table_parts(table)[0 : len(self.supported_table_args)]
241        resolved_parts = self._find_in_trie(parts, self.mapping_trie, raise_on_missing)
242
243        if resolved_parts is None:
244            return None
245
246        return self.nested_get(resolved_parts, raise_on_missing=raise_on_missing)
247
248    def find_udf(self, udf: exp.Anonymous, raise_on_missing: bool = False) -> t.Any | None:
249        """
250        Returns the return type of a given UDF.
251
252        Args:
253            udf: the target UDF expression.
254            raise_on_missing: whether to raise if the UDF is not found.
255
256        Returns:
257            The return type of the UDF, or None if not found.
258        """
259        parts = self.udf_parts(udf)
260        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing)
261
262        if resolved_parts is None:
263            return None
264
265        return nested_get(
266            self.udf_mapping,
267            *zip(resolved_parts, reversed(resolved_parts)),
268            raise_on_missing=raise_on_missing,
269        )
270
271    def nested_get(
272        self,
273        parts: Sequence[str],
274        d: dict[str, object] | None = None,
275        raise_on_missing: bool = True,
276    ) -> t.Any | None:
277        return nested_get(
278            d or self.mapping,
279            *zip(self.supported_table_args, reversed(parts)),
280            raise_on_missing=raise_on_missing,
281        )
282
283
284class MappingSchema(AbstractMappingSchema, Schema):
285    """
286    Schema based on a nested mapping.
287
288    Args:
289        schema: Mapping in one of the following forms:
290            1. {table: {col: type}}
291            2. {db: {table: {col: type}}}
292            3. {catalog: {db: {table: {col: type}}}}
293            4. None - Tables will be added later
294        visible: Optional mapping of which columns in the schema are visible. If not provided, all columns
295            are assumed to be visible. The nesting should mirror that of the schema:
296            1. {table: set(*cols)}}
297            2. {db: {table: set(*cols)}}}
298            3. {catalog: {db: {table: set(*cols)}}}}
299        dialect: The dialect to be used for custom type mappings & parsing string arguments.
300        normalize: Whether to normalize identifier names according to the given dialect or not.
301    """
302
303    def __init__(
304        self,
305        schema: dict[str, object] | None = None,
306        visible: dict[str, object] | None = None,
307        dialect: DialectType = None,
308        normalize: bool = True,
309        udf_mapping: dict[str, object] | None = None,
310    ) -> None:
311        self.visible: dict[str, object] = {} if visible is None else visible
312        self.normalize: bool = normalize
313        self._dialect: Dialect = Dialect.get_or_raise(dialect)
314        self._type_mapping_cache: dict[str, exp.DataType] = {}
315        self._normalized_table_cache: dict[tuple[exp.Table, DialectType, bool], exp.Table] = {}
316        self._normalized_name_cache: dict[tuple[str, DialectType, bool, bool], str] = {}
317        self._find_cache: dict[tuple[exp.Table, bool], dict[str, object] | None] = {}
318        self._depth: int = 0
319        schema = {} if schema is None else schema
320        udf_mapping = {} if udf_mapping is None else udf_mapping
321
322        super().__init__(
323            self._normalize(schema) if self.normalize else schema,
324            self._normalize_udfs(udf_mapping) if self.normalize else udf_mapping,
325        )
326
327    @property
328    def dialect(self) -> Dialect:
329        """Returns the dialect for this mapping schema."""
330        return self._dialect
331
332    @classmethod
333    def from_mapping_schema(cls, mapping_schema: MappingSchema) -> MappingSchema:
334        return MappingSchema(
335            schema=mapping_schema.mapping,
336            visible=mapping_schema.visible,
337            dialect=mapping_schema.dialect,
338            normalize=mapping_schema.normalize,
339            udf_mapping=mapping_schema.udf_mapping,
340        )
341
342    def find(
343        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
344    ) -> t.Any | None:
345        cache_key = (table, ensure_data_types)
346        schema = self._find_cache.get(cache_key)
347
348        if schema is None:
349            schema = super().find(table, raise_on_missing=raise_on_missing)
350            if ensure_data_types and isinstance(schema, dict):
351                schema = {
352                    col: self._to_data_type(dtype) if isinstance(dtype, str) else dtype
353                    for col, dtype in schema.items()
354                }
355            self._find_cache[cache_key] = schema
356
357        return schema
358
359    def copy(
360        self, schema: dict[str, object] | None = None, **kwargs: Unpack[SchemaArgs]
361    ) -> MappingSchema:
362        mapping_kwargs: SchemaArgs = {
363            "visible": self.visible.copy(),
364            "dialect": self.dialect,
365            "normalize": self.normalize,
366            "udf_mapping": self.udf_mapping.copy(),
367            **kwargs,
368        }
369        return MappingSchema(self.mapping.copy() if schema is None else schema, **mapping_kwargs)
370
371    def add_table(
372        self,
373        table: exp.Table | str,
374        column_mapping: ColumnMapping | None = None,
375        dialect: DialectType = None,
376        normalize: bool | None = None,
377        match_depth: bool = True,
378    ) -> None:
379        """
380        Register or update a table. Updates are only performed if a new column mapping is provided.
381        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
382
383        Args:
384            table: the `Table` expression instance or string representing the table.
385            column_mapping: a column mapping that describes the structure of the table.
386            dialect: the SQL dialect that will be used to parse `table` if it's a string.
387            normalize: whether to normalize identifiers according to the dialect of interest.
388            match_depth: whether to enforce that the table must match the schema's depth or not.
389        """
390        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
391
392        if match_depth and not self.empty and len(normalized_table.parts) != self.depth():
393            raise SchemaError(
394                f"Table {normalized_table.sql(dialect=self.dialect)} must match the "
395                f"schema's nesting level: {self.depth()}."
396            )
397
398        normalized_column_mapping = {
399            self._normalize_name(key, dialect=dialect, normalize=normalize): value
400            for key, value in ensure_column_mapping(column_mapping).items()
401        }
402
403        schema = self.find(normalized_table, raise_on_missing=False)
404        if schema and not normalized_column_mapping:
405            return
406
407        parts = self.table_parts(normalized_table)
408
409        nested_set(self.mapping, tuple(reversed(parts)), normalized_column_mapping)
410        new_trie([parts], self.mapping_trie)
411        self._find_cache.pop((normalized_table, True), None)
412        self._find_cache.pop((normalized_table, False), None)
413
414    def column_names(
415        self,
416        table: exp.Table | str,
417        only_visible: bool = False,
418        dialect: DialectType = None,
419        normalize: bool | None = None,
420    ) -> list[str]:
421        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
422
423        schema: dict[str, object] | None = self.find(normalized_table)
424        if schema is None:
425            return []
426
427        if not only_visible or not self.visible:
428            return list(schema)
429
430        visible = self.nested_get(self.table_parts(normalized_table), self.visible) or []
431        return [col for col in schema if col in visible]
432
433    def get_column_type(
434        self,
435        table: exp.Table | str,
436        column: exp.Column | str,
437        dialect: DialectType = None,
438        normalize: bool | None = None,
439    ) -> exp.DataType:
440        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
441
442        normalized_column_name = self._normalize_name(
443            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
444        )
445
446        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
447        if table_schema:
448            column_type = table_schema.get(normalized_column_name)
449
450            if isinstance(column_type, exp.DataType):
451                return column_type
452            elif isinstance(column_type, str):
453                return self._to_data_type(column_type, dialect=dialect)
454
455        return exp.DType.UNKNOWN.into_expr()
456
457    def get_udf_type(
458        self,
459        udf: exp.Anonymous | str,
460        dialect: DialectType = None,
461        normalize: bool | None = None,
462    ) -> exp.DataType:
463        """
464        Get the return type of a UDF.
465
466        Args:
467            udf: the UDF expression or string (e.g., "db.my_func()").
468            dialect: the SQL dialect for parsing string arguments.
469            normalize: whether to normalize identifiers.
470
471        Returns:
472            The return type as a DataType, or UNKNOWN if not found.
473        """
474        parts = self._normalize_udf(udf, dialect=dialect, normalize=normalize)
475        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing=False)
476
477        if resolved_parts is None:
478            return exp.DType.UNKNOWN.into_expr()
479
480        udf_type = nested_get(
481            self.udf_mapping,
482            *zip(resolved_parts, reversed(resolved_parts)),
483            raise_on_missing=False,
484        )
485
486        if isinstance(udf_type, exp.DataType):
487            return udf_type
488        elif isinstance(udf_type, str):
489            return self._to_data_type(udf_type, dialect=dialect)
490
491        return exp.DType.UNKNOWN.into_expr()
492
493    def has_column(
494        self,
495        table: exp.Table | str,
496        column: exp.Column | str,
497        dialect: DialectType = None,
498        normalize: bool | None = None,
499    ) -> bool:
500        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
501
502        normalized_column_name = self._normalize_name(
503            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
504        )
505
506        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
507        return normalized_column_name in table_schema if table_schema else False
508
509    def _normalize(self, schema: dict[str, object]) -> dict[str, object]:
510        """
511        Normalizes all identifiers in the schema.
512
513        Args:
514            schema: the schema to normalize.
515
516        Returns:
517            The normalized schema mapping.
518        """
519        normalized_mapping: dict[str, object] = {}
520        flattened_schema = flatten_schema(schema)
521        error_msg = "Table {} must match the schema's nesting level: {}."
522
523        for keys in flattened_schema:
524            columns = nested_get(schema, *zip(keys, keys))
525
526            if not isinstance(columns, dict):
527                raise SchemaError(error_msg.format(".".join(keys[:-1]), len(flattened_schema[0])))
528            if not columns:
529                raise SchemaError(f"Table {'.'.join(keys[:-1])} must have at least one column")
530            if isinstance(first(columns.values()), dict):
531                raise SchemaError(
532                    error_msg.format(
533                        ".".join(keys + flatten_schema(columns)[0]), len(flattened_schema[0])
534                    ),
535                )
536
537            normalized_keys = [self._normalize_name(key, is_table=True) for key in keys]
538            for column_name, column_type in columns.items():
539                nested_set(
540                    normalized_mapping,
541                    normalized_keys + [self._normalize_name(column_name)],
542                    column_type,
543                )
544
545        return normalized_mapping
546
547    def _normalize_udfs(self, udfs: dict[str, object]) -> dict[str, object]:
548        """
549        Normalizes all identifiers in the UDF mapping.
550
551        Args:
552            udfs: the UDF mapping to normalize.
553
554        Returns:
555            The normalized UDF mapping.
556        """
557        normalized_mapping: dict[str, object] = {}
558
559        for keys in flatten_schema(udfs, depth=dict_depth(udfs)):
560            udf_type = nested_get(udfs, *zip(keys, keys))
561            normalized_keys = [self._normalize_name(key, is_table=True) for key in keys]
562            nested_set(normalized_mapping, normalized_keys, udf_type)
563
564        return normalized_mapping
565
566    def _normalize_udf(
567        self,
568        udf: exp.Anonymous | str,
569        dialect: DialectType = None,
570        normalize: bool | None = None,
571    ) -> list[str]:
572        """
573        Extract and normalize UDF parts for lookup.
574
575        Args:
576            udf: the UDF expression or qualified string (e.g., "db.my_func()").
577            dialect: the SQL dialect for parsing.
578            normalize: whether to normalize identifiers.
579
580        Returns:
581            A list of normalized UDF parts (reversed for trie lookup).
582        """
583        dialect = dialect or self.dialect
584        normalize = self.normalize if normalize is None else normalize
585
586        if isinstance(udf, str):
587            parsed: exp.Expr = exp.maybe_parse(udf, dialect=dialect)
588
589            if isinstance(parsed, exp.Anonymous):
590                udf = parsed
591            elif isinstance(parsed, exp.Dot) and isinstance(parsed.expression, exp.Anonymous):
592                udf = parsed.expression
593            else:
594                raise SchemaError(f"Unable to parse UDF from: {udf!r}")
595        parts = self.udf_parts(udf)
596
597        if normalize:
598            parts = [self._normalize_name(part, dialect=dialect, is_table=True) for part in parts]
599
600        return parts
601
602    def _normalize_table(
603        self,
604        table: exp.Table | str,
605        dialect: DialectType = None,
606        normalize: bool | None = None,
607    ) -> exp.Table:
608        dialect = dialect or self.dialect
609        normalize = self.normalize if normalize is None else normalize
610
611        # Cache normalized tables by object id for exp.Table inputs
612        # This is effective when the same Table object is looked up multiple times
613        if isinstance(table, exp.Table) and (
614            cached := self._normalized_table_cache.get((table, dialect, normalize))
615        ):
616            return cached
617
618        normalized_table = exp.maybe_parse(table, into=exp.Table, dialect=dialect, copy=normalize)
619
620        if normalize:
621            for part in normalized_table.parts:
622                if isinstance(part, exp.Identifier):
623                    part.replace(
624                        normalize_name(part, dialect=dialect, is_table=True, normalize=normalize)
625                    )
626
627        self._normalized_table_cache[(normalized_table, dialect, normalize)] = normalized_table
628        return normalized_table
629
630    def _normalize_name(
631        self,
632        name: str | exp.Identifier,
633        dialect: DialectType = None,
634        is_table: bool = False,
635        normalize: bool | None = None,
636    ) -> str:
637        normalize = self.normalize if normalize is None else normalize
638
639        dialect = dialect or self.dialect
640        name_str = name if isinstance(name, str) else name.name
641        cache_key = (name_str, dialect, is_table, normalize)
642
643        if cached := self._normalized_name_cache.get(cache_key):
644            return cached
645
646        result = normalize_name(
647            name,
648            dialect=dialect,
649            is_table=is_table,
650            normalize=normalize,
651        ).name
652
653        self._normalized_name_cache[cache_key] = result
654        return result
655
656    def depth(self) -> int:
657        if not self.empty and not self._depth:
658            # The columns themselves are a mapping, but we don't want to include those
659            self._depth = super().depth() - 1
660        return self._depth
661
662    def _to_data_type(self, schema_type: str, dialect: DialectType = None) -> exp.DataType:
663        """
664        Convert a type represented as a string to the corresponding `sqlglot.exp.DataType` object.
665
666        Args:
667            schema_type: the type we want to convert.
668            dialect: the SQL dialect that will be used to parse `schema_type`, if needed.
669
670        Returns:
671            The resulting expression type.
672        """
673        if schema_type not in self._type_mapping_cache:
674            dialect = Dialect.get_or_raise(dialect) if dialect else self.dialect
675            udt = dialect.SUPPORTS_USER_DEFINED_TYPES
676
677            try:
678                expression = exp.DataType.from_str(schema_type, dialect=dialect, udt=udt)
679                expression.transform(dialect.normalize_identifier, copy=False)
680                self._type_mapping_cache[schema_type] = expression
681            except AttributeError:
682                in_dialect = f" in dialect {dialect}" if dialect else ""
683                raise SchemaError(f"Failed to build type '{schema_type}'{in_dialect}.")
684
685        return self._type_mapping_cache[schema_type]
686
687
688def normalize_name(
689    identifier: str | exp.Identifier,
690    dialect: DialectType = None,
691    is_table: bool = False,
692    normalize: bool | None = True,
693) -> exp.Identifier:
694    if isinstance(identifier, str):
695        identifier = exp.parse_identifier(identifier, dialect=dialect)
696    elif normalize:
697        identifier = identifier.copy()
698
699    if not normalize:
700        return identifier
701
702    # this is used for normalize_identifier, bigquery has special rules pertaining tables
703    identifier.meta["is_table"] = is_table
704    return Dialect.get_or_raise(dialect).normalize_identifier(identifier)
705
706
707def ensure_schema(
708    schema: Schema | dict[str, object] | None, **kwargs: Unpack[SchemaArgs]
709) -> Schema:
710    if isinstance(schema, Schema):
711        return schema
712
713    return MappingSchema(schema, **kwargs)
714
715
716def ensure_column_mapping(mapping: ColumnMapping | None) -> dict[str, t.Any]:
717    if mapping is None:
718        return {}
719    elif isinstance(mapping, dict):
720        return mapping
721    elif isinstance(mapping, str):
722        col_name_type_strs = [x.strip() for x in mapping.split(",")]
723        return {
724            name_type_str.split(":")[0].strip(): name_type_str.split(":")[1].strip()
725            for name_type_str in col_name_type_strs
726        }
727    elif isinstance(mapping, list):
728        return {x.strip(): None for x in mapping}
729
730    raise ValueError(f"Invalid mapping provided: {type(mapping)}")
731
732
733def flatten_schema(
734    schema: dict[str, object], depth: int | None = None, keys: list[str] | None = None
735) -> list[list[str]]:
736    tables: list[list[str]] = []
737    keys = keys or []
738    depth = dict_depth(schema) - 1 if depth is None else depth
739
740    for k, v in schema.items():
741        if depth == 1 or not isinstance(v, dict):
742            tables.append(keys + [k])
743        elif depth >= 2:
744            tables.extend(flatten_schema(v, depth - 1, keys + [k]))
745
746    return tables
747
748
749def nested_get(
750    d: dict[str, object], *path: tuple[str, str], raise_on_missing: bool = True
751) -> t.Any | None:
752    """
753    Get a value for a nested dictionary.
754
755    Args:
756        d: the dictionary to search.
757        *path: tuples of (name, key), where:
758            `key` is the key in the dictionary to get.
759            `name` is a string to use in the error if `key` isn't found.
760
761    Returns:
762        The value or None if it doesn't exist.
763    """
764    result: t.Any = d
765    for name, key in path:
766        result = result.get(key)
767        if result is None:
768            if raise_on_missing:
769                name = "table" if name == "this" else name
770                raise ValueError(f"Unknown {name}: {key}")
771            return None
772
773    return result
774
775
776def nested_set(d: dict[str, t.Any], keys: Sequence[str], value: t.Any) -> dict[str, t.Any]:
777    """
778    In-place set a value for a nested dictionary
779
780    Example:
781        >>> nested_set({}, ["top_key", "second_key"], "value")
782        {'top_key': {'second_key': 'value'}}
783
784        >>> nested_set({"top_key": {"third_key": "third_value"}}, ["top_key", "second_key"], "value")
785        {'top_key': {'third_key': 'third_value', 'second_key': 'value'}}
786
787    Args:
788        d: dictionary to update.
789        keys: the keys that makeup the path to `value`.
790        value: the value to set in the dictionary for the given key path.
791
792    Returns:
793        The (possibly) updated dictionary.
794    """
795    if not keys:
796        return d
797
798    if len(keys) == 1:
799        d[keys[0]] = value
800        return d
801
802    subd = d
803    for key in keys[:-1]:
804        if key not in subd:
805            subd = subd.setdefault(key, {})
806        else:
807            subd = subd[key]
808
809    subd[keys[-1]] = value
810    return d
class Schema(abc.ABC):
 23class Schema(abc.ABC):
 24    """Abstract base class for database schemas"""
 25
 26    @property
 27    def dialect(self) -> Dialect | None:
 28        """
 29        Returns None by default. Subclasses that require dialect-specific
 30        behavior should override this property.
 31        """
 32        return None
 33
 34    @abc.abstractmethod
 35    def add_table(
 36        self,
 37        table: exp.Table | str,
 38        column_mapping: ColumnMapping | None = None,
 39        dialect: DialectType = None,
 40        normalize: bool | None = None,
 41        match_depth: bool = True,
 42    ) -> None:
 43        """
 44        Register or update a table. Some implementing classes may require column information to also be provided.
 45        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
 46
 47        Args:
 48            table: the `Table` expression instance or string representing the table.
 49            column_mapping: a column mapping that describes the structure of the table.
 50            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 51            normalize: whether to normalize identifiers according to the dialect of interest.
 52            match_depth: whether to enforce that the table must match the schema's depth or not.
 53        """
 54
 55    @abc.abstractmethod
 56    def column_names(
 57        self,
 58        table: exp.Table | str,
 59        only_visible: bool = False,
 60        dialect: DialectType = None,
 61        normalize: bool | None = None,
 62    ) -> Sequence[str]:
 63        """
 64        Get the column names for a table.
 65
 66        Args:
 67            table: the `Table` expression instance.
 68            only_visible: whether to include invisible columns.
 69            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 70            normalize: whether to normalize identifiers according to the dialect of interest.
 71
 72        Returns:
 73            The sequence of column names.
 74        """
 75
 76    @abc.abstractmethod
 77    def get_column_type(
 78        self,
 79        table: exp.Table | str,
 80        column: exp.Column | str,
 81        dialect: DialectType = None,
 82        normalize: bool | None = None,
 83    ) -> exp.DataType:
 84        """
 85        Get the `sqlglot.exp.DataType` type of a column in the schema.
 86
 87        Args:
 88            table: the source table.
 89            column: the target column.
 90            dialect: the SQL dialect that will be used to parse `table` if it's a string.
 91            normalize: whether to normalize identifiers according to the dialect of interest.
 92
 93        Returns:
 94            The resulting column type.
 95        """
 96
 97    def has_column(
 98        self,
 99        table: exp.Table | str,
100        column: exp.Column | str,
101        dialect: DialectType = None,
102        normalize: bool | None = None,
103    ) -> bool:
104        """
105        Returns whether `column` appears in `table`'s schema.
106
107        Args:
108            table: the source table.
109            column: the target column.
110            dialect: the SQL dialect that will be used to parse `table` if it's a string.
111            normalize: whether to normalize identifiers according to the dialect of interest.
112
113        Returns:
114            True if the column appears in the schema, False otherwise.
115        """
116        name = column if isinstance(column, str) else column.name
117        return name in self.column_names(table, dialect=dialect, normalize=normalize)
118
119    def get_udf_type(
120        self,
121        udf: exp.Anonymous | str,
122        dialect: DialectType = None,
123        normalize: bool | None = None,
124    ) -> exp.DataType:
125        """
126        Get the return type of a UDF.
127
128        Args:
129            udf: the UDF expression or string.
130            dialect: the SQL dialect for parsing string arguments.
131            normalize: whether to normalize identifiers.
132
133        Returns:
134            The return type as a DataType, or UNKNOWN if not found.
135        """
136        return exp.DType.UNKNOWN.into_expr()
137
138    @property
139    @abc.abstractmethod
140    def supported_table_args(self) -> tuple[str, ...]:
141        """
142        Table arguments this schema support, e.g. `("this", "db", "catalog")`
143        """
144
145    @property
146    def empty(self) -> bool:
147        """Returns whether the schema is empty."""
148        return True

Abstract base class for database schemas

dialect: sqlglot.dialects.Dialect | None
26    @property
27    def dialect(self) -> Dialect | None:
28        """
29        Returns None by default. Subclasses that require dialect-specific
30        behavior should override this property.
31        """
32        return None

Returns None by default. Subclasses that require dialect-specific behavior should override this property.

@abc.abstractmethod
def add_table( self, table: sqlglot.expressions.query.Table | str, column_mapping: Union[dict[str, Any], str, list[str], NoneType] = None, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None, match_depth: bool = True) -> None:
34    @abc.abstractmethod
35    def add_table(
36        self,
37        table: exp.Table | str,
38        column_mapping: ColumnMapping | None = None,
39        dialect: DialectType = None,
40        normalize: bool | None = None,
41        match_depth: bool = True,
42    ) -> None:
43        """
44        Register or update a table. Some implementing classes may require column information to also be provided.
45        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
46
47        Args:
48            table: the `Table` expression instance or string representing the table.
49            column_mapping: a column mapping that describes the structure of the table.
50            dialect: the SQL dialect that will be used to parse `table` if it's a string.
51            normalize: whether to normalize identifiers according to the dialect of interest.
52            match_depth: whether to enforce that the table must match the schema's depth or not.
53        """

Register or update a table. Some implementing classes may require column information to also be provided. The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.

Arguments:
  • table: the Table expression instance or string representing the table.
  • column_mapping: a column mapping that describes the structure of the table.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
  • match_depth: whether to enforce that the table must match the schema's depth or not.
@abc.abstractmethod
def column_names( self, table: sqlglot.expressions.query.Table | str, only_visible: bool = False, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> Sequence[str]:
55    @abc.abstractmethod
56    def column_names(
57        self,
58        table: exp.Table | str,
59        only_visible: bool = False,
60        dialect: DialectType = None,
61        normalize: bool | None = None,
62    ) -> Sequence[str]:
63        """
64        Get the column names for a table.
65
66        Args:
67            table: the `Table` expression instance.
68            only_visible: whether to include invisible columns.
69            dialect: the SQL dialect that will be used to parse `table` if it's a string.
70            normalize: whether to normalize identifiers according to the dialect of interest.
71
72        Returns:
73            The sequence of column names.
74        """

Get the column names for a table.

Arguments:
  • table: the Table expression instance.
  • only_visible: whether to include invisible columns.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

The sequence of column names.

@abc.abstractmethod
def get_column_type( self, table: sqlglot.expressions.query.Table | str, column: sqlglot.expressions.core.Column | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> sqlglot.expressions.datatypes.DataType:
76    @abc.abstractmethod
77    def get_column_type(
78        self,
79        table: exp.Table | str,
80        column: exp.Column | str,
81        dialect: DialectType = None,
82        normalize: bool | None = None,
83    ) -> exp.DataType:
84        """
85        Get the `sqlglot.exp.DataType` type of a column in the schema.
86
87        Args:
88            table: the source table.
89            column: the target column.
90            dialect: the SQL dialect that will be used to parse `table` if it's a string.
91            normalize: whether to normalize identifiers according to the dialect of interest.
92
93        Returns:
94            The resulting column type.
95        """

Get the sqlglot.exp.DataType type of a column in the schema.

Arguments:
  • table: the source table.
  • column: the target column.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

The resulting column type.

def has_column( self, table: sqlglot.expressions.query.Table | str, column: sqlglot.expressions.core.Column | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> bool:
 97    def has_column(
 98        self,
 99        table: exp.Table | str,
100        column: exp.Column | str,
101        dialect: DialectType = None,
102        normalize: bool | None = None,
103    ) -> bool:
104        """
105        Returns whether `column` appears in `table`'s schema.
106
107        Args:
108            table: the source table.
109            column: the target column.
110            dialect: the SQL dialect that will be used to parse `table` if it's a string.
111            normalize: whether to normalize identifiers according to the dialect of interest.
112
113        Returns:
114            True if the column appears in the schema, False otherwise.
115        """
116        name = column if isinstance(column, str) else column.name
117        return name in self.column_names(table, dialect=dialect, normalize=normalize)

Returns whether column appears in table's schema.

Arguments:
  • table: the source table.
  • column: the target column.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

True if the column appears in the schema, False otherwise.

def get_udf_type( self, udf: sqlglot.expressions.core.Anonymous | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> sqlglot.expressions.datatypes.DataType:
119    def get_udf_type(
120        self,
121        udf: exp.Anonymous | str,
122        dialect: DialectType = None,
123        normalize: bool | None = None,
124    ) -> exp.DataType:
125        """
126        Get the return type of a UDF.
127
128        Args:
129            udf: the UDF expression or string.
130            dialect: the SQL dialect for parsing string arguments.
131            normalize: whether to normalize identifiers.
132
133        Returns:
134            The return type as a DataType, or UNKNOWN if not found.
135        """
136        return exp.DType.UNKNOWN.into_expr()

Get the return type of a UDF.

Arguments:
  • udf: the UDF expression or string.
  • dialect: the SQL dialect for parsing string arguments.
  • normalize: whether to normalize identifiers.
Returns:

The return type as a DataType, or UNKNOWN if not found.

supported_table_args: tuple[str, ...]
138    @property
139    @abc.abstractmethod
140    def supported_table_args(self) -> tuple[str, ...]:
141        """
142        Table arguments this schema support, e.g. `("this", "db", "catalog")`
143        """

Table arguments this schema support, e.g. ("this", "db", "catalog")

empty: bool
145    @property
146    def empty(self) -> bool:
147        """Returns whether the schema is empty."""
148        return True

Returns whether the schema is empty.

class AbstractMappingSchema:
151class AbstractMappingSchema:
152    def __init__(
153        self,
154        mapping: dict[str, object] | None = None,
155        udf_mapping: dict[str, object] | None = None,
156    ) -> None:
157        self.mapping: dict[str, object] = mapping or {}
158        self.mapping_trie: dict[str, object] = new_trie(
159            tuple(reversed(t)) for t in flatten_schema(self.mapping, depth=self.depth())
160        )
161        self.udf_mapping: dict[str, object] = udf_mapping or {}
162        self.udf_trie: dict[str, object] = new_trie(
163            tuple(reversed(t)) for t in flatten_schema(self.udf_mapping, depth=self.udf_depth())
164        )
165
166        self._supported_table_args: tuple[str, ...] = tuple()
167
168    @property
169    def empty(self) -> bool:
170        return not self.mapping
171
172    def depth(self) -> int:
173        return dict_depth(self.mapping)
174
175    def udf_depth(self) -> int:
176        return dict_depth(self.udf_mapping)
177
178    @property
179    def supported_table_args(self) -> tuple[str, ...]:
180        if not self._supported_table_args and self.mapping:
181            depth = self.depth()
182
183            if not depth:  # None
184                self._supported_table_args = tuple()
185            elif 1 <= depth <= 3:
186                self._supported_table_args = exp.TABLE_PARTS[:depth]
187            else:
188                raise SchemaError(f"Invalid mapping shape. Depth: {depth}")
189
190        return self._supported_table_args
191
192    def table_parts(self, table: exp.Table) -> list[str]:
193        return [p.name for p in reversed(table.parts)]
194
195    def udf_parts(self, udf: exp.Anonymous) -> list[str]:
196        # a.b.c(...) is represented as Dot(Dot(a, b), Anonymous(c, ...))
197        parent = udf.parent
198        parts = [p.name for p in parent.flatten()] if isinstance(parent, exp.Dot) else [udf.name]
199        return list(reversed(parts))[0 : self.udf_depth()]
200
201    def _find_in_trie(
202        self,
203        parts: list[str],
204        trie: dict[str, object],
205        raise_on_missing: bool,
206    ) -> list[str] | None:
207        value, trie = in_trie(trie, parts)
208
209        if value == TrieResult.FAILED:
210            return None
211
212        if value == TrieResult.PREFIX:
213            possibilities = flatten_schema(trie)
214
215            if len(possibilities) == 1:
216                parts.extend(possibilities[0])
217            else:
218                if raise_on_missing:
219                    joined_parts = ".".join(parts)
220                    message = ", ".join(".".join(p) for p in possibilities)
221                    raise SchemaError(f"Ambiguous mapping for {joined_parts}: {message}.")
222
223                return None
224
225        return parts
226
227    def find(
228        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
229    ) -> t.Any | None:
230        """
231        Returns the schema of a given table.
232
233        Args:
234            table: the target table.
235            raise_on_missing: whether to raise in case the schema is not found.
236            ensure_data_types: whether to convert `str` types to their `DataType` equivalents.
237
238        Returns:
239            The schema of the target table.
240        """
241        parts = self.table_parts(table)[0 : len(self.supported_table_args)]
242        resolved_parts = self._find_in_trie(parts, self.mapping_trie, raise_on_missing)
243
244        if resolved_parts is None:
245            return None
246
247        return self.nested_get(resolved_parts, raise_on_missing=raise_on_missing)
248
249    def find_udf(self, udf: exp.Anonymous, raise_on_missing: bool = False) -> t.Any | None:
250        """
251        Returns the return type of a given UDF.
252
253        Args:
254            udf: the target UDF expression.
255            raise_on_missing: whether to raise if the UDF is not found.
256
257        Returns:
258            The return type of the UDF, or None if not found.
259        """
260        parts = self.udf_parts(udf)
261        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing)
262
263        if resolved_parts is None:
264            return None
265
266        return nested_get(
267            self.udf_mapping,
268            *zip(resolved_parts, reversed(resolved_parts)),
269            raise_on_missing=raise_on_missing,
270        )
271
272    def nested_get(
273        self,
274        parts: Sequence[str],
275        d: dict[str, object] | None = None,
276        raise_on_missing: bool = True,
277    ) -> t.Any | None:
278        return nested_get(
279            d or self.mapping,
280            *zip(self.supported_table_args, reversed(parts)),
281            raise_on_missing=raise_on_missing,
282        )
AbstractMappingSchema( mapping: dict[str, object] | None = None, udf_mapping: dict[str, object] | None = None)
152    def __init__(
153        self,
154        mapping: dict[str, object] | None = None,
155        udf_mapping: dict[str, object] | None = None,
156    ) -> None:
157        self.mapping: dict[str, object] = mapping or {}
158        self.mapping_trie: dict[str, object] = new_trie(
159            tuple(reversed(t)) for t in flatten_schema(self.mapping, depth=self.depth())
160        )
161        self.udf_mapping: dict[str, object] = udf_mapping or {}
162        self.udf_trie: dict[str, object] = new_trie(
163            tuple(reversed(t)) for t in flatten_schema(self.udf_mapping, depth=self.udf_depth())
164        )
165
166        self._supported_table_args: tuple[str, ...] = tuple()
mapping: dict[str, object]
mapping_trie: dict[str, object]
udf_mapping: dict[str, object]
udf_trie: dict[str, object]
empty: bool
168    @property
169    def empty(self) -> bool:
170        return not self.mapping
def depth(self) -> int:
172    def depth(self) -> int:
173        return dict_depth(self.mapping)
def udf_depth(self) -> int:
175    def udf_depth(self) -> int:
176        return dict_depth(self.udf_mapping)
supported_table_args: tuple[str, ...]
178    @property
179    def supported_table_args(self) -> tuple[str, ...]:
180        if not self._supported_table_args and self.mapping:
181            depth = self.depth()
182
183            if not depth:  # None
184                self._supported_table_args = tuple()
185            elif 1 <= depth <= 3:
186                self._supported_table_args = exp.TABLE_PARTS[:depth]
187            else:
188                raise SchemaError(f"Invalid mapping shape. Depth: {depth}")
189
190        return self._supported_table_args
def table_parts(self, table: sqlglot.expressions.query.Table) -> list[str]:
192    def table_parts(self, table: exp.Table) -> list[str]:
193        return [p.name for p in reversed(table.parts)]
def udf_parts(self, udf: sqlglot.expressions.core.Anonymous) -> list[str]:
195    def udf_parts(self, udf: exp.Anonymous) -> list[str]:
196        # a.b.c(...) is represented as Dot(Dot(a, b), Anonymous(c, ...))
197        parent = udf.parent
198        parts = [p.name for p in parent.flatten()] if isinstance(parent, exp.Dot) else [udf.name]
199        return list(reversed(parts))[0 : self.udf_depth()]
def find( self, table: sqlglot.expressions.query.Table, raise_on_missing: bool = True, ensure_data_types: bool = False) -> Optional[Any]:
227    def find(
228        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
229    ) -> t.Any | None:
230        """
231        Returns the schema of a given table.
232
233        Args:
234            table: the target table.
235            raise_on_missing: whether to raise in case the schema is not found.
236            ensure_data_types: whether to convert `str` types to their `DataType` equivalents.
237
238        Returns:
239            The schema of the target table.
240        """
241        parts = self.table_parts(table)[0 : len(self.supported_table_args)]
242        resolved_parts = self._find_in_trie(parts, self.mapping_trie, raise_on_missing)
243
244        if resolved_parts is None:
245            return None
246
247        return self.nested_get(resolved_parts, raise_on_missing=raise_on_missing)

Returns the schema of a given table.

Arguments:
  • table: the target table.
  • raise_on_missing: whether to raise in case the schema is not found.
  • ensure_data_types: whether to convert str types to their DataType equivalents.
Returns:

The schema of the target table.

def find_udf( self, udf: sqlglot.expressions.core.Anonymous, raise_on_missing: bool = False) -> Optional[Any]:
249    def find_udf(self, udf: exp.Anonymous, raise_on_missing: bool = False) -> t.Any | None:
250        """
251        Returns the return type of a given UDF.
252
253        Args:
254            udf: the target UDF expression.
255            raise_on_missing: whether to raise if the UDF is not found.
256
257        Returns:
258            The return type of the UDF, or None if not found.
259        """
260        parts = self.udf_parts(udf)
261        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing)
262
263        if resolved_parts is None:
264            return None
265
266        return nested_get(
267            self.udf_mapping,
268            *zip(resolved_parts, reversed(resolved_parts)),
269            raise_on_missing=raise_on_missing,
270        )

Returns the return type of a given UDF.

Arguments:
  • udf: the target UDF expression.
  • raise_on_missing: whether to raise if the UDF is not found.
Returns:

The return type of the UDF, or None if not found.

def nested_get( self, parts: Sequence[str], d: dict[str, object] | None = None, raise_on_missing: bool = True) -> Optional[Any]:
272    def nested_get(
273        self,
274        parts: Sequence[str],
275        d: dict[str, object] | None = None,
276        raise_on_missing: bool = True,
277    ) -> t.Any | None:
278        return nested_get(
279            d or self.mapping,
280            *zip(self.supported_table_args, reversed(parts)),
281            raise_on_missing=raise_on_missing,
282        )
class MappingSchema(AbstractMappingSchema, Schema):
285class MappingSchema(AbstractMappingSchema, Schema):
286    """
287    Schema based on a nested mapping.
288
289    Args:
290        schema: Mapping in one of the following forms:
291            1. {table: {col: type}}
292            2. {db: {table: {col: type}}}
293            3. {catalog: {db: {table: {col: type}}}}
294            4. None - Tables will be added later
295        visible: Optional mapping of which columns in the schema are visible. If not provided, all columns
296            are assumed to be visible. The nesting should mirror that of the schema:
297            1. {table: set(*cols)}}
298            2. {db: {table: set(*cols)}}}
299            3. {catalog: {db: {table: set(*cols)}}}}
300        dialect: The dialect to be used for custom type mappings & parsing string arguments.
301        normalize: Whether to normalize identifier names according to the given dialect or not.
302    """
303
304    def __init__(
305        self,
306        schema: dict[str, object] | None = None,
307        visible: dict[str, object] | None = None,
308        dialect: DialectType = None,
309        normalize: bool = True,
310        udf_mapping: dict[str, object] | None = None,
311    ) -> None:
312        self.visible: dict[str, object] = {} if visible is None else visible
313        self.normalize: bool = normalize
314        self._dialect: Dialect = Dialect.get_or_raise(dialect)
315        self._type_mapping_cache: dict[str, exp.DataType] = {}
316        self._normalized_table_cache: dict[tuple[exp.Table, DialectType, bool], exp.Table] = {}
317        self._normalized_name_cache: dict[tuple[str, DialectType, bool, bool], str] = {}
318        self._find_cache: dict[tuple[exp.Table, bool], dict[str, object] | None] = {}
319        self._depth: int = 0
320        schema = {} if schema is None else schema
321        udf_mapping = {} if udf_mapping is None else udf_mapping
322
323        super().__init__(
324            self._normalize(schema) if self.normalize else schema,
325            self._normalize_udfs(udf_mapping) if self.normalize else udf_mapping,
326        )
327
328    @property
329    def dialect(self) -> Dialect:
330        """Returns the dialect for this mapping schema."""
331        return self._dialect
332
333    @classmethod
334    def from_mapping_schema(cls, mapping_schema: MappingSchema) -> MappingSchema:
335        return MappingSchema(
336            schema=mapping_schema.mapping,
337            visible=mapping_schema.visible,
338            dialect=mapping_schema.dialect,
339            normalize=mapping_schema.normalize,
340            udf_mapping=mapping_schema.udf_mapping,
341        )
342
343    def find(
344        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
345    ) -> t.Any | None:
346        cache_key = (table, ensure_data_types)
347        schema = self._find_cache.get(cache_key)
348
349        if schema is None:
350            schema = super().find(table, raise_on_missing=raise_on_missing)
351            if ensure_data_types and isinstance(schema, dict):
352                schema = {
353                    col: self._to_data_type(dtype) if isinstance(dtype, str) else dtype
354                    for col, dtype in schema.items()
355                }
356            self._find_cache[cache_key] = schema
357
358        return schema
359
360    def copy(
361        self, schema: dict[str, object] | None = None, **kwargs: Unpack[SchemaArgs]
362    ) -> MappingSchema:
363        mapping_kwargs: SchemaArgs = {
364            "visible": self.visible.copy(),
365            "dialect": self.dialect,
366            "normalize": self.normalize,
367            "udf_mapping": self.udf_mapping.copy(),
368            **kwargs,
369        }
370        return MappingSchema(self.mapping.copy() if schema is None else schema, **mapping_kwargs)
371
372    def add_table(
373        self,
374        table: exp.Table | str,
375        column_mapping: ColumnMapping | None = None,
376        dialect: DialectType = None,
377        normalize: bool | None = None,
378        match_depth: bool = True,
379    ) -> None:
380        """
381        Register or update a table. Updates are only performed if a new column mapping is provided.
382        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
383
384        Args:
385            table: the `Table` expression instance or string representing the table.
386            column_mapping: a column mapping that describes the structure of the table.
387            dialect: the SQL dialect that will be used to parse `table` if it's a string.
388            normalize: whether to normalize identifiers according to the dialect of interest.
389            match_depth: whether to enforce that the table must match the schema's depth or not.
390        """
391        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
392
393        if match_depth and not self.empty and len(normalized_table.parts) != self.depth():
394            raise SchemaError(
395                f"Table {normalized_table.sql(dialect=self.dialect)} must match the "
396                f"schema's nesting level: {self.depth()}."
397            )
398
399        normalized_column_mapping = {
400            self._normalize_name(key, dialect=dialect, normalize=normalize): value
401            for key, value in ensure_column_mapping(column_mapping).items()
402        }
403
404        schema = self.find(normalized_table, raise_on_missing=False)
405        if schema and not normalized_column_mapping:
406            return
407
408        parts = self.table_parts(normalized_table)
409
410        nested_set(self.mapping, tuple(reversed(parts)), normalized_column_mapping)
411        new_trie([parts], self.mapping_trie)
412        self._find_cache.pop((normalized_table, True), None)
413        self._find_cache.pop((normalized_table, False), None)
414
415    def column_names(
416        self,
417        table: exp.Table | str,
418        only_visible: bool = False,
419        dialect: DialectType = None,
420        normalize: bool | None = None,
421    ) -> list[str]:
422        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
423
424        schema: dict[str, object] | None = self.find(normalized_table)
425        if schema is None:
426            return []
427
428        if not only_visible or not self.visible:
429            return list(schema)
430
431        visible = self.nested_get(self.table_parts(normalized_table), self.visible) or []
432        return [col for col in schema if col in visible]
433
434    def get_column_type(
435        self,
436        table: exp.Table | str,
437        column: exp.Column | str,
438        dialect: DialectType = None,
439        normalize: bool | None = None,
440    ) -> exp.DataType:
441        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
442
443        normalized_column_name = self._normalize_name(
444            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
445        )
446
447        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
448        if table_schema:
449            column_type = table_schema.get(normalized_column_name)
450
451            if isinstance(column_type, exp.DataType):
452                return column_type
453            elif isinstance(column_type, str):
454                return self._to_data_type(column_type, dialect=dialect)
455
456        return exp.DType.UNKNOWN.into_expr()
457
458    def get_udf_type(
459        self,
460        udf: exp.Anonymous | str,
461        dialect: DialectType = None,
462        normalize: bool | None = None,
463    ) -> exp.DataType:
464        """
465        Get the return type of a UDF.
466
467        Args:
468            udf: the UDF expression or string (e.g., "db.my_func()").
469            dialect: the SQL dialect for parsing string arguments.
470            normalize: whether to normalize identifiers.
471
472        Returns:
473            The return type as a DataType, or UNKNOWN if not found.
474        """
475        parts = self._normalize_udf(udf, dialect=dialect, normalize=normalize)
476        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing=False)
477
478        if resolved_parts is None:
479            return exp.DType.UNKNOWN.into_expr()
480
481        udf_type = nested_get(
482            self.udf_mapping,
483            *zip(resolved_parts, reversed(resolved_parts)),
484            raise_on_missing=False,
485        )
486
487        if isinstance(udf_type, exp.DataType):
488            return udf_type
489        elif isinstance(udf_type, str):
490            return self._to_data_type(udf_type, dialect=dialect)
491
492        return exp.DType.UNKNOWN.into_expr()
493
494    def has_column(
495        self,
496        table: exp.Table | str,
497        column: exp.Column | str,
498        dialect: DialectType = None,
499        normalize: bool | None = None,
500    ) -> bool:
501        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
502
503        normalized_column_name = self._normalize_name(
504            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
505        )
506
507        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
508        return normalized_column_name in table_schema if table_schema else False
509
510    def _normalize(self, schema: dict[str, object]) -> dict[str, object]:
511        """
512        Normalizes all identifiers in the schema.
513
514        Args:
515            schema: the schema to normalize.
516
517        Returns:
518            The normalized schema mapping.
519        """
520        normalized_mapping: dict[str, object] = {}
521        flattened_schema = flatten_schema(schema)
522        error_msg = "Table {} must match the schema's nesting level: {}."
523
524        for keys in flattened_schema:
525            columns = nested_get(schema, *zip(keys, keys))
526
527            if not isinstance(columns, dict):
528                raise SchemaError(error_msg.format(".".join(keys[:-1]), len(flattened_schema[0])))
529            if not columns:
530                raise SchemaError(f"Table {'.'.join(keys[:-1])} must have at least one column")
531            if isinstance(first(columns.values()), dict):
532                raise SchemaError(
533                    error_msg.format(
534                        ".".join(keys + flatten_schema(columns)[0]), len(flattened_schema[0])
535                    ),
536                )
537
538            normalized_keys = [self._normalize_name(key, is_table=True) for key in keys]
539            for column_name, column_type in columns.items():
540                nested_set(
541                    normalized_mapping,
542                    normalized_keys + [self._normalize_name(column_name)],
543                    column_type,
544                )
545
546        return normalized_mapping
547
548    def _normalize_udfs(self, udfs: dict[str, object]) -> dict[str, object]:
549        """
550        Normalizes all identifiers in the UDF mapping.
551
552        Args:
553            udfs: the UDF mapping to normalize.
554
555        Returns:
556            The normalized UDF mapping.
557        """
558        normalized_mapping: dict[str, object] = {}
559
560        for keys in flatten_schema(udfs, depth=dict_depth(udfs)):
561            udf_type = nested_get(udfs, *zip(keys, keys))
562            normalized_keys = [self._normalize_name(key, is_table=True) for key in keys]
563            nested_set(normalized_mapping, normalized_keys, udf_type)
564
565        return normalized_mapping
566
567    def _normalize_udf(
568        self,
569        udf: exp.Anonymous | str,
570        dialect: DialectType = None,
571        normalize: bool | None = None,
572    ) -> list[str]:
573        """
574        Extract and normalize UDF parts for lookup.
575
576        Args:
577            udf: the UDF expression or qualified string (e.g., "db.my_func()").
578            dialect: the SQL dialect for parsing.
579            normalize: whether to normalize identifiers.
580
581        Returns:
582            A list of normalized UDF parts (reversed for trie lookup).
583        """
584        dialect = dialect or self.dialect
585        normalize = self.normalize if normalize is None else normalize
586
587        if isinstance(udf, str):
588            parsed: exp.Expr = exp.maybe_parse(udf, dialect=dialect)
589
590            if isinstance(parsed, exp.Anonymous):
591                udf = parsed
592            elif isinstance(parsed, exp.Dot) and isinstance(parsed.expression, exp.Anonymous):
593                udf = parsed.expression
594            else:
595                raise SchemaError(f"Unable to parse UDF from: {udf!r}")
596        parts = self.udf_parts(udf)
597
598        if normalize:
599            parts = [self._normalize_name(part, dialect=dialect, is_table=True) for part in parts]
600
601        return parts
602
603    def _normalize_table(
604        self,
605        table: exp.Table | str,
606        dialect: DialectType = None,
607        normalize: bool | None = None,
608    ) -> exp.Table:
609        dialect = dialect or self.dialect
610        normalize = self.normalize if normalize is None else normalize
611
612        # Cache normalized tables by object id for exp.Table inputs
613        # This is effective when the same Table object is looked up multiple times
614        if isinstance(table, exp.Table) and (
615            cached := self._normalized_table_cache.get((table, dialect, normalize))
616        ):
617            return cached
618
619        normalized_table = exp.maybe_parse(table, into=exp.Table, dialect=dialect, copy=normalize)
620
621        if normalize:
622            for part in normalized_table.parts:
623                if isinstance(part, exp.Identifier):
624                    part.replace(
625                        normalize_name(part, dialect=dialect, is_table=True, normalize=normalize)
626                    )
627
628        self._normalized_table_cache[(normalized_table, dialect, normalize)] = normalized_table
629        return normalized_table
630
631    def _normalize_name(
632        self,
633        name: str | exp.Identifier,
634        dialect: DialectType = None,
635        is_table: bool = False,
636        normalize: bool | None = None,
637    ) -> str:
638        normalize = self.normalize if normalize is None else normalize
639
640        dialect = dialect or self.dialect
641        name_str = name if isinstance(name, str) else name.name
642        cache_key = (name_str, dialect, is_table, normalize)
643
644        if cached := self._normalized_name_cache.get(cache_key):
645            return cached
646
647        result = normalize_name(
648            name,
649            dialect=dialect,
650            is_table=is_table,
651            normalize=normalize,
652        ).name
653
654        self._normalized_name_cache[cache_key] = result
655        return result
656
657    def depth(self) -> int:
658        if not self.empty and not self._depth:
659            # The columns themselves are a mapping, but we don't want to include those
660            self._depth = super().depth() - 1
661        return self._depth
662
663    def _to_data_type(self, schema_type: str, dialect: DialectType = None) -> exp.DataType:
664        """
665        Convert a type represented as a string to the corresponding `sqlglot.exp.DataType` object.
666
667        Args:
668            schema_type: the type we want to convert.
669            dialect: the SQL dialect that will be used to parse `schema_type`, if needed.
670
671        Returns:
672            The resulting expression type.
673        """
674        if schema_type not in self._type_mapping_cache:
675            dialect = Dialect.get_or_raise(dialect) if dialect else self.dialect
676            udt = dialect.SUPPORTS_USER_DEFINED_TYPES
677
678            try:
679                expression = exp.DataType.from_str(schema_type, dialect=dialect, udt=udt)
680                expression.transform(dialect.normalize_identifier, copy=False)
681                self._type_mapping_cache[schema_type] = expression
682            except AttributeError:
683                in_dialect = f" in dialect {dialect}" if dialect else ""
684                raise SchemaError(f"Failed to build type '{schema_type}'{in_dialect}.")
685
686        return self._type_mapping_cache[schema_type]

Schema based on a nested mapping.

Arguments:
  • schema: Mapping in one of the following forms:
    1. {table: {col: type}}
    2. {db: {table: {col: type}}}
    3. {catalog: {db: {table: {col: type}}}}
    4. None - Tables will be added later
  • visible: Optional mapping of which columns in the schema are visible. If not provided, all columns are assumed to be visible. The nesting should mirror that of the schema:
    1. {table: set(*cols)}}
    2. {db: {table: set(*cols)}}}
    3. {catalog: {db: {table: set(*cols)}}}}
  • dialect: The dialect to be used for custom type mappings & parsing string arguments.
  • normalize: Whether to normalize identifier names according to the given dialect or not.
MappingSchema( schema: dict[str, object] | None = None, visible: dict[str, object] | None = None, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool = True, udf_mapping: dict[str, object] | None = None)
304    def __init__(
305        self,
306        schema: dict[str, object] | None = None,
307        visible: dict[str, object] | None = None,
308        dialect: DialectType = None,
309        normalize: bool = True,
310        udf_mapping: dict[str, object] | None = None,
311    ) -> None:
312        self.visible: dict[str, object] = {} if visible is None else visible
313        self.normalize: bool = normalize
314        self._dialect: Dialect = Dialect.get_or_raise(dialect)
315        self._type_mapping_cache: dict[str, exp.DataType] = {}
316        self._normalized_table_cache: dict[tuple[exp.Table, DialectType, bool], exp.Table] = {}
317        self._normalized_name_cache: dict[tuple[str, DialectType, bool, bool], str] = {}
318        self._find_cache: dict[tuple[exp.Table, bool], dict[str, object] | None] = {}
319        self._depth: int = 0
320        schema = {} if schema is None else schema
321        udf_mapping = {} if udf_mapping is None else udf_mapping
322
323        super().__init__(
324            self._normalize(schema) if self.normalize else schema,
325            self._normalize_udfs(udf_mapping) if self.normalize else udf_mapping,
326        )
visible: dict[str, object]
normalize: bool
dialect: sqlglot.dialects.Dialect
328    @property
329    def dialect(self) -> Dialect:
330        """Returns the dialect for this mapping schema."""
331        return self._dialect

Returns the dialect for this mapping schema.

@classmethod
def from_mapping_schema( cls, mapping_schema: MappingSchema) -> MappingSchema:
333    @classmethod
334    def from_mapping_schema(cls, mapping_schema: MappingSchema) -> MappingSchema:
335        return MappingSchema(
336            schema=mapping_schema.mapping,
337            visible=mapping_schema.visible,
338            dialect=mapping_schema.dialect,
339            normalize=mapping_schema.normalize,
340            udf_mapping=mapping_schema.udf_mapping,
341        )
def find( self, table: sqlglot.expressions.query.Table, raise_on_missing: bool = True, ensure_data_types: bool = False) -> Optional[Any]:
343    def find(
344        self, table: exp.Table, raise_on_missing: bool = True, ensure_data_types: bool = False
345    ) -> t.Any | None:
346        cache_key = (table, ensure_data_types)
347        schema = self._find_cache.get(cache_key)
348
349        if schema is None:
350            schema = super().find(table, raise_on_missing=raise_on_missing)
351            if ensure_data_types and isinstance(schema, dict):
352                schema = {
353                    col: self._to_data_type(dtype) if isinstance(dtype, str) else dtype
354                    for col, dtype in schema.items()
355                }
356            self._find_cache[cache_key] = schema
357
358        return schema

Returns the schema of a given table.

Arguments:
  • table: the target table.
  • raise_on_missing: whether to raise in case the schema is not found.
  • ensure_data_types: whether to convert str types to their DataType equivalents.
Returns:

The schema of the target table.

def copy( self, schema: dict[str, object] | None = None, **kwargs: typing_extensions.Unpack[sqlglot._typing.SchemaArgs]) -> MappingSchema:
360    def copy(
361        self, schema: dict[str, object] | None = None, **kwargs: Unpack[SchemaArgs]
362    ) -> MappingSchema:
363        mapping_kwargs: SchemaArgs = {
364            "visible": self.visible.copy(),
365            "dialect": self.dialect,
366            "normalize": self.normalize,
367            "udf_mapping": self.udf_mapping.copy(),
368            **kwargs,
369        }
370        return MappingSchema(self.mapping.copy() if schema is None else schema, **mapping_kwargs)
def add_table( self, table: sqlglot.expressions.query.Table | str, column_mapping: Union[dict[str, Any], str, list[str], NoneType] = None, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None, match_depth: bool = True) -> None:
372    def add_table(
373        self,
374        table: exp.Table | str,
375        column_mapping: ColumnMapping | None = None,
376        dialect: DialectType = None,
377        normalize: bool | None = None,
378        match_depth: bool = True,
379    ) -> None:
380        """
381        Register or update a table. Updates are only performed if a new column mapping is provided.
382        The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.
383
384        Args:
385            table: the `Table` expression instance or string representing the table.
386            column_mapping: a column mapping that describes the structure of the table.
387            dialect: the SQL dialect that will be used to parse `table` if it's a string.
388            normalize: whether to normalize identifiers according to the dialect of interest.
389            match_depth: whether to enforce that the table must match the schema's depth or not.
390        """
391        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
392
393        if match_depth and not self.empty and len(normalized_table.parts) != self.depth():
394            raise SchemaError(
395                f"Table {normalized_table.sql(dialect=self.dialect)} must match the "
396                f"schema's nesting level: {self.depth()}."
397            )
398
399        normalized_column_mapping = {
400            self._normalize_name(key, dialect=dialect, normalize=normalize): value
401            for key, value in ensure_column_mapping(column_mapping).items()
402        }
403
404        schema = self.find(normalized_table, raise_on_missing=False)
405        if schema and not normalized_column_mapping:
406            return
407
408        parts = self.table_parts(normalized_table)
409
410        nested_set(self.mapping, tuple(reversed(parts)), normalized_column_mapping)
411        new_trie([parts], self.mapping_trie)
412        self._find_cache.pop((normalized_table, True), None)
413        self._find_cache.pop((normalized_table, False), None)

Register or update a table. Updates are only performed if a new column mapping is provided. The added table must have the necessary number of qualifiers in its path to match the schema's nesting level.

Arguments:
  • table: the Table expression instance or string representing the table.
  • column_mapping: a column mapping that describes the structure of the table.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
  • match_depth: whether to enforce that the table must match the schema's depth or not.
def column_names( self, table: sqlglot.expressions.query.Table | str, only_visible: bool = False, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> list[str]:
415    def column_names(
416        self,
417        table: exp.Table | str,
418        only_visible: bool = False,
419        dialect: DialectType = None,
420        normalize: bool | None = None,
421    ) -> list[str]:
422        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
423
424        schema: dict[str, object] | None = self.find(normalized_table)
425        if schema is None:
426            return []
427
428        if not only_visible or not self.visible:
429            return list(schema)
430
431        visible = self.nested_get(self.table_parts(normalized_table), self.visible) or []
432        return [col for col in schema if col in visible]

Get the column names for a table.

Arguments:
  • table: the Table expression instance.
  • only_visible: whether to include invisible columns.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

The sequence of column names.

def get_column_type( self, table: sqlglot.expressions.query.Table | str, column: sqlglot.expressions.core.Column | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> sqlglot.expressions.datatypes.DataType:
434    def get_column_type(
435        self,
436        table: exp.Table | str,
437        column: exp.Column | str,
438        dialect: DialectType = None,
439        normalize: bool | None = None,
440    ) -> exp.DataType:
441        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
442
443        normalized_column_name = self._normalize_name(
444            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
445        )
446
447        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
448        if table_schema:
449            column_type = table_schema.get(normalized_column_name)
450
451            if isinstance(column_type, exp.DataType):
452                return column_type
453            elif isinstance(column_type, str):
454                return self._to_data_type(column_type, dialect=dialect)
455
456        return exp.DType.UNKNOWN.into_expr()

Get the sqlglot.exp.DataType type of a column in the schema.

Arguments:
  • table: the source table.
  • column: the target column.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

The resulting column type.

def get_udf_type( self, udf: sqlglot.expressions.core.Anonymous | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> sqlglot.expressions.datatypes.DataType:
458    def get_udf_type(
459        self,
460        udf: exp.Anonymous | str,
461        dialect: DialectType = None,
462        normalize: bool | None = None,
463    ) -> exp.DataType:
464        """
465        Get the return type of a UDF.
466
467        Args:
468            udf: the UDF expression or string (e.g., "db.my_func()").
469            dialect: the SQL dialect for parsing string arguments.
470            normalize: whether to normalize identifiers.
471
472        Returns:
473            The return type as a DataType, or UNKNOWN if not found.
474        """
475        parts = self._normalize_udf(udf, dialect=dialect, normalize=normalize)
476        resolved_parts = self._find_in_trie(parts, self.udf_trie, raise_on_missing=False)
477
478        if resolved_parts is None:
479            return exp.DType.UNKNOWN.into_expr()
480
481        udf_type = nested_get(
482            self.udf_mapping,
483            *zip(resolved_parts, reversed(resolved_parts)),
484            raise_on_missing=False,
485        )
486
487        if isinstance(udf_type, exp.DataType):
488            return udf_type
489        elif isinstance(udf_type, str):
490            return self._to_data_type(udf_type, dialect=dialect)
491
492        return exp.DType.UNKNOWN.into_expr()

Get the return type of a UDF.

Arguments:
  • udf: the UDF expression or string (e.g., "db.my_func()").
  • dialect: the SQL dialect for parsing string arguments.
  • normalize: whether to normalize identifiers.
Returns:

The return type as a DataType, or UNKNOWN if not found.

def has_column( self, table: sqlglot.expressions.query.Table | str, column: sqlglot.expressions.core.Column | str, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, normalize: bool | None = None) -> bool:
494    def has_column(
495        self,
496        table: exp.Table | str,
497        column: exp.Column | str,
498        dialect: DialectType = None,
499        normalize: bool | None = None,
500    ) -> bool:
501        normalized_table = self._normalize_table(table, dialect=dialect, normalize=normalize)
502
503        normalized_column_name = self._normalize_name(
504            column if isinstance(column, str) else column.this, dialect=dialect, normalize=normalize
505        )
506
507        table_schema: dict[str, object] | None = self.find(normalized_table, raise_on_missing=False)
508        return normalized_column_name in table_schema if table_schema else False

Returns whether column appears in table's schema.

Arguments:
  • table: the source table.
  • column: the target column.
  • dialect: the SQL dialect that will be used to parse table if it's a string.
  • normalize: whether to normalize identifiers according to the dialect of interest.
Returns:

True if the column appears in the schema, False otherwise.

def depth(self) -> int:
657    def depth(self) -> int:
658        if not self.empty and not self._depth:
659            # The columns themselves are a mapping, but we don't want to include those
660            self._depth = super().depth() - 1
661        return self._depth
def normalize_name( identifier: str | sqlglot.expressions.core.Identifier, dialect: Union[str, sqlglot.dialects.Dialect, type[sqlglot.dialects.Dialect], NoneType] = None, is_table: bool = False, normalize: bool | None = True) -> sqlglot.expressions.core.Identifier:
689def normalize_name(
690    identifier: str | exp.Identifier,
691    dialect: DialectType = None,
692    is_table: bool = False,
693    normalize: bool | None = True,
694) -> exp.Identifier:
695    if isinstance(identifier, str):
696        identifier = exp.parse_identifier(identifier, dialect=dialect)
697    elif normalize:
698        identifier = identifier.copy()
699
700    if not normalize:
701        return identifier
702
703    # this is used for normalize_identifier, bigquery has special rules pertaining tables
704    identifier.meta["is_table"] = is_table
705    return Dialect.get_or_raise(dialect).normalize_identifier(identifier)
def ensure_schema( schema: Schema | dict[str, object] | None, **kwargs: typing_extensions.Unpack[sqlglot._typing.SchemaArgs]) -> Schema:
708def ensure_schema(
709    schema: Schema | dict[str, object] | None, **kwargs: Unpack[SchemaArgs]
710) -> Schema:
711    if isinstance(schema, Schema):
712        return schema
713
714    return MappingSchema(schema, **kwargs)
def ensure_column_mapping( mapping: Union[dict[str, Any], str, list[str], NoneType]) -> dict[str, typing.Any]:
717def ensure_column_mapping(mapping: ColumnMapping | None) -> dict[str, t.Any]:
718    if mapping is None:
719        return {}
720    elif isinstance(mapping, dict):
721        return mapping
722    elif isinstance(mapping, str):
723        col_name_type_strs = [x.strip() for x in mapping.split(",")]
724        return {
725            name_type_str.split(":")[0].strip(): name_type_str.split(":")[1].strip()
726            for name_type_str in col_name_type_strs
727        }
728    elif isinstance(mapping, list):
729        return {x.strip(): None for x in mapping}
730
731    raise ValueError(f"Invalid mapping provided: {type(mapping)}")
def flatten_schema( schema: dict[str, object], depth: int | None = None, keys: list[str] | None = None) -> list[list[str]]:
734def flatten_schema(
735    schema: dict[str, object], depth: int | None = None, keys: list[str] | None = None
736) -> list[list[str]]:
737    tables: list[list[str]] = []
738    keys = keys or []
739    depth = dict_depth(schema) - 1 if depth is None else depth
740
741    for k, v in schema.items():
742        if depth == 1 or not isinstance(v, dict):
743            tables.append(keys + [k])
744        elif depth >= 2:
745            tables.extend(flatten_schema(v, depth - 1, keys + [k]))
746
747    return tables
def nested_get( d: dict[str, object], *path: tuple[str, str], raise_on_missing: bool = True) -> Optional[Any]:
750def nested_get(
751    d: dict[str, object], *path: tuple[str, str], raise_on_missing: bool = True
752) -> t.Any | None:
753    """
754    Get a value for a nested dictionary.
755
756    Args:
757        d: the dictionary to search.
758        *path: tuples of (name, key), where:
759            `key` is the key in the dictionary to get.
760            `name` is a string to use in the error if `key` isn't found.
761
762    Returns:
763        The value or None if it doesn't exist.
764    """
765    result: t.Any = d
766    for name, key in path:
767        result = result.get(key)
768        if result is None:
769            if raise_on_missing:
770                name = "table" if name == "this" else name
771                raise ValueError(f"Unknown {name}: {key}")
772            return None
773
774    return result

Get a value for a nested dictionary.

Arguments:
  • d: the dictionary to search.
  • *path: tuples of (name, key), where: key is the key in the dictionary to get. name is a string to use in the error if key isn't found.
Returns:

The value or None if it doesn't exist.

def nested_set( d: dict[str, typing.Any], keys: Sequence[str], value: Any) -> dict[str, typing.Any]:
777def nested_set(d: dict[str, t.Any], keys: Sequence[str], value: t.Any) -> dict[str, t.Any]:
778    """
779    In-place set a value for a nested dictionary
780
781    Example:
782        >>> nested_set({}, ["top_key", "second_key"], "value")
783        {'top_key': {'second_key': 'value'}}
784
785        >>> nested_set({"top_key": {"third_key": "third_value"}}, ["top_key", "second_key"], "value")
786        {'top_key': {'third_key': 'third_value', 'second_key': 'value'}}
787
788    Args:
789        d: dictionary to update.
790        keys: the keys that makeup the path to `value`.
791        value: the value to set in the dictionary for the given key path.
792
793    Returns:
794        The (possibly) updated dictionary.
795    """
796    if not keys:
797        return d
798
799    if len(keys) == 1:
800        d[keys[0]] = value
801        return d
802
803    subd = d
804    for key in keys[:-1]:
805        if key not in subd:
806            subd = subd.setdefault(key, {})
807        else:
808            subd = subd[key]
809
810    subd[keys[-1]] = value
811    return d

In-place set a value for a nested dictionary

Example:
>>> nested_set({}, ["top_key", "second_key"], "value")
{'top_key': {'second_key': 'value'}}
>>> nested_set({"top_key": {"third_key": "third_value"}}, ["top_key", "second_key"], "value")
{'top_key': {'third_key': 'third_value', 'second_key': 'value'}}
Arguments:
  • d: dictionary to update.
  • keys: the keys that makeup the path to value.
  • value: the value to set in the dictionary for the given key path.
Returns:

The (possibly) updated dictionary.