From 650a55b6b37b44d66b20fd28440f11beb46bd907 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt Date: Wed, 30 Nov 2022 20:26:03 +0200 Subject: [PATCH 1/5] fix(sqla): use same template processor in all methods --- superset/connectors/base/models.py | 6 ++- superset/connectors/sqla/models.py | 76 +++++++++++++++++++++++------- superset/models/helpers.py | 6 ++- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index b1272293286d9..af4473a4fe211 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -464,7 +464,11 @@ def query(self, query_obj: QueryObjectDict) -> QueryResult: """ raise NotImplementedError() - def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: + def values_for_column( + self, + column_name: str, + limit: int = 10000, + ) -> List[Any]: """Given a column, returns an iterable of distinct values This is used to populate the dropdown showing a list of diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index a67e686ff31f0..8c0a5a254f145 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -211,7 +211,11 @@ def query(self, query_obj: QueryObjectDict) -> QueryResult: def get_query_str(self, query_obj: QueryObjectDict) -> str: raise NotImplementedError() - def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: + def values_for_column( + self, + column_name: str, + limit: int = 10000, + ) -> List[Any]: raise NotImplementedError() @@ -301,14 +305,18 @@ def type_generic(self) -> Optional[utils.GenericDataType]: ) return column_spec.generic_type if column_spec else None - def get_sqla_col(self, label: Optional[str] = None) -> Column: + def get_sqla_col( + self, + label: Optional[str] = None, + template_processor: Optional[BaseTemplateProcessor] = None, + ) -> Column: label = label or self.column_name db_engine_spec = self.db_engine_spec column_spec = db_engine_spec.get_column_spec(self.type, db_extra=self.db_extra) type_ = column_spec.sqla_type if column_spec else None - if self.expression: - tp = self.table.get_template_processor() - expression = tp.process_template(self.expression) + if expression := self.expression: + if template_processor: + template_processor.process_template(expression) col = literal_column(expression, type_=type_) else: col = column(self.column_name, type_=type_) @@ -458,10 +466,17 @@ class SqlMetric(Model, BaseMetric, CertificationMixin): def __repr__(self) -> str: return str(self.metric_name) - def get_sqla_col(self, label: Optional[str] = None) -> Column: + def get_sqla_col( + self, + label: Optional[str] = None, + template_processor: Optional[BaseTemplateProcessor] = None, + ) -> Column: label = label or self.metric_name - tp = self.table.get_template_processor() - sqla_col: ColumnClause = literal_column(tp.process_template(self.expression)) + expression = self.expression + if template_processor: + expression = template_processor.process_template(expression) + + sqla_col: ColumnClause = literal_column(expression) return self.table.make_sqla_column_compatible(sqla_col, label) @property @@ -778,10 +793,17 @@ def extra_dict(self) -> Dict[str, Any]: except (TypeError, json.JSONDecodeError): return {} - def get_fetch_values_predicate(self) -> TextClause: - tp = self.get_template_processor() + def get_fetch_values_predicate( + self, + template_processor: Optional[BaseTemplateProcessor] = None, + ) -> TextClause: + fetch_values_predicate = self.fetch_values_predicate + if template_processor: + fetch_values_predicate = template_processor.process_template( + fetch_values_predicate + ) try: - return self.text(tp.process_template(self.fetch_values_predicate)) + return self.text(fetch_values_predicate) except TemplateError as ex: raise QueryObjectValidationError( _( @@ -790,7 +812,11 @@ def get_fetch_values_predicate(self) -> TextClause: ) ) from ex - def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: + def values_for_column( + self, + column_name: str, + limit: int = 10000, + ) -> List[Any]: """Runs query against sqla to retrieve some sample values for the given column. """ @@ -799,12 +825,16 @@ def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: tp = self.get_template_processor() tbl, cte = self.get_from_clause(tp) - qry = select([target_col.get_sqla_col()]).select_from(tbl).distinct() + qry = ( + select([target_col.get_sqla_col(template_processor=tp)]) + .select_from(tbl) + .distinct() + ) if limit: qry = qry.limit(limit) if self.fetch_values_predicate: - qry = qry.where(self.get_fetch_values_predicate()) + qry = qry.where(self.get_fetch_values_predicate(template_processor=tp)) with self.database.get_sqla_engine_with_context() as engine: sql = qry.compile(engine, compile_kwargs={"literal_binds": True}) @@ -1190,7 +1220,11 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma ) ) elif isinstance(metric, str) and metric in metrics_by_name: - metrics_exprs.append(metrics_by_name[metric].get_sqla_col()) + metrics_exprs.append( + metrics_by_name[metric].get_sqla_col( + template_processor=template_processor + ) + ) else: raise QueryObjectValidationError( _("Metric '%(metric)s' does not exist", metric=metric) @@ -1229,12 +1263,16 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma col = metrics_exprs_by_expr.get(str(col), col) need_groupby = True elif col in columns_by_name: - col = columns_by_name[col].get_sqla_col() + col = columns_by_name[col].get_sqla_col( + template_processor=template_processor + ) elif col in metrics_exprs_by_label: col = metrics_exprs_by_label[col] need_groupby = True elif col in metrics_by_name: - col = metrics_by_name[col].get_sqla_col() + col = metrics_by_name[col].get_sqla_col( + template_processor=template_processor + ) need_groupby = True if isinstance(col, ColumnElement): @@ -1565,7 +1603,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma having_clause_and += [self.text(having)] if apply_fetch_values_predicate and self.fetch_values_predicate: - qry = qry.where(self.get_fetch_values_predicate()) + qry = qry.where( + self.get_fetch_values_predicate(template_processor=template_processor) + ) if granularity: qry = qry.where(and_(*(time_filters + where_clause_and))) else: diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 2582f7e8d2b60..898b6bc2b7c2a 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1260,7 +1260,11 @@ def get_time_filter( ) return and_(*l) - def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: + def values_for_column( + self, + column_name: str, + limit: int = 10000, + ) -> List[Any]: """Runs query against sqla to retrieve some sample values for the given column. """ From e23b894c13292447d17ee68950282ac436545dd3 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt Date: Wed, 30 Nov 2022 21:11:07 +0200 Subject: [PATCH 2/5] add more --- superset/connectors/base/models.py | 6 +-- superset/connectors/sqla/models.py | 73 +++++++++++++++++++----------- superset/models/helpers.py | 6 +-- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index af4473a4fe211..b1272293286d9 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -464,11 +464,7 @@ def query(self, query_obj: QueryObjectDict) -> QueryResult: """ raise NotImplementedError() - def values_for_column( - self, - column_name: str, - limit: int = 10000, - ) -> List[Any]: + def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: """Given a column, returns an iterable of distinct values This is used to populate the dropdown showing a list of diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 8c0a5a254f145..652139f293626 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -211,11 +211,7 @@ def query(self, query_obj: QueryObjectDict) -> QueryResult: def get_query_str(self, query_obj: QueryObjectDict) -> str: raise NotImplementedError() - def values_for_column( - self, - column_name: str, - limit: int = 10000, - ) -> List[Any]: + def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: raise NotImplementedError() @@ -332,8 +328,9 @@ def get_time_filter( start_dttm: Optional[DateTime] = None, end_dttm: Optional[DateTime] = None, label: Optional[str] = "__time", + template_processor: Optional[BaseTemplateProcessor] = None, ) -> ColumnElement: - col = self.get_sqla_col(label=label) + col = self.get_sqla_col(label=label, template_processor=template_processor) l = [] if start_dttm: l.append(col >= self.table.text(self.dttm_sql_literal(start_dttm))) @@ -812,11 +809,7 @@ def get_fetch_values_predicate( ) ) from ex - def values_for_column( - self, - column_name: str, - limit: int = 10000, - ) -> List[Any]: + def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: """Runs query against sqla to retrieve some sample values for the given column. """ @@ -966,7 +959,9 @@ def adhoc_metric_to_sqla( column_name = cast(str, metric_column.get("column_name")) table_column: Optional[TableColumn] = columns_by_name.get(column_name) if table_column: - sqla_column = table_column.get_sqla_col() + sqla_column = table_column.get_sqla_col( + template_processor=template_processor + ) else: sqla_column = column(column_name) sqla_metric = self.sqla_aggregations[metric["aggregate"]](sqla_column) @@ -1003,9 +998,11 @@ def adhoc_column_to_sqla( schema=self.schema, template_processor=template_processor, ) - col_in_metadata = self.get_column(expression) + col_in_metadata = cast(TableColumn, self.get_column(expression)) if col_in_metadata: - sqla_column = col_in_metadata.get_sqla_col() + sqla_column = col_in_metadata.get_sqla_col( + template_processor=template_processor + ) is_dttm = col_in_metadata.is_temporal else: sqla_column = literal_column(expression) @@ -1306,7 +1303,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma ) # if groupby field equals a selected column elif selected in columns_by_name: - outer = columns_by_name[selected].get_sqla_col() + outer = columns_by_name[selected].get_sqla_col( + template_processor=template_processor + ) else: selected = validate_adhoc_subquery( selected, @@ -1340,7 +1339,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma self.schema, ) select_exprs.append( - columns_by_name[selected].get_sqla_col() + columns_by_name[selected].get_sqla_col( + template_processor=template_processor + ) if isinstance(selected, str) and selected in columns_by_name else self.make_sqla_column_compatible( literal_column(selected), _column_label @@ -1374,11 +1375,18 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma ): time_filters.append( columns_by_name[self.main_dttm_col].get_time_filter( - from_dttm, - to_dttm, + start_dttm=from_dttm, + end_dttm=to_dttm, + template_processor=template_processor, ) ) - time_filters.append(dttm_col.get_time_filter(from_dttm, to_dttm)) + time_filters.append( + dttm_col.get_time_filter( + start_dttm=from_dttm, + end_dttm=to_dttm, + template_processor=template_processor, + ) + ) # Always remove duplicates by column name, as sometimes `metrics_exprs` # can have the same name as a groupby column (e.g. when users use @@ -1434,7 +1442,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma time_grain=filter_grain, template_processor=template_processor ) elif col_obj: - sqla_col = col_obj.get_sqla_col() + sqla_col = col_obj.get_sqla_col( + template_processor=template_processor + ) col_type = col_obj.type if col_obj else None col_spec = db_engine_spec.get_column_spec( native_type=col_type, @@ -1559,6 +1569,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma start_dttm=_since, end_dttm=_until, label=sqla_col.key, + template_processor=template_processor, ) ) else: @@ -1657,8 +1668,9 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma if dttm_col and not db_engine_spec.time_groupby_inline: inner_time_filter = [ dttm_col.get_time_filter( - inner_from_dttm or from_dttm, - inner_to_dttm or to_dttm, + start_dttm=inner_from_dttm or from_dttm, + end_dttm=inner_to_dttm or to_dttm, + template_processor=template_processor, ) ] subq = subq.where(and_(*(where_clause_and + inner_time_filter))) @@ -1667,7 +1679,10 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma ob = inner_main_metric_expr if series_limit_metric: ob = self._get_series_orderby( - series_limit_metric, metrics_by_name, columns_by_name + series_limit_metric=series_limit_metric, + metrics_by_name=metrics_by_name, + columns_by_name=columns_by_name, + template_processor=template_processor, ) direction = desc if order_desc else asc subq = subq.order_by(direction(ob)) @@ -1687,9 +1702,10 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma orderby = [ ( self._get_series_orderby( - series_limit_metric, - metrics_by_name, - columns_by_name, + series_limit_metric=series_limit_metric, + metrics_by_name=metrics_by_name, + columns_by_name=columns_by_name, + template_processor=template_processor, ), not order_desc, ) @@ -1749,6 +1765,7 @@ def _get_series_orderby( series_limit_metric: Metric, metrics_by_name: Dict[str, SqlMetric], columns_by_name: Dict[str, TableColumn], + template_processor: Optional[BaseTemplateProcessor] = None, ) -> Column: if utils.is_adhoc_metric(series_limit_metric): assert isinstance(series_limit_metric, dict) @@ -1757,7 +1774,9 @@ def _get_series_orderby( isinstance(series_limit_metric, str) and series_limit_metric in metrics_by_name ): - ob = metrics_by_name[series_limit_metric].get_sqla_col() + ob = metrics_by_name[series_limit_metric].get_sqla_col( + template_processor=template_processor + ) else: raise QueryObjectValidationError( _("Metric '%(metric)s' does not exist", metric=series_limit_metric) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 898b6bc2b7c2a..2582f7e8d2b60 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1260,11 +1260,7 @@ def get_time_filter( ) return and_(*l) - def values_for_column( - self, - column_name: str, - limit: int = 10000, - ) -> List[Any]: + def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: """Runs query against sqla to retrieve some sample values for the given column. """ From c5e2f2d8de6fd59a4cfb61856029da34e5219823 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt Date: Wed, 30 Nov 2022 21:19:37 +0200 Subject: [PATCH 3/5] some more simplification --- superset/connectors/sqla/models.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 652139f293626..825e37c14e18a 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. # pylint: disable=too-many-lines +from __future__ import annotations + import dataclasses import json import logging @@ -222,7 +224,7 @@ class TableColumn(Model, BaseColumn, CertificationMixin): __tablename__ = "table_columns" __table_args__ = (UniqueConstraint("table_id", "column_name"),) table_id = Column(Integer, ForeignKey("tables.id")) - table: "SqlaTable" = relationship( + table: SqlaTable = relationship( "SqlaTable", backref=backref("columns", cascade="all, delete-orphan"), foreign_keys=[table_id], @@ -606,6 +608,9 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho def __repr__(self) -> str: # pylint: disable=invalid-repr-returned return self.name + def get_column(self, column_name: Optional[str]) -> Optional[TableColumn]: + return super().get_column(column_name) + @staticmethod def _apply_cte(sql: str, cte: Optional[str]) -> str: """ @@ -662,7 +667,7 @@ def get_datasource_by_name( datasource_name: str, schema: Optional[str], database_name: str, - ) -> Optional["SqlaTable"]: + ) -> Optional[SqlaTable]: schema = schema or None query = ( session.query(cls) @@ -998,7 +1003,7 @@ def adhoc_column_to_sqla( schema=self.schema, template_processor=template_processor, ) - col_in_metadata = cast(TableColumn, self.get_column(expression)) + col_in_metadata = self.get_column(expression) if col_in_metadata: sqla_column = col_in_metadata.get_sqla_col( template_processor=template_processor @@ -1989,7 +1994,7 @@ def query_datasources_by_name( database: Database, datasource_name: str, schema: Optional[str] = None, - ) -> List["SqlaTable"]: + ) -> List[SqlaTable]: query = ( session.query(cls) .filter_by(database_id=database.id) @@ -2006,7 +2011,7 @@ def query_datasources_by_permissions( # pylint: disable=invalid-name database: Database, permissions: Set[str], schema_perms: Set[str], - ) -> List["SqlaTable"]: + ) -> List[SqlaTable]: # TODO(hughhhh): add unit test return ( session.query(cls) @@ -2023,7 +2028,7 @@ def query_datasources_by_permissions( # pylint: disable=invalid-name @classmethod def get_eager_sqlatable_datasource( cls, session: Session, datasource_id: int - ) -> "SqlaTable": + ) -> SqlaTable: """Returns SqlaTable with columns and metrics.""" return ( session.query(cls) @@ -2036,7 +2041,7 @@ def get_eager_sqlatable_datasource( ) @classmethod - def get_all_datasources(cls, session: Session) -> List["SqlaTable"]: + def get_all_datasources(cls, session: Session) -> List[SqlaTable]: qry = session.query(cls) qry = cls.default_query(qry) return qry.all() @@ -2097,7 +2102,7 @@ def quote_identifier(self) -> Callable[[str], str]: def before_update( mapper: Mapper, # pylint: disable=unused-argument connection: Connection, # pylint: disable=unused-argument - target: "SqlaTable", + target: SqlaTable, ) -> None: """ Check before update if the target table already exists. @@ -2169,7 +2174,7 @@ def update_column( # pylint: disable=unused-argument def after_insert( mapper: Mapper, connection: Connection, - sqla_table: "SqlaTable", + sqla_table: SqlaTable, ) -> None: """ Update dataset permissions after insert @@ -2183,7 +2188,7 @@ def after_insert( def after_delete( mapper: Mapper, connection: Connection, - sqla_table: "SqlaTable", + sqla_table: SqlaTable, ) -> None: """ Update dataset permissions after delete @@ -2194,7 +2199,7 @@ def after_delete( def after_update( mapper: Mapper, connection: Connection, - sqla_table: "SqlaTable", + sqla_table: SqlaTable, ) -> None: """ Update dataset permissions @@ -2229,7 +2234,7 @@ def after_update( return def write_shadow_dataset( - self: "SqlaTable", + self: SqlaTable, ) -> None: """ This method is deprecated From d7ea23f6d171d7520f126c56c37bfe508c2ad124 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt Date: Thu, 1 Dec 2022 09:35:21 +0200 Subject: [PATCH 4/5] add tests --- superset/connectors/sqla/models.py | 7 ++-- tests/integration_tests/sqla_models_tests.py | 43 +++++++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 825e37c14e18a..6ceaeab79bc86 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -314,7 +314,7 @@ def get_sqla_col( type_ = column_spec.sqla_type if column_spec else None if expression := self.expression: if template_processor: - template_processor.process_template(expression) + expression = template_processor.process_template(expression) col = literal_column(expression, type_=type_) else: col = column(self.column_name, type_=type_) @@ -365,10 +365,9 @@ def get_timestamp_expression( if not self.expression and not time_grain and not is_epoch: sqla_col = column(self.column_name, type_=type_) return self.table.make_sqla_column_compatible(sqla_col, label) - if self.expression: - expression = self.expression + if expression := self.expression: if template_processor: - expression = template_processor.process_template(self.expression) + expression = template_processor.process_template(expression) col = literal_column(expression, type_=type_) else: col = column(self.column_name, type_=type_) diff --git a/tests/integration_tests/sqla_models_tests.py b/tests/integration_tests/sqla_models_tests.py index 3088bdfb02c55..dfba16179d45e 100644 --- a/tests/integration_tests/sqla_models_tests.py +++ b/tests/integration_tests/sqla_models_tests.py @@ -201,22 +201,34 @@ def test_jinja_metrics_and_calc_columns(self, flask_g): "granularity": None, "from_dttm": None, "to_dttm": None, - "groupby": ["user", "expr"], + "columns": [ + "user", + "expr", + { + "hasCustomLabel": True, + "label": "adhoc_column", + "sqlExpression": "'{{ 'foo_' + time_grain }}'", + }, + ], "metrics": [ { + "hasCustomLabel": True, + "label": "adhoc_metric", "expressionType": AdhocMetricExpressionType.SQL, - "sqlExpression": "SUM(case when user = '{{ current_username() }}' " - "then 1 else 0 end)", - "label": "SUM(userid)", - } + "sqlExpression": "SUM(case when user = '{{ 'user_' + " + "current_username() }}' then 1 else 0 end)", + }, + "count_timegrain", ], "is_timeseries": False, "filter": [], + "extras": {"time_grain_sqla": "P1D"}, } table = SqlaTable( table_name="test_has_jinja_metric_and_expr", - sql="SELECT '{{ current_username() }}' as user", + sql="SELECT '{{ 'user_' + current_username() }}' as user, " + "'{{ 'xyz_' + time_grain }}' as time_grain", database=get_example_database(), ) TableColumn( @@ -226,14 +238,25 @@ def test_jinja_metrics_and_calc_columns(self, flask_g): type="VARCHAR(100)", table=table, ) + SqlMetric( + metric_name="count_timegrain", + expression="count('{{ 'bar_' + time_grain }}')", + table=table, + ) db.session.commit() sqla_query = table.get_sqla_query(**base_query_obj) query = table.database.compile_sqla_query(sqla_query.sqla_query) - # assert expression - assert "case when 'abc' = 'abc' then 'yes' else 'no' end" in query - # assert metric - assert "SUM(case when user = 'abc' then 1 else 0 end)" in query + # assert virtual dataset + assert "SELECT 'user_abc' as user, 'xyz_P1D' as time_grain" in query + # assert dataset calculated column + assert "case when 'abc' = 'abc' then 'yes' else 'no' end AS expr" in query + # assert adhoc column + assert "'foo_P1D'" in query + # assert dataset saved metric + assert "count('bar_P1D')" in query + # assert adhoc metric + assert "SUM(case when user = 'user_abc' then 1 else 0 end)" in query # Cleanup db.session.delete(table) db.session.commit() From 82478e20690a557234ca74dbedd7572d488cdbf0 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt Date: Thu, 1 Dec 2022 09:58:38 +0200 Subject: [PATCH 5/5] lint --- superset/connectors/sqla/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 6ceaeab79bc86..fd5942c51a5a0 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -607,9 +607,6 @@ class SqlaTable(Model, BaseDatasource): # pylint: disable=too-many-public-metho def __repr__(self) -> str: # pylint: disable=invalid-repr-returned return self.name - def get_column(self, column_name: Optional[str]) -> Optional[TableColumn]: - return super().get_column(column_name) - @staticmethod def _apply_cte(sql: str, cte: Optional[str]) -> str: """