Edit on GitHub

sqlmesh.core.engine_adapter.redshift

  1from __future__ import annotations
  2
  3import logging
  4import typing as t
  5
  6from sqlglot import exp
  7from sqlglot.helper import ensure_list
  8
  9from sqlmesh.core.dialect import to_schema
 10from sqlmesh.core.engine_adapter.base import MERGE_SOURCE_ALIAS, MERGE_TARGET_ALIAS
 11from sqlmesh.core.engine_adapter.base_postgres import BasePostgresEngineAdapter
 12from sqlmesh.core.engine_adapter.mixins import (
 13    GetCurrentCatalogFromFunctionMixin,
 14    NonTransactionalTruncateMixin,
 15    VarcharSizeWorkaroundMixin,
 16    RowDiffMixin,
 17    logical_merge,
 18    GrantsFromInfoSchemaMixin,
 19)
 20from sqlmesh.core.engine_adapter.shared import (
 21    CommentCreationView,
 22    DataObject,
 23    DataObjectType,
 24    SourceQuery,
 25    set_catalog,
 26)
 27from sqlmesh.utils.errors import SQLMeshError
 28
 29if t.TYPE_CHECKING:
 30    import pandas as pd
 31
 32    from sqlmesh.core._typing import SchemaName, TableName
 33    from sqlmesh.core.engine_adapter.base import QueryOrDF, Query
 34    from sqlmesh.core.node import IntervalUnit
 35
 36logger = logging.getLogger(__name__)
 37
 38
 39@set_catalog()
 40class RedshiftEngineAdapter(
 41    BasePostgresEngineAdapter,
 42    GetCurrentCatalogFromFunctionMixin,
 43    NonTransactionalTruncateMixin,
 44    VarcharSizeWorkaroundMixin,
 45    RowDiffMixin,
 46    GrantsFromInfoSchemaMixin,
 47):
 48    DIALECT = "redshift"
 49    CURRENT_CATALOG_EXPRESSION = exp.func("current_database")
 50    # Redshift doesn't support comments for VIEWs WITH NO SCHEMA BINDING (which we always use)
 51    COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
 52    SUPPORTS_REPLACE_TABLE = False
 53    SUPPORTS_GRANTS = True
 54    SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True
 55
 56    SCHEMA_DIFFER_KWARGS = {
 57        "parameterized_type_defaults": {
 58            exp.DataType.build("VARBYTE", dialect=DIALECT).this: [(64000,)],
 59            exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)],
 60            exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)],
 61            exp.DataType.build("VARCHAR", dialect=DIALECT).this: [(256,)],
 62            exp.DataType.build("NCHAR", dialect=DIALECT).this: [(1,)],
 63            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: [(256,)],
 64        },
 65        "max_parameter_length": {
 66            exp.DataType.build("CHAR", dialect=DIALECT).this: 4096,
 67            exp.DataType.build("VARCHAR", dialect=DIALECT).this: 65535,
 68        },
 69        "precision_increase_allowed_types": {exp.DataType.build("VARCHAR", dialect=DIALECT).this},
 70        "drop_cascade": True,
 71    }
 72    VARIABLE_LENGTH_DATA_TYPES = {
 73        "char",
 74        "character",
 75        "nchar",
 76        "varchar",
 77        "character varying",
 78        "nvarchar",
 79        "varbyte",
 80        "varbinary",
 81        "binary varying",
 82    }
 83
 84    def columns(
 85        self,
 86        table_name: TableName,
 87        include_pseudo_columns: bool = True,
 88    ) -> t.Dict[str, exp.DataType]:
 89        table = exp.to_table(table_name)
 90
 91        sql = (
 92            exp.select(
 93                "column_name",
 94                "data_type",
 95                "character_maximum_length",
 96                "numeric_precision",
 97                "numeric_scale",
 98            )
 99            .from_("svv_columns")  # Includes late-binding views
100            .where(exp.column("table_name").eq(table.alias_or_name))
101        )
102        if table.args.get("db"):
103            sql = sql.where(exp.column("table_schema").eq(table.args["db"].name))
104
105        columns_raw = self.fetchall(sql, quote_identifiers=True)
106
107        def build_var_length_col(
108            column_name: str,
109            data_type: str,
110            character_maximum_length: t.Optional[int] = None,
111            numeric_precision: t.Optional[int] = None,
112            numeric_scale: t.Optional[int] = None,
113        ) -> tuple:
114            data_type = data_type.lower()
115            if (
116                data_type in self.VARIABLE_LENGTH_DATA_TYPES
117                and character_maximum_length is not None
118            ):
119                return (column_name, f"{data_type}({character_maximum_length})")
120            if data_type in ("decimal", "numeric"):
121                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
122
123            return (column_name, data_type)
124
125        columns = [build_var_length_col(*row) for row in columns_raw]
126
127        return {
128            column_name: exp.DataType.build(data_type, dialect=self.dialect)
129            for column_name, data_type in columns
130        }
131
132    @property
133    def enable_merge(self) -> bool:
134        # Redshift supports the MERGE operation but we use the logical merge
135        # unless the user has opted in by setting enable_merge in the connection.
136        return bool(self._extra_config.get("enable_merge"))
137
138    @property
139    def cursor(self) -> t.Any:
140        # Redshift by default uses a `format` paramstyle that has issues when we try to write our snapshot
141        # data to snapshot table. There doesn't seem to be a way to disable parameter overriding so we just
142        # set it to `qmark` since that doesn't cause issues.
143        cursor = self._connection_pool.get_cursor()
144        cursor.paramstyle = "qmark"
145        return cursor
146
147    def _fetch_native_df(
148        self, query: t.Union[exp.Expr, str], quote_identifiers: bool = False
149    ) -> pd.DataFrame:
150        """Fetches a Pandas DataFrame from the cursor"""
151        import pandas as pd
152
153        self.execute(query, quote_identifiers=quote_identifiers)
154
155        # We manually build the `DataFrame` here because the driver's `fetch_dataframe`
156        # method does not respect the active case-sensitivity configuration.
157        #
158        # Context: https://github.com/aws/amazon-redshift-python-driver/issues/238
159        fetcheddata = self.cursor.fetchall()
160
161        try:
162            columns = [column[0] for column in self.cursor.description]
163        except Exception:
164            columns = None
165            logging.warning(
166                "No row description was found, pandas dataframe will be missing column labels."
167            )
168
169        result = [tuple(row) for row in fetcheddata]
170        return pd.DataFrame(result, columns=columns)
171
172    def _create_table_from_source_queries(
173        self,
174        table_name: TableName,
175        source_queries: t.List[SourceQuery],
176        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
177        exists: bool = True,
178        replace: bool = False,
179        table_description: t.Optional[str] = None,
180        column_descriptions: t.Optional[t.Dict[str, str]] = None,
181        table_kind: t.Optional[str] = None,
182        track_rows_processed: bool = True,
183        **kwargs: t.Any,
184    ) -> None:
185        """
186        Redshift doesn't support `CREATE TABLE IF NOT EXISTS AS...` but does support `CREATE TABLE AS...` so
187        we check if the exists check exists and if not then we can use the base implementation. Otherwise we
188        manually check if it exists and if it does then this is a no-op anyways so we return and if it doesn't
189        then we run the query with exists set to False since we just confirmed it doesn't exist.
190        """
191        if not exists:
192            return super()._create_table_from_source_queries(
193                table_name,
194                source_queries,
195                target_columns_to_types,
196                exists,
197                table_description=table_description,
198                column_descriptions=column_descriptions,
199                **kwargs,
200            )
201        if self.table_exists(table_name):
202            return
203        super()._create_table_from_source_queries(
204            table_name,
205            source_queries,
206            exists=False,
207            table_description=table_description,
208            column_descriptions=column_descriptions,
209            **kwargs,
210        )
211
212    def create_view(
213        self,
214        view_name: TableName,
215        query_or_df: QueryOrDF,
216        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
217        replace: bool = True,
218        materialized: bool = False,
219        materialized_properties: t.Optional[t.Dict[str, t.Any]] = None,
220        table_description: t.Optional[str] = None,
221        column_descriptions: t.Optional[t.Dict[str, str]] = None,
222        view_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
223        source_columns: t.Optional[t.List[str]] = None,
224        **create_kwargs: t.Any,
225    ) -> None:
226        """
227        Redshift views are "binding" by default to their underlying table which means you can't drop that
228        underlying table without dropping the view first. This is a problem for us since we want to be able to
229        swap tables out from under views. Therefore, we create the view as non-binding.
230        """
231        no_schema_binding = True
232        if isinstance(query_or_df, exp.Expr):
233            # We can't include NO SCHEMA BINDING if the query has a recursive CTE
234            has_recursive_cte = any(
235                w.args.get("recursive", False) for w in query_or_df.find_all(exp.With)
236            )
237            no_schema_binding = not has_recursive_cte
238
239        return super().create_view(
240            view_name,
241            query_or_df,
242            target_columns_to_types,
243            replace,
244            materialized,
245            materialized_properties,
246            table_description=table_description,
247            column_descriptions=column_descriptions,
248            no_schema_binding=no_schema_binding,
249            view_properties=view_properties,
250            source_columns=source_columns,
251            **create_kwargs,
252        )
253
254    def _build_table_properties_exp(
255        self,
256        catalog_name: t.Optional[str] = None,
257        table_format: t.Optional[str] = None,
258        storage_format: t.Optional[str] = None,
259        partitioned_by: t.Optional[t.List[exp.Expr]] = None,
260        partition_interval_unit: t.Optional[IntervalUnit] = None,
261        clustered_by: t.Optional[t.List[exp.Expr]] = None,
262        table_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
263        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
264        table_description: t.Optional[str] = None,
265        table_kind: t.Optional[str] = None,
266        **kwargs: t.Any,
267    ) -> t.Optional[exp.Properties]:
268        properties: t.List[exp.Expr] = []
269
270        if table_description:
271            properties.append(
272                exp.SchemaCommentProperty(
273                    this=exp.Literal.string(self._truncate_table_comment(table_description))
274                )
275            )
276
277        def _to_identifier_if_string(expression: exp.Expr) -> exp.Expr:
278            if isinstance(expression, exp.Literal) and expression.is_string:
279                return exp.to_identifier(expression.this)
280            return expression.copy()
281
282        if table_properties:
283            table_properties = {k.upper(): v for k, v in table_properties.items()}
284
285            table_type = self._pop_creatable_type_from_properties(table_properties)
286            properties.extend(ensure_list(table_type))
287
288            diststyle = table_properties.get("DISTSTYLE")
289            if diststyle:
290                properties.append(exp.DistStyleProperty(this=exp.var(diststyle.name.upper())))
291
292            distkey = table_properties.get("DISTKEY")
293            if distkey:
294                properties.append(exp.DistKeyProperty(this=_to_identifier_if_string(distkey)))
295
296            sortkey = table_properties.get("SORTKEY")
297            if sortkey:
298                sortkey_expressions = sortkey.expressions if sortkey.expressions else [sortkey]
299                properties.append(
300                    exp.SortKeyProperty(
301                        this=[
302                            _to_identifier_if_string(expression)
303                            for expression in sortkey_expressions
304                        ],
305                        compound=False,
306                    )
307                )
308
309        return exp.Properties(expressions=properties) if properties else None
310
311    def replace_query(
312        self,
313        table_name: TableName,
314        query_or_df: QueryOrDF,
315        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
316        table_description: t.Optional[str] = None,
317        column_descriptions: t.Optional[t.Dict[str, str]] = None,
318        source_columns: t.Optional[t.List[str]] = None,
319        supports_replace_table_override: t.Optional[bool] = None,
320        **kwargs: t.Any,
321    ) -> None:
322        """
323        Redshift doesn't support `CREATE OR REPLACE TABLE...` and it also doesn't support `VALUES` expression so we need to specially
324        handle DataFrame replacements.
325
326        If the table doesn't exist then we just create it and load it with insert statements
327        If it does exist then we need to do the:
328            `CREATE TABLE...`, `INSERT INTO...`, `RENAME TABLE...`, `RENAME TABLE...`, DROP TABLE...`  dance.
329        """
330        import pandas as pd
331
332        target_data_object = self.get_data_object(table_name)
333        table_exists = target_data_object is not None
334        if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE):
335            table_exists = False
336
337        if not isinstance(query_or_df, pd.DataFrame) or not table_exists:
338            return super().replace_query(
339                table_name,
340                query_or_df,
341                target_columns_to_types,
342                table_description,
343                column_descriptions,
344                source_columns=source_columns,
345                **kwargs,
346            )
347        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
348            query_or_df,
349            target_columns_to_types,
350            target_table=table_name,
351            source_columns=source_columns,
352        )
353        target_columns_to_types = target_columns_to_types or self.columns(table_name)
354        target_table = exp.to_table(table_name)
355        with self.transaction():
356            temp_table = self._get_temp_table(target_table)
357            old_table = self._get_temp_table(target_table)
358            self.create_table(
359                temp_table,
360                target_columns_to_types,
361                exists=False,
362                table_description=table_description,
363                column_descriptions=column_descriptions,
364                **kwargs,
365            )
366            self._insert_append_source_queries(temp_table, source_queries, target_columns_to_types)
367            self.rename_table(target_table, old_table)
368            self.rename_table(temp_table, target_table)
369            self.drop_table(old_table)
370
371    def _get_data_objects(
372        self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
373    ) -> t.List[DataObject]:
374        """
375        Returns all the data objects that exist in the given schema and optionally catalog.
376        """
377        catalog = self.get_current_catalog()
378        table_query = exp.select(
379            exp.column("schemaname").as_("schema_name"),
380            exp.column("tablename").as_("name"),
381            exp.Literal.string("TABLE").as_("type"),
382        ).from_("pg_tables")
383        view_query = (
384            exp.select(
385                exp.column("schemaname").as_("schema_name"),
386                exp.column("viewname").as_("name"),
387                exp.Literal.string("VIEW").as_("type"),
388            )
389            .from_("pg_views")
390            .where(exp.column("definition").ilike("%create materialized view%").not_())
391        )
392        materialized_view_query = (
393            exp.select(
394                exp.column("schemaname").as_("schema_name"),
395                exp.column("viewname").as_("name"),
396                exp.Literal.string("MATERIALIZED_VIEW").as_("type"),
397            )
398            .from_("pg_views")
399            .where(exp.column("definition").ilike("%create materialized view%"))
400        )
401        subquery = exp.union(
402            table_query,
403            exp.union(view_query, materialized_view_query, distinct=False),
404            distinct=False,
405        )
406        query = (
407            exp.select("*")
408            .from_(subquery.subquery(alias="objs"))
409            .where(exp.column("schema_name").eq(to_schema(schema_name).db))
410        )
411        if object_names:
412            query = query.where(exp.column("name").isin(*object_names))
413        df = self.fetchdf(query)
414        return [
415            DataObject(
416                catalog=catalog,
417                schema=row.schema_name,
418                name=row.name,
419                type=DataObjectType.from_str(row.type),  # type: ignore
420            )
421            for row in df.itertuples()
422        ]
423
424    def merge(
425        self,
426        target_table: TableName,
427        source_table: QueryOrDF,
428        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
429        unique_key: t.Sequence[exp.Expr],
430        when_matched: t.Optional[exp.Whens] = None,
431        merge_filter: t.Optional[exp.Expr] = None,
432        source_columns: t.Optional[t.List[str]] = None,
433        **kwargs: t.Any,
434    ) -> None:
435        if self.enable_merge:
436            # By default we use the logical merge unless the user has opted in
437            super().merge(
438                target_table=target_table,
439                source_table=source_table,
440                target_columns_to_types=target_columns_to_types,
441                unique_key=unique_key,
442                when_matched=when_matched,
443                merge_filter=merge_filter,
444                source_columns=source_columns,
445            )
446        else:
447            logical_merge(
448                self,
449                target_table,
450                source_table,
451                target_columns_to_types,
452                unique_key,
453                when_matched=when_matched,
454                merge_filter=merge_filter,
455                source_columns=source_columns,
456            )
457
458    def _merge(
459        self,
460        target_table: TableName,
461        query: Query,
462        on: exp.Expr,
463        whens: exp.Whens,
464    ) -> None:
465        # Redshift does not support table aliases in the target table of a MERGE statement.
466        # So we must use the actual table name instead of an alias, as we do with the source table.
467        def resolve_target_table(expression: exp.Expr) -> exp.Expr:
468            if (
469                isinstance(expression, exp.Column)
470                and expression.table.upper() == MERGE_TARGET_ALIAS
471            ):
472                expression.set("table", exp.to_table(target_table))
473            return expression
474
475        # Ensure that there is exactly one "WHEN MATCHED" and one "WHEN NOT MATCHED" clause.
476        # Since Redshift does not support multiple "WHEN MATCHED" clauses.
477        if (
478            len(whens.expressions) != 2
479            or whens.expressions[0].args["matched"] == whens.expressions[1].args["matched"]
480        ):
481            raise SQLMeshError(
482                "Redshift only supports a single WHEN MATCHED and WHEN NOT MATCHED clause"
483            )
484
485        using = exp.alias_(
486            exp.Subquery(this=query), alias=MERGE_SOURCE_ALIAS, copy=False, table=True
487        )
488        self.execute(
489            exp.Merge(
490                this=target_table,
491                using=using,
492                on=on.transform(resolve_target_table),
493                whens=whens.transform(resolve_target_table),
494            ),
495            track_rows_processed=True,
496        )
497
498    def _normalize_decimal_value(self, expr: exp.Expr, precision: int) -> exp.Expr:
499        # Redshift is finicky. It truncates when the data is already in a table, but rounds when the data is generated as part of a SELECT.
500        #
501        # The following works:
502        #  > select cast(cast(3.14159 as decimal(6, 5)) as decimal(6, 3)); --produces '3.142', the value we want / what every other database produces
503        #
504        # However, if you write that to a table, and then cast it to a less precise decimal, you get _truncation_.
505        #  > create table foo (val decimal(6, 5)); insert into foo(val) values (3.14159);
506        #  > select cast(val as decimal(6, 3)) from foo; --produces '3.141'
507        #
508        # So to make up for this, we force it to round by injecting a round() expression
509        rounded = exp.func("ROUND", expr, precision)
510
511        return super()._normalize_decimal_value(rounded, precision)
logger = <Logger sqlmesh.core.engine_adapter.redshift (WARNING)>
 40@set_catalog()
 41class RedshiftEngineAdapter(
 42    BasePostgresEngineAdapter,
 43    GetCurrentCatalogFromFunctionMixin,
 44    NonTransactionalTruncateMixin,
 45    VarcharSizeWorkaroundMixin,
 46    RowDiffMixin,
 47    GrantsFromInfoSchemaMixin,
 48):
 49    DIALECT = "redshift"
 50    CURRENT_CATALOG_EXPRESSION = exp.func("current_database")
 51    # Redshift doesn't support comments for VIEWs WITH NO SCHEMA BINDING (which we always use)
 52    COMMENT_CREATION_VIEW = CommentCreationView.UNSUPPORTED
 53    SUPPORTS_REPLACE_TABLE = False
 54    SUPPORTS_GRANTS = True
 55    SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True
 56
 57    SCHEMA_DIFFER_KWARGS = {
 58        "parameterized_type_defaults": {
 59            exp.DataType.build("VARBYTE", dialect=DIALECT).this: [(64000,)],
 60            exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 0), (0,)],
 61            exp.DataType.build("CHAR", dialect=DIALECT).this: [(1,)],
 62            exp.DataType.build("VARCHAR", dialect=DIALECT).this: [(256,)],
 63            exp.DataType.build("NCHAR", dialect=DIALECT).this: [(1,)],
 64            exp.DataType.build("NVARCHAR", dialect=DIALECT).this: [(256,)],
 65        },
 66        "max_parameter_length": {
 67            exp.DataType.build("CHAR", dialect=DIALECT).this: 4096,
 68            exp.DataType.build("VARCHAR", dialect=DIALECT).this: 65535,
 69        },
 70        "precision_increase_allowed_types": {exp.DataType.build("VARCHAR", dialect=DIALECT).this},
 71        "drop_cascade": True,
 72    }
 73    VARIABLE_LENGTH_DATA_TYPES = {
 74        "char",
 75        "character",
 76        "nchar",
 77        "varchar",
 78        "character varying",
 79        "nvarchar",
 80        "varbyte",
 81        "varbinary",
 82        "binary varying",
 83    }
 84
 85    def columns(
 86        self,
 87        table_name: TableName,
 88        include_pseudo_columns: bool = True,
 89    ) -> t.Dict[str, exp.DataType]:
 90        table = exp.to_table(table_name)
 91
 92        sql = (
 93            exp.select(
 94                "column_name",
 95                "data_type",
 96                "character_maximum_length",
 97                "numeric_precision",
 98                "numeric_scale",
 99            )
100            .from_("svv_columns")  # Includes late-binding views
101            .where(exp.column("table_name").eq(table.alias_or_name))
102        )
103        if table.args.get("db"):
104            sql = sql.where(exp.column("table_schema").eq(table.args["db"].name))
105
106        columns_raw = self.fetchall(sql, quote_identifiers=True)
107
108        def build_var_length_col(
109            column_name: str,
110            data_type: str,
111            character_maximum_length: t.Optional[int] = None,
112            numeric_precision: t.Optional[int] = None,
113            numeric_scale: t.Optional[int] = None,
114        ) -> tuple:
115            data_type = data_type.lower()
116            if (
117                data_type in self.VARIABLE_LENGTH_DATA_TYPES
118                and character_maximum_length is not None
119            ):
120                return (column_name, f"{data_type}({character_maximum_length})")
121            if data_type in ("decimal", "numeric"):
122                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
123
124            return (column_name, data_type)
125
126        columns = [build_var_length_col(*row) for row in columns_raw]
127
128        return {
129            column_name: exp.DataType.build(data_type, dialect=self.dialect)
130            for column_name, data_type in columns
131        }
132
133    @property
134    def enable_merge(self) -> bool:
135        # Redshift supports the MERGE operation but we use the logical merge
136        # unless the user has opted in by setting enable_merge in the connection.
137        return bool(self._extra_config.get("enable_merge"))
138
139    @property
140    def cursor(self) -> t.Any:
141        # Redshift by default uses a `format` paramstyle that has issues when we try to write our snapshot
142        # data to snapshot table. There doesn't seem to be a way to disable parameter overriding so we just
143        # set it to `qmark` since that doesn't cause issues.
144        cursor = self._connection_pool.get_cursor()
145        cursor.paramstyle = "qmark"
146        return cursor
147
148    def _fetch_native_df(
149        self, query: t.Union[exp.Expr, str], quote_identifiers: bool = False
150    ) -> pd.DataFrame:
151        """Fetches a Pandas DataFrame from the cursor"""
152        import pandas as pd
153
154        self.execute(query, quote_identifiers=quote_identifiers)
155
156        # We manually build the `DataFrame` here because the driver's `fetch_dataframe`
157        # method does not respect the active case-sensitivity configuration.
158        #
159        # Context: https://github.com/aws/amazon-redshift-python-driver/issues/238
160        fetcheddata = self.cursor.fetchall()
161
162        try:
163            columns = [column[0] for column in self.cursor.description]
164        except Exception:
165            columns = None
166            logging.warning(
167                "No row description was found, pandas dataframe will be missing column labels."
168            )
169
170        result = [tuple(row) for row in fetcheddata]
171        return pd.DataFrame(result, columns=columns)
172
173    def _create_table_from_source_queries(
174        self,
175        table_name: TableName,
176        source_queries: t.List[SourceQuery],
177        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
178        exists: bool = True,
179        replace: bool = False,
180        table_description: t.Optional[str] = None,
181        column_descriptions: t.Optional[t.Dict[str, str]] = None,
182        table_kind: t.Optional[str] = None,
183        track_rows_processed: bool = True,
184        **kwargs: t.Any,
185    ) -> None:
186        """
187        Redshift doesn't support `CREATE TABLE IF NOT EXISTS AS...` but does support `CREATE TABLE AS...` so
188        we check if the exists check exists and if not then we can use the base implementation. Otherwise we
189        manually check if it exists and if it does then this is a no-op anyways so we return and if it doesn't
190        then we run the query with exists set to False since we just confirmed it doesn't exist.
191        """
192        if not exists:
193            return super()._create_table_from_source_queries(
194                table_name,
195                source_queries,
196                target_columns_to_types,
197                exists,
198                table_description=table_description,
199                column_descriptions=column_descriptions,
200                **kwargs,
201            )
202        if self.table_exists(table_name):
203            return
204        super()._create_table_from_source_queries(
205            table_name,
206            source_queries,
207            exists=False,
208            table_description=table_description,
209            column_descriptions=column_descriptions,
210            **kwargs,
211        )
212
213    def create_view(
214        self,
215        view_name: TableName,
216        query_or_df: QueryOrDF,
217        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
218        replace: bool = True,
219        materialized: bool = False,
220        materialized_properties: t.Optional[t.Dict[str, t.Any]] = None,
221        table_description: t.Optional[str] = None,
222        column_descriptions: t.Optional[t.Dict[str, str]] = None,
223        view_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
224        source_columns: t.Optional[t.List[str]] = None,
225        **create_kwargs: t.Any,
226    ) -> None:
227        """
228        Redshift views are "binding" by default to their underlying table which means you can't drop that
229        underlying table without dropping the view first. This is a problem for us since we want to be able to
230        swap tables out from under views. Therefore, we create the view as non-binding.
231        """
232        no_schema_binding = True
233        if isinstance(query_or_df, exp.Expr):
234            # We can't include NO SCHEMA BINDING if the query has a recursive CTE
235            has_recursive_cte = any(
236                w.args.get("recursive", False) for w in query_or_df.find_all(exp.With)
237            )
238            no_schema_binding = not has_recursive_cte
239
240        return super().create_view(
241            view_name,
242            query_or_df,
243            target_columns_to_types,
244            replace,
245            materialized,
246            materialized_properties,
247            table_description=table_description,
248            column_descriptions=column_descriptions,
249            no_schema_binding=no_schema_binding,
250            view_properties=view_properties,
251            source_columns=source_columns,
252            **create_kwargs,
253        )
254
255    def _build_table_properties_exp(
256        self,
257        catalog_name: t.Optional[str] = None,
258        table_format: t.Optional[str] = None,
259        storage_format: t.Optional[str] = None,
260        partitioned_by: t.Optional[t.List[exp.Expr]] = None,
261        partition_interval_unit: t.Optional[IntervalUnit] = None,
262        clustered_by: t.Optional[t.List[exp.Expr]] = None,
263        table_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
264        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
265        table_description: t.Optional[str] = None,
266        table_kind: t.Optional[str] = None,
267        **kwargs: t.Any,
268    ) -> t.Optional[exp.Properties]:
269        properties: t.List[exp.Expr] = []
270
271        if table_description:
272            properties.append(
273                exp.SchemaCommentProperty(
274                    this=exp.Literal.string(self._truncate_table_comment(table_description))
275                )
276            )
277
278        def _to_identifier_if_string(expression: exp.Expr) -> exp.Expr:
279            if isinstance(expression, exp.Literal) and expression.is_string:
280                return exp.to_identifier(expression.this)
281            return expression.copy()
282
283        if table_properties:
284            table_properties = {k.upper(): v for k, v in table_properties.items()}
285
286            table_type = self._pop_creatable_type_from_properties(table_properties)
287            properties.extend(ensure_list(table_type))
288
289            diststyle = table_properties.get("DISTSTYLE")
290            if diststyle:
291                properties.append(exp.DistStyleProperty(this=exp.var(diststyle.name.upper())))
292
293            distkey = table_properties.get("DISTKEY")
294            if distkey:
295                properties.append(exp.DistKeyProperty(this=_to_identifier_if_string(distkey)))
296
297            sortkey = table_properties.get("SORTKEY")
298            if sortkey:
299                sortkey_expressions = sortkey.expressions if sortkey.expressions else [sortkey]
300                properties.append(
301                    exp.SortKeyProperty(
302                        this=[
303                            _to_identifier_if_string(expression)
304                            for expression in sortkey_expressions
305                        ],
306                        compound=False,
307                    )
308                )
309
310        return exp.Properties(expressions=properties) if properties else None
311
312    def replace_query(
313        self,
314        table_name: TableName,
315        query_or_df: QueryOrDF,
316        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
317        table_description: t.Optional[str] = None,
318        column_descriptions: t.Optional[t.Dict[str, str]] = None,
319        source_columns: t.Optional[t.List[str]] = None,
320        supports_replace_table_override: t.Optional[bool] = None,
321        **kwargs: t.Any,
322    ) -> None:
323        """
324        Redshift doesn't support `CREATE OR REPLACE TABLE...` and it also doesn't support `VALUES` expression so we need to specially
325        handle DataFrame replacements.
326
327        If the table doesn't exist then we just create it and load it with insert statements
328        If it does exist then we need to do the:
329            `CREATE TABLE...`, `INSERT INTO...`, `RENAME TABLE...`, `RENAME TABLE...`, DROP TABLE...`  dance.
330        """
331        import pandas as pd
332
333        target_data_object = self.get_data_object(table_name)
334        table_exists = target_data_object is not None
335        if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE):
336            table_exists = False
337
338        if not isinstance(query_or_df, pd.DataFrame) or not table_exists:
339            return super().replace_query(
340                table_name,
341                query_or_df,
342                target_columns_to_types,
343                table_description,
344                column_descriptions,
345                source_columns=source_columns,
346                **kwargs,
347            )
348        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
349            query_or_df,
350            target_columns_to_types,
351            target_table=table_name,
352            source_columns=source_columns,
353        )
354        target_columns_to_types = target_columns_to_types or self.columns(table_name)
355        target_table = exp.to_table(table_name)
356        with self.transaction():
357            temp_table = self._get_temp_table(target_table)
358            old_table = self._get_temp_table(target_table)
359            self.create_table(
360                temp_table,
361                target_columns_to_types,
362                exists=False,
363                table_description=table_description,
364                column_descriptions=column_descriptions,
365                **kwargs,
366            )
367            self._insert_append_source_queries(temp_table, source_queries, target_columns_to_types)
368            self.rename_table(target_table, old_table)
369            self.rename_table(temp_table, target_table)
370            self.drop_table(old_table)
371
372    def _get_data_objects(
373        self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
374    ) -> t.List[DataObject]:
375        """
376        Returns all the data objects that exist in the given schema and optionally catalog.
377        """
378        catalog = self.get_current_catalog()
379        table_query = exp.select(
380            exp.column("schemaname").as_("schema_name"),
381            exp.column("tablename").as_("name"),
382            exp.Literal.string("TABLE").as_("type"),
383        ).from_("pg_tables")
384        view_query = (
385            exp.select(
386                exp.column("schemaname").as_("schema_name"),
387                exp.column("viewname").as_("name"),
388                exp.Literal.string("VIEW").as_("type"),
389            )
390            .from_("pg_views")
391            .where(exp.column("definition").ilike("%create materialized view%").not_())
392        )
393        materialized_view_query = (
394            exp.select(
395                exp.column("schemaname").as_("schema_name"),
396                exp.column("viewname").as_("name"),
397                exp.Literal.string("MATERIALIZED_VIEW").as_("type"),
398            )
399            .from_("pg_views")
400            .where(exp.column("definition").ilike("%create materialized view%"))
401        )
402        subquery = exp.union(
403            table_query,
404            exp.union(view_query, materialized_view_query, distinct=False),
405            distinct=False,
406        )
407        query = (
408            exp.select("*")
409            .from_(subquery.subquery(alias="objs"))
410            .where(exp.column("schema_name").eq(to_schema(schema_name).db))
411        )
412        if object_names:
413            query = query.where(exp.column("name").isin(*object_names))
414        df = self.fetchdf(query)
415        return [
416            DataObject(
417                catalog=catalog,
418                schema=row.schema_name,
419                name=row.name,
420                type=DataObjectType.from_str(row.type),  # type: ignore
421            )
422            for row in df.itertuples()
423        ]
424
425    def merge(
426        self,
427        target_table: TableName,
428        source_table: QueryOrDF,
429        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
430        unique_key: t.Sequence[exp.Expr],
431        when_matched: t.Optional[exp.Whens] = None,
432        merge_filter: t.Optional[exp.Expr] = None,
433        source_columns: t.Optional[t.List[str]] = None,
434        **kwargs: t.Any,
435    ) -> None:
436        if self.enable_merge:
437            # By default we use the logical merge unless the user has opted in
438            super().merge(
439                target_table=target_table,
440                source_table=source_table,
441                target_columns_to_types=target_columns_to_types,
442                unique_key=unique_key,
443                when_matched=when_matched,
444                merge_filter=merge_filter,
445                source_columns=source_columns,
446            )
447        else:
448            logical_merge(
449                self,
450                target_table,
451                source_table,
452                target_columns_to_types,
453                unique_key,
454                when_matched=when_matched,
455                merge_filter=merge_filter,
456                source_columns=source_columns,
457            )
458
459    def _merge(
460        self,
461        target_table: TableName,
462        query: Query,
463        on: exp.Expr,
464        whens: exp.Whens,
465    ) -> None:
466        # Redshift does not support table aliases in the target table of a MERGE statement.
467        # So we must use the actual table name instead of an alias, as we do with the source table.
468        def resolve_target_table(expression: exp.Expr) -> exp.Expr:
469            if (
470                isinstance(expression, exp.Column)
471                and expression.table.upper() == MERGE_TARGET_ALIAS
472            ):
473                expression.set("table", exp.to_table(target_table))
474            return expression
475
476        # Ensure that there is exactly one "WHEN MATCHED" and one "WHEN NOT MATCHED" clause.
477        # Since Redshift does not support multiple "WHEN MATCHED" clauses.
478        if (
479            len(whens.expressions) != 2
480            or whens.expressions[0].args["matched"] == whens.expressions[1].args["matched"]
481        ):
482            raise SQLMeshError(
483                "Redshift only supports a single WHEN MATCHED and WHEN NOT MATCHED clause"
484            )
485
486        using = exp.alias_(
487            exp.Subquery(this=query), alias=MERGE_SOURCE_ALIAS, copy=False, table=True
488        )
489        self.execute(
490            exp.Merge(
491                this=target_table,
492                using=using,
493                on=on.transform(resolve_target_table),
494                whens=whens.transform(resolve_target_table),
495            ),
496            track_rows_processed=True,
497        )
498
499    def _normalize_decimal_value(self, expr: exp.Expr, precision: int) -> exp.Expr:
500        # Redshift is finicky. It truncates when the data is already in a table, but rounds when the data is generated as part of a SELECT.
501        #
502        # The following works:
503        #  > select cast(cast(3.14159 as decimal(6, 5)) as decimal(6, 3)); --produces '3.142', the value we want / what every other database produces
504        #
505        # However, if you write that to a table, and then cast it to a less precise decimal, you get _truncation_.
506        #  > create table foo (val decimal(6, 5)); insert into foo(val) values (3.14159);
507        #  > select cast(val as decimal(6, 3)) from foo; --produces '3.141'
508        #
509        # So to make up for this, we force it to round by injecting a round() expression
510        rounded = exp.func("ROUND", expr, precision)
511
512        return super()._normalize_decimal_value(rounded, precision)

Base class wrapping a Database API compliant connection.

The EngineAdapter is an easily-subclassable interface that interacts with the underlying engine and data store.

Arguments:
  • connection_factory_or_pool: a callable which produces a new Database API-compliant connection on every call.
  • dialect: The dialect with which this adapter is associated.
  • multithreaded: Indicates whether this adapter will be used by more than one thread.
DIALECT = 'redshift'
CURRENT_CATALOG_EXPRESSION = CurrentDatabase()
COMMENT_CREATION_VIEW = <CommentCreationView.UNSUPPORTED: 1>
SUPPORTS_REPLACE_TABLE = False
SUPPORTS_GRANTS = True
SUPPORTS_MULTIPLE_GRANT_PRINCIPALS = True
SCHEMA_DIFFER_KWARGS = {'parameterized_type_defaults': {<DType.VARBINARY: 'VARBINARY'>: [(64000,)], <DType.DECIMAL: 'DECIMAL'>: [(18, 0), (0,)], <DType.CHAR: 'CHAR'>: [(1,)], <DType.VARCHAR: 'VARCHAR'>: [(256,)], <DType.NCHAR: 'NCHAR'>: [(1,)], <DType.NVARCHAR: 'NVARCHAR'>: [(256,)]}, 'max_parameter_length': {<DType.CHAR: 'CHAR'>: 4096, <DType.VARCHAR: 'VARCHAR'>: 65535}, 'precision_increase_allowed_types': {<DType.VARCHAR: 'VARCHAR'>}, 'drop_cascade': True}
VARIABLE_LENGTH_DATA_TYPES = {'binary varying', 'char', 'varbyte', 'nchar', 'nvarchar', 'character', 'varbinary', 'varchar', 'character varying'}
def columns( self, table_name: Union[str, sqlglot.expressions.query.Table], include_pseudo_columns: bool = True) -> Dict[str, sqlglot.expressions.datatypes.DataType]:
 85    def columns(
 86        self,
 87        table_name: TableName,
 88        include_pseudo_columns: bool = True,
 89    ) -> t.Dict[str, exp.DataType]:
 90        table = exp.to_table(table_name)
 91
 92        sql = (
 93            exp.select(
 94                "column_name",
 95                "data_type",
 96                "character_maximum_length",
 97                "numeric_precision",
 98                "numeric_scale",
 99            )
100            .from_("svv_columns")  # Includes late-binding views
101            .where(exp.column("table_name").eq(table.alias_or_name))
102        )
103        if table.args.get("db"):
104            sql = sql.where(exp.column("table_schema").eq(table.args["db"].name))
105
106        columns_raw = self.fetchall(sql, quote_identifiers=True)
107
108        def build_var_length_col(
109            column_name: str,
110            data_type: str,
111            character_maximum_length: t.Optional[int] = None,
112            numeric_precision: t.Optional[int] = None,
113            numeric_scale: t.Optional[int] = None,
114        ) -> tuple:
115            data_type = data_type.lower()
116            if (
117                data_type in self.VARIABLE_LENGTH_DATA_TYPES
118                and character_maximum_length is not None
119            ):
120                return (column_name, f"{data_type}({character_maximum_length})")
121            if data_type in ("decimal", "numeric"):
122                return (column_name, f"{data_type}({numeric_precision}, {numeric_scale})")
123
124            return (column_name, data_type)
125
126        columns = [build_var_length_col(*row) for row in columns_raw]
127
128        return {
129            column_name: exp.DataType.build(data_type, dialect=self.dialect)
130            for column_name, data_type in columns
131        }

Fetches column names and types for the target table.

enable_merge: bool
133    @property
134    def enable_merge(self) -> bool:
135        # Redshift supports the MERGE operation but we use the logical merge
136        # unless the user has opted in by setting enable_merge in the connection.
137        return bool(self._extra_config.get("enable_merge"))
cursor: Any
139    @property
140    def cursor(self) -> t.Any:
141        # Redshift by default uses a `format` paramstyle that has issues when we try to write our snapshot
142        # data to snapshot table. There doesn't seem to be a way to disable parameter overriding so we just
143        # set it to `qmark` since that doesn't cause issues.
144        cursor = self._connection_pool.get_cursor()
145        cursor.paramstyle = "qmark"
146        return cursor
def create_view( self, view_name: Union[str, sqlglot.expressions.query.Table], query_or_df: <MagicMock id='130969804860832'>, target_columns_to_types: Optional[Dict[str, sqlglot.expressions.datatypes.DataType]] = None, replace: bool = True, materialized: bool = False, materialized_properties: Optional[Dict[str, Any]] = None, table_description: Optional[str] = None, column_descriptions: Optional[Dict[str, str]] = None, view_properties: Optional[Dict[str, sqlglot.expressions.core.Expr]] = None, source_columns: Optional[List[str]] = None, **create_kwargs: Any) -> None:
213    def create_view(
214        self,
215        view_name: TableName,
216        query_or_df: QueryOrDF,
217        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
218        replace: bool = True,
219        materialized: bool = False,
220        materialized_properties: t.Optional[t.Dict[str, t.Any]] = None,
221        table_description: t.Optional[str] = None,
222        column_descriptions: t.Optional[t.Dict[str, str]] = None,
223        view_properties: t.Optional[t.Dict[str, exp.Expr]] = None,
224        source_columns: t.Optional[t.List[str]] = None,
225        **create_kwargs: t.Any,
226    ) -> None:
227        """
228        Redshift views are "binding" by default to their underlying table which means you can't drop that
229        underlying table without dropping the view first. This is a problem for us since we want to be able to
230        swap tables out from under views. Therefore, we create the view as non-binding.
231        """
232        no_schema_binding = True
233        if isinstance(query_or_df, exp.Expr):
234            # We can't include NO SCHEMA BINDING if the query has a recursive CTE
235            has_recursive_cte = any(
236                w.args.get("recursive", False) for w in query_or_df.find_all(exp.With)
237            )
238            no_schema_binding = not has_recursive_cte
239
240        return super().create_view(
241            view_name,
242            query_or_df,
243            target_columns_to_types,
244            replace,
245            materialized,
246            materialized_properties,
247            table_description=table_description,
248            column_descriptions=column_descriptions,
249            no_schema_binding=no_schema_binding,
250            view_properties=view_properties,
251            source_columns=source_columns,
252            **create_kwargs,
253        )

Redshift views are "binding" by default to their underlying table which means you can't drop that underlying table without dropping the view first. This is a problem for us since we want to be able to swap tables out from under views. Therefore, we create the view as non-binding.

def replace_query( self, table_name: Union[str, sqlglot.expressions.query.Table], query_or_df: <MagicMock id='130969804860832'>, target_columns_to_types: Optional[Dict[str, sqlglot.expressions.datatypes.DataType]] = None, table_description: Optional[str] = None, column_descriptions: Optional[Dict[str, str]] = None, source_columns: Optional[List[str]] = None, supports_replace_table_override: Optional[bool] = None, **kwargs: Any) -> None:
312    def replace_query(
313        self,
314        table_name: TableName,
315        query_or_df: QueryOrDF,
316        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
317        table_description: t.Optional[str] = None,
318        column_descriptions: t.Optional[t.Dict[str, str]] = None,
319        source_columns: t.Optional[t.List[str]] = None,
320        supports_replace_table_override: t.Optional[bool] = None,
321        **kwargs: t.Any,
322    ) -> None:
323        """
324        Redshift doesn't support `CREATE OR REPLACE TABLE...` and it also doesn't support `VALUES` expression so we need to specially
325        handle DataFrame replacements.
326
327        If the table doesn't exist then we just create it and load it with insert statements
328        If it does exist then we need to do the:
329            `CREATE TABLE...`, `INSERT INTO...`, `RENAME TABLE...`, `RENAME TABLE...`, DROP TABLE...`  dance.
330        """
331        import pandas as pd
332
333        target_data_object = self.get_data_object(table_name)
334        table_exists = target_data_object is not None
335        if self.drop_data_object_on_type_mismatch(target_data_object, DataObjectType.TABLE):
336            table_exists = False
337
338        if not isinstance(query_or_df, pd.DataFrame) or not table_exists:
339            return super().replace_query(
340                table_name,
341                query_or_df,
342                target_columns_to_types,
343                table_description,
344                column_descriptions,
345                source_columns=source_columns,
346                **kwargs,
347            )
348        source_queries, target_columns_to_types = self._get_source_queries_and_columns_to_types(
349            query_or_df,
350            target_columns_to_types,
351            target_table=table_name,
352            source_columns=source_columns,
353        )
354        target_columns_to_types = target_columns_to_types or self.columns(table_name)
355        target_table = exp.to_table(table_name)
356        with self.transaction():
357            temp_table = self._get_temp_table(target_table)
358            old_table = self._get_temp_table(target_table)
359            self.create_table(
360                temp_table,
361                target_columns_to_types,
362                exists=False,
363                table_description=table_description,
364                column_descriptions=column_descriptions,
365                **kwargs,
366            )
367            self._insert_append_source_queries(temp_table, source_queries, target_columns_to_types)
368            self.rename_table(target_table, old_table)
369            self.rename_table(temp_table, target_table)
370            self.drop_table(old_table)

Redshift doesn't support CREATE OR REPLACE TABLE... and it also doesn't support VALUES expression so we need to specially handle DataFrame replacements.

If the table doesn't exist then we just create it and load it with insert statements

If it does exist then we need to do the:

CREATE TABLE..., INSERT INTO..., RENAME TABLE..., RENAME TABLE..., DROP TABLE...` dance.

def merge( self, target_table: Union[str, sqlglot.expressions.query.Table], source_table: <MagicMock id='130969804860832'>, target_columns_to_types: Optional[Dict[str, sqlglot.expressions.datatypes.DataType]], unique_key: Sequence[sqlglot.expressions.core.Expr], when_matched: Optional[sqlglot.expressions.dml.Whens] = None, merge_filter: Optional[sqlglot.expressions.core.Expr] = None, source_columns: Optional[List[str]] = None, **kwargs: Any) -> None:
425    def merge(
426        self,
427        target_table: TableName,
428        source_table: QueryOrDF,
429        target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
430        unique_key: t.Sequence[exp.Expr],
431        when_matched: t.Optional[exp.Whens] = None,
432        merge_filter: t.Optional[exp.Expr] = None,
433        source_columns: t.Optional[t.List[str]] = None,
434        **kwargs: t.Any,
435    ) -> None:
436        if self.enable_merge:
437            # By default we use the logical merge unless the user has opted in
438            super().merge(
439                target_table=target_table,
440                source_table=source_table,
441                target_columns_to_types=target_columns_to_types,
442                unique_key=unique_key,
443                when_matched=when_matched,
444                merge_filter=merge_filter,
445                source_columns=source_columns,
446            )
447        else:
448            logical_merge(
449                self,
450                target_table,
451                source_table,
452                target_columns_to_types,
453                unique_key,
454                when_matched=when_matched,
455                merge_filter=merge_filter,
456                source_columns=source_columns,
457            )
def table_exists(self, table_name: Union[str, sqlglot.expressions.query.Table]) -> bool:
 76    def table_exists(self, table_name: TableName) -> bool:
 77        """
 78        Postgres doesn't support describe so I'm using what the redshift cursor does to check if a table
 79        exists. We don't use this directly in order for this to work as a base class for other postgres
 80
 81        Reference: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/cursor.py#L528-L553
 82        """
 83        table = exp.to_table(table_name)
 84        data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
 85        if data_object_cache_key in self._data_object_cache:
 86            logger.debug("Table existence cache hit: %s", data_object_cache_key)
 87            return self._data_object_cache[data_object_cache_key] is not None
 88
 89        sql = (
 90            exp.select("1")
 91            .from_("information_schema.tables")
 92            .where(f"table_name = '{table.alias_or_name}'")
 93        )
 94        database_name = table.db
 95        if database_name:
 96            sql = sql.where(f"table_schema = '{database_name}'")
 97
 98        self.execute(sql)
 99
100        result = self.cursor.fetchone()
101
102        return result[0] == 1 if result is not None else False

Postgres doesn't support describe so I'm using what the redshift cursor does to check if a table exists. We don't use this directly in order for this to work as a base class for other postgres

Reference: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/cursor.py#L528-L553

def drop_view( self, view_name: Union[str, sqlglot.expressions.query.Table], ignore_if_not_exists: bool = True, materialized: bool = False, **kwargs: Any) -> None:
142    def drop_view(
143        self,
144        view_name: TableName,
145        ignore_if_not_exists: bool = True,
146        materialized: bool = False,
147        **kwargs: t.Any,
148    ) -> None:
149        kwargs["cascade"] = kwargs.get("cascade", True)
150        return super().drop_view(
151            view_name,
152            ignore_if_not_exists=ignore_if_not_exists,
153            materialized=materialized,
154            **kwargs,
155        )

Drop a view.

Inherited Members
sqlmesh.core.engine_adapter.base.EngineAdapter
EngineAdapter
DATA_OBJECT_FILTER_BATCH_SIZE
SUPPORTS_TRANSACTIONS
SUPPORTS_INDEXES
MAX_TABLE_COMMENT_LENGTH
MAX_COLUMN_COMMENT_LENGTH
INSERT_OVERWRITE_STRATEGY
SUPPORTS_MATERIALIZED_VIEWS
SUPPORTS_MATERIALIZED_VIEW_SCHEMA
SUPPORTS_VIEW_SCHEMA
SUPPORTS_CLONING
SUPPORTS_MANAGED_MODELS
SUPPORTS_CREATE_DROP_CATALOG
SUPPORTS_TUPLE_IN
HAS_VIEW_BINDING
RECREATE_MATERIALIZED_VIEW_ON_EVALUATION
DEFAULT_CATALOG_TYPE
QUOTE_IDENTIFIERS_IN_VIEWS
MAX_IDENTIFIER_LENGTH
ATTACH_CORRELATION_ID
SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS
RESOLVE_TABLE_REFS_IN_PHYSICAL_PROPERTIES
dialect
correlation_id
with_settings
connection
spark
snowpark
bigframe
comments_enabled
supports_virtual_catalog
inject_virtual_catalog
schema_differ
default_catalog
engine_run_mode
recycle
close
set_current_catalog
get_catalog_type
get_catalog_type_from_table
current_catalog_type
create_index
create_table
create_managed_table
ctas
create_state_table
create_table_like
clone_table
drop_data_object
drop_table
drop_managed_table
get_alter_operations
alter_table
create_schema
drop_schema
create_catalog
drop_catalog
delete_from
insert_append
insert_overwrite_by_partition
insert_overwrite_by_time_partition
update_table
scd_type_2_by_time
scd_type_2_by_column
rename_table
get_data_object
get_data_objects
fetchone
fetchall
fetchdf
fetch_pyspark_df
wap_enabled
wap_supported
wap_table_name
wap_prepare
wap_publish
sync_grants_config
transaction
session
execute
temp_table
adjust_physical_properties_for_incremental
drop_data_object_on_type_mismatch
ensure_nulls_for_unmatched_after_join
use_server_nulls_for_unmatched_after_join
ping
get_table_last_modified_ts
sqlmesh.core.engine_adapter.base_postgres.BasePostgresEngineAdapter
DEFAULT_BATCH_SIZE
COMMENT_CREATION_TABLE
SUPPORTS_QUERY_EXECUTION_TRACKING
SUPPORTED_DROP_CASCADE_OBJECT_KINDS
catalog_support
sqlmesh.core.engine_adapter.mixins.GetCurrentCatalogFromFunctionMixin
get_current_catalog
sqlmesh.core.engine_adapter.mixins.RowDiffMixin
MAX_TIMESTAMP_PRECISION
concat_columns
normalize_value
sqlmesh.core.engine_adapter.mixins.GrantsFromInfoSchemaMixin
CURRENT_USER_OR_ROLE_EXPRESSION
USE_CATALOG_IN_GRANTS
GRANT_INFORMATION_SCHEMA_TABLE_NAME