From ec6c4e96636892c4c1d0d1a16c319bf0457f0a30 Mon Sep 17 00:00:00 2001 From: mfatihaktas Date: Mon, 1 Apr 2024 11:38:27 -0400 Subject: [PATCH] feat(ddl): make list_tables() and list_views() explicit --- compose.yaml | 4 + ibis/backends/__init__.py | 104 ++++++++++-- ibis/backends/bigquery/__init__.py | 57 ++++++- ibis/backends/clickhouse/__init__.py | 83 ++++++++-- ibis/backends/datafusion/__init__.py | 29 +++- ibis/backends/druid/__init__.py | 18 ++ ibis/backends/duckdb/__init__.py | 172 ++++++++++++++++++-- ibis/backends/duckdb/tests/test_client.py | 2 +- ibis/backends/duckdb/tests/test_register.py | 4 +- ibis/backends/exasol/__init__.py | 57 +++++-- ibis/backends/flink/__init__.py | 97 ++++++++--- ibis/backends/flink/tests/test_ddl.py | 20 +-- ibis/backends/impala/__init__.py | 27 ++- ibis/backends/mssql/__init__.py | 140 +++++++++++++--- ibis/backends/mssql/tests/test_client.py | 13 +- ibis/backends/mysql/__init__.py | 143 +++++++++++++--- ibis/backends/oracle/__init__.py | 140 +++++++++++----- ibis/backends/oracle/tests/test_client.py | 8 +- ibis/backends/pandas/__init__.py | 22 ++- ibis/backends/pandas/executor.py | 4 + ibis/backends/pandas/helpers.py | 1 + ibis/backends/polars/__init__.py | 34 ++++ ibis/backends/postgres/__init__.py | 140 +++++++++++++--- ibis/backends/postgres/tests/test_client.py | 8 +- ibis/backends/pyspark/__init__.py | 66 +++++++- ibis/backends/pyspark/converter.py | 1 + ibis/backends/pyspark/tests/conftest.py | 12 ++ ibis/backends/pyspark/tests/test_ddl.py | 2 +- ibis/backends/snowflake/__init__.py | 91 ++++++++++- ibis/backends/sqlite/__init__.py | 79 +++++++-- ibis/backends/tests/test_api.py | 87 +++++++++- ibis/backends/tests/test_client.py | 6 +- ibis/backends/tests/test_register.py | 6 +- ibis/backends/trino/__init__.py | 37 ++++- ibis/examples/tests/test_examples.py | 2 +- 35 files changed, 1445 insertions(+), 271 deletions(-) diff --git a/compose.yaml b/compose.yaml index 6cee8b7293bd..06bb82176e8c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,6 +16,7 @@ services: - clickhouse mysql: + platform: linux/amd64 environment: MYSQL_ALLOW_EMPTY_PASSWORD: "true" MYSQL_DATABASE: ibis_testing @@ -38,6 +39,7 @@ services: - $PWD/docker/mysql:/docker-entrypoint-initdb.d:ro postgres: + platform: linux/amd64 user: postgres environment: POSTGRES_PASSWORD: postgres @@ -59,6 +61,7 @@ services: - postgres:/data mssql: + platform: linux/amd64 image: mcr.microsoft.com/mssql/server:2022-latest environment: MSSQL_SA_PASSWORD: 1bis_Testing! @@ -341,6 +344,7 @@ services: - druid oracle: + platform: linux/amd64 image: gvenzl/oracle-free:23.4-slim environment: ORACLE_PASSWORD: ibis diff --git a/ibis/backends/__init__.py b/ibis/backends/__init__.py index f2341b2f6b1f..68635c8cf497 100644 --- a/ibis/backends/__init__.py +++ b/ibis/backends/__init__.py @@ -31,21 +31,28 @@ __all__ = ("BaseBackend", "connect") -class TablesAccessor(collections.abc.Mapping): - """A mapping-like object for accessing tables off a backend. +# TODO (mehmet): Given that we are dedicating "table" to +# the actual tables (excluding views), do we need to add `TabularsAccessor` +# and add `all/tabulars/relations()` in `BaseBackend`. - Tables may be accessed by name using either index or attribute access: + +class TabularsAccessor(collections.abc.Mapping): + """A mapping-like object for accessing tabulars off a backend. + + A tabular is a table or a view. They may be accessed by name using + either index or attribute access: Examples -------- >>> con = ibis.sqlite.connect("example.db") - >>> people = con.tables["people"] # access via index - >>> people = con.tables.people # access via attribute - + >>> people = con.tabulars["people"] # access via index + >>> people = con.tabulars.people # access via attribute """ def __init__(self, backend: BaseBackend): self._backend = backend + self._list = self._backend.list + self.repr_heading = "Tabulars" def __getitem__(self, name) -> ir.Table: try: @@ -62,29 +69,65 @@ def __getattr__(self, name) -> ir.Table: raise AttributeError(name) from exc def __iter__(self) -> Iterator[str]: - return iter(sorted(self._backend.list_tables())) + return iter(sorted(self._list())) def __len__(self) -> int: - return len(self._backend.list_tables()) + return len(self._list()) def __dir__(self) -> list[str]: o = set() o.update(dir(type(self))) o.update( name - for name in self._backend.list_tables() + for name in self._list() if name.isidentifier() and not keyword.iskeyword(name) ) return list(o) def __repr__(self) -> str: - tables = self._backend.list_tables() - rows = ["Tables", "------"] - rows.extend(f"- {name}" for name in sorted(tables)) - return "\n".join(rows) + return "\n".join( + [f"{self.repr_heading}", "------"] + + [f"- {name}" for name in sorted(self._list())] + ) def _ipython_key_completions_(self) -> list[str]: - return self._backend.list_tables() + return self._list() + + +class TablesAccessor(TabularsAccessor): + """A mapping-like object for accessing tables off a backend. + + Tables may be accessed by name using either index or attribute access: + + Examples + -------- + >>> con = ibis.sqlite.connect("example.db") + >>> people = con.tables["people"] # access via index + >>> people = con.tables.people # access via attribute + """ + + def __init__(self, backend: BaseBackend): + super().__init__(backend) + self._list = self._backend.list_tables + self.repr_heading = "Tables" + + +class ViewsAccessor(TabularsAccessor): + """A mapping-like object for accessing views off a backend. + + Views may be accessed by name using either index or attribute access: + + Examples + -------- + >>> con = ibis.sqlite.connect("example.db") + >>> people = con.views["people"] # access via index + >>> people = con.views.people # access via attribute + """ + + def __init__(self, backend: BaseBackend): + super().__init__(backend) + self._list = self._backend.list_views + self.repr_heading = "Views" class _FileIOHandler: @@ -867,7 +910,7 @@ def do_connect(self, *args, **kwargs) -> None: def _filter_with_like(values: Iterable[str], like: str | None = None) -> list[str]: """Filter names with a `like` pattern (regex). - The methods `list_databases` and `list_tables` accept a `like` + The methods `list_databases` and `list_tables/views` accept a `like` argument, which filters the returned tables with tables that match the provided pattern. @@ -966,6 +1009,22 @@ def table( """ + @functools.cached_property + def tabulars(self): + """An accessor for tabulars in the database. + + A tabular is a table or a view. They may be accessed by name + using either index or attribute access: + + Examples + -------- + >>> con = ibis.sqlite.connect("example.db") + >>> people = con.tabulars["people"] # access via index + >>> people = con.tabulars.people # access via attribute + + """ + return TabularsAccessor(self) + @functools.cached_property def tables(self): """An accessor for tables in the database. @@ -981,6 +1040,21 @@ def tables(self): """ return TablesAccessor(self) + @functools.cached_property + def views(self): + """An accessor for views in the database. + + Views may be accessed by name using either index or attribute access: + + Examples + -------- + >>> con = ibis.sqlite.connect("example.db") + >>> people = con.views["people"] # access via index + >>> people = con.views.people # access via attribute + + """ + return ViewsAccessor(self) + @property @abc.abstractmethod def version(self) -> str: diff --git a/ibis/backends/bigquery/__init__.py b/ibis/backends/bigquery/__init__.py index af70a7df2350..1c5d2e4efbd4 100644 --- a/ibis/backends/bigquery/__init__.py +++ b/ibis/backends/bigquery/__init__.py @@ -897,18 +897,56 @@ def list_databases( ] return self._filter_with_like(results, like) + def list( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + The database location to perform the list against. + + By default uses the current `dataset` (`self.current_database`) and + `project` (`self.current_catalog`). + + To specify a table in a separate BigQuery dataset, you can pass in the + dataset and project as a string `"dataset.project"`, or as a tuple of + strings `("dataset", "project")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema (dataset) inside `database` to perform the list against. + """ + + return self.list_tables(like=like, database=database, schema=schema) + def list_tables( self, like: str | None = None, database: tuple[str, str] | str | None = None, schema: str | None = None, ) -> list[str]: - """List the tables in the database. + """List the names of tables and views in the database. Parameters ---------- like - A pattern to use for listing tables. + A pattern to use for listing tables/views. database The database location to perform the list against. @@ -922,7 +960,7 @@ def list_tables( ::: {.callout-note} ## Ibis does not use the word `schema` to refer to database hierarchy. - A collection of tables is referred to as a `database`. + A collection of tables/views is referred to as a `database`. A collection of `database` is referred to as a `catalog`. These terms are mapped onto the corresponding features in each @@ -932,6 +970,19 @@ def list_tables( schema [deprecated] The schema (dataset) inside `database` to perform the list against. """ + + # TODO (mehmet): BigQuery API does not seem to allow for + # retrieving only tables or only views: + # - https://cloud.google.com/bigquery/docs/reference/rest/v2/tables/list#request-body + # - https://cloud.google.com/bigquery/docs/view-metadata#python + # + # We could exclude views from the list by retrieving the + # meta-data for each table with `self.client.get_table()` and + # checking if it is a view or not. But this would obviously be + # costly. + # + # Leaving this decision to the review process. + table_loc = self._warn_and_create_table_loc(database, schema) project, dataset = self._parse_project_and_dataset(table_loc) diff --git a/ibis/backends/clickhouse/__init__.py b/ibis/backends/clickhouse/__init__.py index d676d95a7870..4232c8d417b1 100644 --- a/ibis/backends/clickhouse/__init__.py +++ b/ibis/backends/clickhouse/__init__.py @@ -197,19 +197,13 @@ def list_databases(self, like: str | None = None) -> list[str]: databases = [] return self._filter_with_like(databases, like) - def list_tables( - self, like: str | None = None, database: str | None = None + def _list_tables_or_views( + self, + views: bool, + like: str | None = None, + database: str | None = None, ) -> list[str]: - """List the tables in the database. - - Parameters - ---------- - like - A pattern to use for listing tables. - database - Database to list tables from. Default behavior is to show tables in - the current database. - """ + """Helper function for `list_tables/views()`.""" query = sg.select(C.name).from_(sg.table("tables", db="system")) @@ -218,7 +212,8 @@ def list_tables( else: database = sge.convert(database) - query = query.where(C.database.eq(database).or_(C.is_temporary)) + view_cond = C.engine.eq("View") if views else C.engine.neq("View") + query = query.where(C.database.eq(database).or_(C.is_temporary), view_cond) with self._safe_raw_sql(query) as result: results = result.result_columns @@ -229,6 +224,67 @@ def list_tables( tables = [] return self._filter_with_like(tables, like) + def list( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + """ + return self.list_tables(like=like, database=database) + self.list_views( + like=like, database=database + ) + + def list_tables( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + database + Database to list tables from. Default behavior is to + show tables in the current database. + """ + return self._list_tables_or_views( + views=False, + like=like, + database=database, + ) + + def list_views( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + database + Database to list views from. Default behavior is to + show views in the current database. + """ + return self._list_tables_or_views( + views=True, + like=like, + database=database, + ) + def _normalize_external_tables(self, external_tables=None) -> ExternalData | None: """Merge registered external tables with any new external tables.""" external_data = ExternalData() @@ -456,6 +512,7 @@ def raw_sql( external_data = self._normalize_external_tables(external_tables) with contextlib.suppress(AttributeError): query = query.sql(dialect=self.name, pretty=True) + self._log(query) return self.con.query(query, external_data=external_data, **kwargs) diff --git a/ibis/backends/datafusion/__init__.py b/ibis/backends/datafusion/__init__.py index a2ed95d5d54f..be0de90e3cb8 100644 --- a/ibis/backends/datafusion/__init__.py +++ b/ibis/backends/datafusion/__init__.py @@ -261,12 +261,34 @@ def drop_database( with self._safe_raw_sql(sge.Drop(kind="SCHEMA", this=db_name, exists=force)): pass + def list( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern in Python's regex format. + database + Unused in the datafusion backend. + + Returns + ------- + list[str] + The list of the table/view names that match the pattern `like`. + """ + + return self.list_tables(like=like, database=database) + def list_tables( self, like: str | None = None, database: str | None = None, ) -> list[str]: - """Return the list of table names in the current database. + """List the names of tables in the database. Parameters ---------- @@ -280,6 +302,11 @@ def list_tables( list[str] The list of the table names that match the pattern `like`. """ + # TODO (mehmet): `datafusion` library treats views as tables and + # does not expose any API that would make it possible to filter + # out the views from the tables. + # Ref: https://github.com/apache/arrow-datafusion-python + database = database or "public" query = ( sg.select("table_name") diff --git a/ibis/backends/druid/__init__.py b/ibis/backends/druid/__init__.py index d9c6c19b1d8c..12e1ff73fe3c 100644 --- a/ibis/backends/druid/__init__.py +++ b/ibis/backends/druid/__init__.py @@ -158,6 +158,19 @@ def create_table( def drop_table(self, *args, **kwargs): raise NotImplementedError() + def list(self, like: str | None = None, database: str | None = None) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + """ + return self.list_tables(like=like, database=database) + def list_tables( self, like: str | None = None, database: str | None = None ) -> list[str]: @@ -171,6 +184,11 @@ def list_tables( Database to list tables from. Default behavior is to show tables in the current database. """ + + # TODO (mehmet): Druid SQL does not seem to have a way to + # list the views. + # Ref: https://druid.apache.org/docs/latest/tutorials/tutorial-sql-query-view/ + t = sg.table("TABLES", db="INFORMATION_SCHEMA", quoted=True) c = self.compiler query = sg.select(sg.column("TABLE_NAME", quoted=True)).from_(t).sql(c.dialect) diff --git a/ibis/backends/duckdb/__init__.py b/ibis/backends/duckdb/__init__.py index 6e497dbb294b..6d2d2a09443c 100644 --- a/ibis/backends/duckdb/__init__.py +++ b/ibis/backends/duckdb/__init__.py @@ -112,11 +112,13 @@ def _to_sqlglot( return sg.select( *( - self.compiler.f.st_aswkb( - sg.column(col, quoted=self.compiler.quoted) - ).as_(col) - if col in geocols - else col + ( + self.compiler.f.st_aswkb( + sg.column(col, quoted=self.compiler.quoted) + ).as_(col) + if col in geocols + else col + ) for col in table_expr.columns ) ).from_(sql.subquery()) @@ -934,13 +936,13 @@ def read_delta( delta_table.to_pyarrow_dataset(), table_name=table_name ) - def list_tables( + def list( self, like: str | None = None, database: tuple[str, str] | str | None = None, schema: str | None = None, ) -> list[str]: - """List tables and views. + """List the names of tables and views in the database. ::: {.callout-note} ## Ibis does not use the word `schema` to refer to database hierarchy. @@ -963,9 +965,20 @@ def list_tables( By default uses the current `database` (`self.current_database`) and `catalog` (`self.current_catalog`). - To specify a table in a separate catalog, you can pass in the + To specify a table/view in a separate catalog, you can pass in the catalog and database as a string `"catalog.database"`, or as a tuple of strings `("catalog", "database")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: schema [deprecated] Schema name. If not passed, uses the current schema. @@ -979,18 +992,18 @@ def list_tables( >>> import ibis >>> con = ibis.duckdb.connect() >>> foo = con.create_table("foo", schema=ibis.schema(dict(a="int"))) - >>> con.list_tables() + >>> con.list() ['foo'] >>> bar = con.create_view("bar", foo) - >>> con.list_tables() - ['bar', 'foo'] + >>> con.list() + ['foo', 'bar'] >>> con.create_database("my_database") - >>> con.list_tables(database="my_database") + >>> con.list(database="my_database") [] >>> with con.begin() as c: ... c.exec_driver_sql("CREATE TABLE my_database.baz (a INTEGER)") # doctest: +ELLIPSIS <...> - >>> con.list_tables(database="my_database") + >>> con.list(database="my_database") ['baz'] """ @@ -1017,6 +1030,139 @@ def list_tables( return self._filter_with_like(out[col].to_pylist(), like) + def list_tables( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + Regex to filter by table name. + database + Database location. If not passed, uses the current database. + + By default uses the current `database` (`self.current_database`) and + `catalog` (`self.current_catalog`). + + To specify a table in a separate catalog, you can pass in the + catalog and database as a string `"catalog.database"`, or as a tuple of + strings `("catalog", "database")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] Schema name. If not passed, uses the current schema. + + Returns + ------- + list[str] + List of table names. + + Examples + -------- + >>> import ibis + >>> con = ibis.duckdb.connect() + >>> foo = con.create_table("foo", schema=ibis.schema(dict(a="int"))) + >>> con.list_tables() + ['foo'] + >>> bar = con.create_view("bar", foo) + >>> con.list_tables() + ['foo'] + >>> con.list_views() + ['bar'] + >>> con.create_database("my_database") + >>> con.list_tables(database="my_database") + [] + >>> with con.begin() as c: + ... c.exec_driver_sql("CREATE TABLE my_database.baz (a INTEGER)") # doctest: +ELLIPSIS + <...> + >>> con.list_tables(database="my_database") + ['baz'] + + """ + tables_and_views = self.list(like=like, database=database, schema=schema) + views = self.list_views(like=like, database=database, schema=schema) + + # Duckdb does not allow creating a table and a view + # with the same name. + return list(set(tables_and_views) - set(views)) + + def list_views( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + Regex to filter by table name. + database + Database location. If not passed, uses the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] Schema name. If not passed, uses the current schema. + + Returns + ------- + list[str] + List of view names. + + Examples + -------- + >>> import ibis + >>> con = ibis.duckdb.connect() + >>> foo = con.create_table("foo", schema=ibis.schema(dict(a="int"))) + >>> con.list_tables() + ['foo'] + >>> bar = con.create_view("bar", foo) + >>> con.list_tables() + ['foo'] + >>> con.list_views() + ['bar'] + """ + table_loc = self._warn_and_create_table_loc(database, schema) + + database = self.compiler.f.current_schema() + if table_loc is not None: + database = table_loc.db or database + + col = "view_name" + sql = ( + sg.select(col) + .from_(sg.table("duckdb_views")) + .distinct() + .where(C.schema_name.eq(database)) + .sql(self.name, pretty=True) + ) + out = self.con.execute(sql).fetch_arrow_table() + + return self._filter_with_like(out[col].to_pylist(), like) + def read_postgres( self, uri: str, *, table_name: str | None = None, database: str = "public" ) -> ir.Table: diff --git a/ibis/backends/duckdb/tests/test_client.py b/ibis/backends/duckdb/tests/test_client.py index b53044f81f6b..36028979f00c 100644 --- a/ibis/backends/duckdb/tests/test_client.py +++ b/ibis/backends/duckdb/tests/test_client.py @@ -271,7 +271,7 @@ def test_connect_local_file(out_method, extension, test_employee_data_1, tmp_pat with pytest.warns(FutureWarning, match="v9.1"): # ibis.connect uses con.register con = ibis.connect(tmp_path / f"out.{extension}") - t = next(iter(con.tables.values())) + t = next(iter(con.tabulars.values())) assert not t.head().execute().empty diff --git a/ibis/backends/duckdb/tests/test_register.py b/ibis/backends/duckdb/tests/test_register.py index 0bffd3b4d86d..1745f8695ca0 100644 --- a/ibis/backends/duckdb/tests/test_register.py +++ b/ibis/backends/duckdb/tests/test_register.py @@ -286,7 +286,7 @@ def test_attach_sqlite(data_dir, tmp_path): con = ibis.duckdb.connect() con.attach_sqlite(test_db_path) - assert set(con.list_tables()) >= { + assert set(con.list()) >= { "functional_alltypes", "awards_players", "batting", @@ -298,7 +298,7 @@ def test_attach_sqlite(data_dir, tmp_path): # overwrite existing sqlite_db and force schema to all strings con.attach_sqlite(test_db_path, overwrite=True, all_varchar=True) - assert set(con.list_tables()) >= { + assert set(con.list()) >= { "functional_alltypes", "awards_players", "batting", diff --git a/ibis/backends/exasol/__init__.py b/ibis/backends/exasol/__init__.py index 0eb1afc1664a..f543ff0fafeb 100644 --- a/ibis/backends/exasol/__init__.py +++ b/ibis/backends/exasol/__init__.py @@ -136,41 +136,72 @@ def begin(self): con.commit() @contextlib.contextmanager - def _safe_raw_sql(self, query: str, *args, **kwargs): + def _safe_raw_sql(self, query: str | sge.Expression, *args, **kwargs): with contextlib.suppress(AttributeError): query = query.sql(dialect=self.dialect) with self.begin() as cur: yield cur.execute(query, *args, **kwargs) - def list_tables(self, like=None, database=None): - """List the tables in the database. + def list(self, like=None, database=None) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Database to list tables/views from. Default behavior is to + list tables/views in the current database. + """ + return self.list_tables(like=like, database=database) + self.list_views( + like=like, database=database + ) + + def list_tables(self, like=None, database=None) -> list[str]: + """List the names of tables in the database. Parameters ---------- like A pattern to use for listing tables. database - Database to list tables from. Default behavior is to show tables in + Database to list tables from. Default behavior is to list tables in the current database. """ - tables = sg.select("table_name").from_( + sg_expr = sg.select("table_name").from_( sg.table("EXA_ALL_TABLES", catalog="SYS") ) - views = sg.select(sg.column("view_name").as_("table_name")).from_( + if database is not None: + sg_expr = sg_expr.where(sg.column("table_schema").eq(sge.convert(database))) + + with self._safe_raw_sql(sg_expr) as con: + tables = con.fetchall() + + return self._filter_with_like([table for (table,) in tables], like=like) + + def list_views(self, like=None, database=None) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + database + Database to list views from. Default behavior is to show views in + the current database. + """ + sg_expr = sg.select(sg.column("view_name")).from_( sg.table("EXA_ALL_VIEWS", catalog="SYS") ) if database is not None: - tables = tables.where(sg.column("table_schema").eq(sge.convert(database))) - views = views.where(sg.column("view_schema").eq(sge.convert(database))) - - query = sg.union(tables, views) + sg_expr = sg_expr.where(sg.column("view_schema").eq(sge.convert(database))) - with self._safe_raw_sql(query) as con: - tables = con.fetchall() + with self._safe_raw_sql(sg_expr) as con: + views = con.fetchall() - return self._filter_with_like([table for (table,) in tables], like=like) + return self._filter_with_like([view for (view,) in views], like=like) def get_schema( self, diff --git a/ibis/backends/flink/__init__.py b/ibis/backends/flink/__init__.py index e0b58e34cc15..c44f0fcec408 100644 --- a/ibis/backends/flink/__init__.py +++ b/ibis/backends/flink/__init__.py @@ -143,7 +143,7 @@ def drop_database( statement = DropDatabase(name=name, catalog=catalog, must_exist=not force) self.raw_sql(statement.compile()) - def list_tables( + def list( self, like: str | None = None, *, @@ -151,68 +151,116 @@ def list_tables( catalog: str | None = None, temp: bool = False, ) -> list[str]: - """Return the list of table/view names. + """List the names of tables and views in the database. - Return the list of table/view names in the `database` and `catalog`. If - `database`/`catalog` are not specified, their default values will be - used. Temporary tables can only be listed for the default database and - catalog, hence `database` and `catalog` are ignored if `temp` is True. + Returns the list of names for all the temporary or permanent + tables/views in the `database` and `catalog`. If `database` + and `catalog` are not specified, their default values will be + used. If `temp` is True, permanent tables/views get excluded, + and `database` and `catalog` arguments are ignored as + temporary tables/views can only be listed for the default + database and catalog. Parameters ---------- like : str, optional A pattern in Python's regex format. - temp : bool, optional - Whether to list temporary tables or permanent tables. database : str, optional The database to list tables of, if not the current one. catalog : str, optional The catalog to list tables of, if not the current one. + temp : bool, optional + Whether to list only temporary tables/views (True) or + both temporary and permanent tables/views (False). Returns ------- list[str] - The list of the table/view names that match the pattern `like`. - + The list of table/view names that match the pattern `like`. """ catalog = catalog or self.current_catalog database = database or self.current_database # The following is equivalent to the SQL query string `SHOW TABLES FROM|IN`, # but executing the SQL string directly yields a `TableResult` object + # Note (mehmet): `list_***_tables()` returns both tables and views. + # Ref: Docstring for pyflink/table/table_environment.py:list_tables() if temp: # Note (mehmet): TableEnvironment does not provide a function to list # the temporary tables in a given catalog and database. # Ref: https://nightlies.apache.org/flink/flink-docs-master/api/java/org/apache/flink/table/api/TableEnvironment.html - tables = self._table_env.list_temporary_tables() + tables_and_views = self._table_env.list_temporary_tables() else: - # Note (mehmet): `listTables` returns both tables and views. - # Ref: Docstring for pyflink/table/table_environment.py:list_tables() - tables = self._table_env._j_tenv.listTables(catalog, database) + tables_and_views = self._table_env._j_tenv.listTables(catalog, database) - return self._filter_with_like(tables, like) + return self._filter_with_like(tables_and_views, like) - def list_views( + def list_tables( self, like: str | None = None, + *, + database: str | None = None, + catalog: str | None = None, temp: bool = False, ) -> list[str]: - """Return the list of view names. + """List the names of tables in the database. - Return the list of view names. + Returns the list of names for all the temporary or permanent + tables in the `database` and `catalog`. If `database` and + `catalog` are not specified, their default values will be + used. If `temp` is True, permanent tables get excluded, and + `database` and `catalog` arguments are ignored as temporary + tables can only be listed for the default database and + catalog. Parameters ---------- like : str, optional A pattern in Python's regex format. + database : str, optional + The database to list tables of, if not the current one. + catalog : str, optional + The catalog to list tables of, if not the current one. temp : bool, optional - Whether to list temporary views or permanent views. + Whether to list only temporary tables (True) or + both temporary and permanent tables (False). Returns ------- list[str] - The list of the view names that match the pattern `like`. + The list of table names that match the pattern `like`. + """ + tables_and_views = self.list( + like=like, + database=database, + catalog=catalog, + temp=temp, + ) + views = self.list_views(like=like, temp=temp) + + # Note: Flink does not allow creating a view (virtual table) + # using the name of an existing table. + return list(set(tables_and_views) - set(views)) + def list_views( + self, + like: str | None = None, + temp: bool = False, + ) -> list[str]: + """List the names of views. + + Parameters + ---------- + like : str, optional + A pattern in Python's regex format. + temp : bool, optional + Whether to list only the temporary views (True) or both + temporary and permanent views (False). + + Returns + ------- + list[str] + The list of view names that match the pattern `like`. """ if temp: @@ -657,9 +705,11 @@ def create_view( ), expression=query_expression, exists=force, - properties=sge.Properties(expressions=[sge.TemporaryProperty()]) - if temp - else None, + properties=( + sge.Properties(expressions=[sge.TemporaryProperty()]) + if temp + else None + ), ) self.raw_sql(stmt.sql(self.name)) @@ -998,6 +1048,7 @@ def _from_pyflink_table_to_pyarrow_batches( from pyflink.table.types import create_arrow_schema from ibis.backends.flink.datatypes import get_field_data_types + # Note (mehmet): Implementation of this is based on # pyflink/table/table.py: to_pandas(). diff --git a/ibis/backends/flink/tests/test_ddl.py b/ibis/backends/flink/tests/test_ddl.py index 44742bc7c821..4ae330040eb5 100644 --- a/ibis/backends/flink/tests/test_ddl.py +++ b/ibis/backends/flink/tests/test_ddl.py @@ -27,8 +27,8 @@ def generate_tempdir_configs(tempdir): @pytest.mark.parametrize("temp", [True, False]) def test_list_tables(con, temp): - assert len(con.list_tables(temp=temp)) - assert con.list_tables(catalog="default_catalog", database="default_database") + assert len(con.list(temp=temp)) + assert con.list(catalog="default_catalog", database="default_database") def test_create_table_from_schema( @@ -132,7 +132,7 @@ def test_recreate_in_mem_table(con, schema, table_name, temp_table, csv_source_c temp=True, ) try: - assert temp_table in con.list_tables() + assert temp_table in con.list() if schema is not None: assert new_table.schema() == schema @@ -175,7 +175,7 @@ def test_force_recreate_in_mem_table(con, schema_props, temp_table, csv_source_c temp=True, ) try: - assert temp_table in con.list_tables() + assert temp_table in con.list() if schema is not None: assert new_table.schema() == schema @@ -188,7 +188,7 @@ def test_force_recreate_in_mem_table(con, schema_props, temp_table, csv_source_c temp=True, overwrite=True, ) - assert temp_table in con.list_tables() + assert temp_table in con.list() if schema is not None: assert new_table.schema() == schema finally: @@ -299,7 +299,7 @@ def test_create_view( temp=temp, overwrite=False, ) - view_list = sorted(con.list_tables()) + view_list = sorted(con.list_views()) assert temp_view in view_list # Try to re-create the same view with `force=False` @@ -311,7 +311,7 @@ def test_create_view( temp=temp, overwrite=False, ) - assert view_list == sorted(con.list_tables()) + assert view_list == sorted(con.list_views()) # Try to re-create the same view with `force=True` con.create_view( @@ -321,7 +321,7 @@ def test_create_view( temp=temp, overwrite=False, ) - assert view_list == sorted(con.list_tables()) + assert view_list == sorted(con.list_views()) # Overwrite the view con.create_view( @@ -331,10 +331,10 @@ def test_create_view( temp=temp, overwrite=True, ) - assert view_list == sorted(con.list_tables()) + assert view_list == sorted(con.list_views()) con.drop_view(name=temp_view, temp=temp, force=True) - assert temp_view not in con.list_tables() + assert temp_view not in con.list_views() def test_rename_table(con, awards_players_schema, temp_table, csv_source_configs): diff --git a/ibis/backends/impala/__init__.py b/ibis/backends/impala/__init__.py index 0371d3fffe8c..a7832278cd32 100644 --- a/ibis/backends/impala/__init__.py +++ b/ibis/backends/impala/__init__.py @@ -208,8 +208,27 @@ def list_databases(self, like=None): databases = fetchall(cur) return self._filter_with_like(databases.name.tolist(), like) + def list(self, like=None, database=None): + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern in Python's regex format. + database + The database from which to list tables/views. + If not provided, the current database is used. + + Returns + ------- + list[str] + The list of table/view names that match the + pattern `like`. + """ + return self.list_tables(like=like, database=database) + def list_tables(self, like=None, database=None): - """Return the list of table names in the current database. + """List the names of tables in the database. Parameters ---------- @@ -225,6 +244,12 @@ def list_tables(self, like=None, database=None): The list of the table names that match the pattern `like`. """ + # TODO (mehmet): Impala SQL does not seem to have a way to list + # the views. We could run DESCRIBE FORMATTED query for each + # table and check if it is view. But this would obviously be + # costly. + # Ref: https://impala.apache.org/docs/build/html/topics/impala_describe.html + statement = "SHOW TABLES" if database is not None: statement += f" IN {database}" diff --git a/ibis/backends/mssql/__init__.py b/ibis/backends/mssql/__init__.py index 0bde35b60b52..cf591c9903da 100644 --- a/ibis/backends/mssql/__init__.py +++ b/ibis/backends/mssql/__init__.py @@ -358,13 +358,88 @@ def drop_database( if should_switch_catalog: cur.execute(f"USE {self._quote(current_catalog)}") + def _list_tables_and_views_or_only_views( + self, + views_only: bool, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """Helper function for `list_tables/views()`.""" + + table_loc = self._warn_and_create_table_loc(database, schema) + catalog, db = self._to_catalog_db_tuple(table_loc) + conditions = [] + + if table_loc is not None: + conditions.append(C.table_schema.eq(sge.convert(db))) + + sg_expr = ( + sg.select("table_name") + .from_( + sg.table( + "views" if views_only else "tables", + db="information_schema", + catalog=catalog if catalog is not None else self.current_catalog, + ) + ) + .distinct() + ) + + if conditions: + sg_expr = sg_expr.where(*conditions) + + sql = sg_expr.sql(self.dialect) + with self._safe_raw_sql(sql) as cur: + out = cur.fetchall() + + return self._filter_with_like(map(itemgetter(0), out), like) + + def list( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Table/view location. If not passed, uses the current + catalog and database. + + To specify a table/view in a separate catalog, you can + pass in the catalog and database as a string + `"catalog.database"`, or as a tuple of strings + `("catalog", "database")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema inside `database` to perform the list against. + """ + return self._list_tables_and_views_or_only_views( + views_only=False, like=like, database=database, schema=schema + ) + def list_tables( self, like: str | None = None, database: tuple[str, str] | str | None = None, schema: str | None = None, ) -> list[str]: - """List the tables in the database. + """List the names of tables in the database. ::: {.callout-note} ## Ibis does not use the word `schema` to refer to database hierarchy. @@ -390,34 +465,53 @@ def list_tables( schema [deprecated] The schema inside `database` to perform the list against. """ - table_loc = self._warn_and_create_table_loc(database, schema) - catalog, db = self._to_catalog_db_tuple(table_loc) - conditions = [] + tables_and_views = self.list( + like=like, + database=database, + schema=schema, + ) + views = self.list_views(like=like, database=database, schema=schema) - if table_loc is not None: - conditions.append(C.table_schema.eq(sge.convert(db))) + # Note: Mssql does not allow creating a view (virtual table) + # using the name of an existing table. + return list(set(tables_and_views) - set(views)) - sql = ( - sg.select("table_name") - .from_( - sg.table( - "tables", - db="information_schema", - catalog=catalog if catalog is not None else self.current_catalog, - ) - ) - .distinct() - ) + def list_views( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of views in the database. - if conditions: - sql = sql.where(*conditions) + Parameters + ---------- + like + A pattern to use for listing views. + database + View location. If not passed, uses the current catalog and database. - sql = sql.sql(self.dialect) + To specify a view in a separate catalog, you can pass in the + catalog and database as a string `"catalog.database"`, or as a tuple of + strings `("catalog", "database")`. - with self._safe_raw_sql(sql) as cur: - out = cur.fetchall() + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. - return self._filter_with_like(map(itemgetter(0), out), like) + A collection of views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema inside `database` to perform the list against. + """ + + return self._list_tables_and_views_or_only_views( + views_only=True, like=like, database=database, schema=schema + ) def list_databases( self, like: str | None = None, catalog: str | None = None diff --git a/ibis/backends/mssql/tests/test_client.py b/ibis/backends/mssql/tests/test_client.py index 576a4c04ed93..1051f0d04efe 100644 --- a/ibis/backends/mssql/tests/test_client.py +++ b/ibis/backends/mssql/tests/test_client.py @@ -114,8 +114,8 @@ def test_glorious_length_function_hack(con, string): assert result == len(string) -def test_list_tables_schema_warning_refactor(con): - assert set(con.list_tables()) >= { +def test_list_schema_warning_refactor(con): + assert set(con.list()) >= { "astronauts", "awards_players", "batting", @@ -127,10 +127,7 @@ def test_list_tables_schema_warning_refactor(con): restore_tables = ["restorefile", "restorefilegroup", "restorehistory"] with pytest.warns(FutureWarning): - assert ( - con.list_tables(database="msdb", schema="dbo", like="restore") - == restore_tables - ) + assert con.list(database="msdb", schema="dbo", like="restore") == restore_tables - assert con.list_tables(database="msdb.dbo", like="restore") == restore_tables - assert con.list_tables(database=("msdb", "dbo"), like="restore") == restore_tables + assert con.list(database="msdb.dbo", like="restore") == restore_tables + assert con.list(database=("msdb", "dbo"), like="restore") == restore_tables diff --git a/ibis/backends/mysql/__init__.py b/ibis/backends/mysql/__init__.py index dd9ef3056d58..0967c5fff444 100644 --- a/ibis/backends/mysql/__init__.py +++ b/ibis/backends/mysql/__init__.py @@ -269,36 +269,15 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: con.commit() return cursor - # TODO: disable positional arguments - def list_tables( + def _list_tables_and_views_or_only_views( self, + views_only: bool, like: str | None = None, schema: str | None = None, database: tuple[str, str] | str | None = None, ) -> list[str]: - """List the tables in the database. - - ::: {.callout-note} - ## Ibis does not use the word `schema` to refer to database hierarchy. - - A collection of tables is referred to as a `database`. - A collection of `database` is referred to as a `catalog`. - - These terms are mapped onto the corresponding features in each - backend (where available), regardless of whether the backend itself - uses the same terminology. - ::: + """Helper functions for `list_tables/views()`.""" - Parameters - ---------- - like - A pattern to use for listing tables. - schema - [deprecated] The schema to perform the list against. - database - Database to list tables from. Default behavior is to show tables in - the current database (``self.current_database``). - """ if schema is not None: self._warn_schema() @@ -329,10 +308,10 @@ def list_tables( sg_db.args["quoted"] = False conditions = [C.table_schema.eq(sge.convert(table_loc.sql(self.name)))] - col = "table_name" + table = "views" if views_only else "tables" sql = ( - sg.select(col) - .from_(sg.table("tables", db="information_schema")) + sg.select("table_name") + .from_(sg.table(table, db="information_schema")) .distinct() .where(*conditions) .sql(self.name) @@ -343,6 +322,116 @@ def list_tables( return self._filter_with_like(map(itemgetter(0), out), like) + def list( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database + (``self.current_database``). + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + return self._list_tables_and_views_or_only_views( + views_only=False, + like=like, + schema=schema, + database=database, + ) + + # TODO: disable positional arguments + def list_tables( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables from. Default behavior is to show tables in + the current database (``self.current_database``). + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + tables_and_views = self.list(like=like, schema=schema, database=database) + views = self.list_views(like=like, schema=schema, database=database) + + # Note: Mysql does not allow creating a view using + # the name of an existing table. + return list(set(tables_and_views) - set(views)) + + def list_views( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + schema + [deprecated] The schema to perform the list against. + database + Database to list views from. Default behavior is to show + views in the current database (``self.current_database``). + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + return self._list_tables_and_views_or_only_views( + views_only=True, + like=like, + schema=schema, + database=database, + ) + def execute( self, expr: ir.Expr, limit: str | None = "default", **kwargs: Any ) -> Any: diff --git a/ibis/backends/oracle/__init__.py b/ibis/backends/oracle/__init__.py index 1107a5bc2cbc..4f8cd0c32e57 100644 --- a/ibis/backends/oracle/__init__.py +++ b/ibis/backends/oracle/__init__.py @@ -203,37 +203,14 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: con.commit() return cursor - def list_tables( + def _list_tables_or_views( self, + views: bool, like: str | None = None, schema: str | None = None, database: tuple[str, str] | str | None = None, ) -> list[str]: - """List the tables in the database. - - ::: {.callout-note} - ## Ibis does not use the word `schema` to refer to database hierarchy. - - A collection of tables is referred to as a `database`. - A collection of `database` is referred to as a `catalog`. - - These terms are mapped onto the corresponding features in each - backend (where available), regardless of whether the backend itself - uses the same terminology. - ::: - - Parameters - ---------- - like - A pattern to use for listing tables. - schema - [deprecated] The schema to perform the list against. - database - Database to list tables from. Default behavior is to show tables in - the current database. - - - """ + """Helper function for `list_tables/views()`.""" if schema is not None and database is not None: raise exc.IbisInputError( "Using both the `schema` and `database` kwargs is not supported. " @@ -260,25 +237,110 @@ def list_tables( # as literal strings and those are rendered correctly. conditions = C.owner.eq(sge.convert(table_loc.sql(self.name))) - tables = ( - sg.select("table_name", "owner") - .from_(sg.table("all_tables")) - .distinct() - .where(conditions) - ) - views = ( - sg.select("view_name", "owner") - .from_(sg.table("all_views")) - .distinct() - .where(conditions) - ) - sql = tables.union(views).sql(self.name) + if views: + sg_expr = ( + sg.select("view_name", "owner") + .from_(sg.table("all_views")) + .distinct() + .where(conditions) + ) + else: + sg_expr = ( + sg.select("table_name", "owner") + .from_(sg.table("all_tables")) + .distinct() + .where(conditions) + ) + sql = sg_expr.sql(self.name) with self._safe_raw_sql(sql) as cur: out = cur.fetchall() return self._filter_with_like(map(itemgetter(0), out), like) + def list( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + return self.list_tables( + like=like, schema=schema, database=database + ) + self.list_views(like=like, schema=schema, database=database) + + def list_tables( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables from. Default behavior is to show tables in + the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + return self._list_tables_or_views( + views=False, + like=like, + schema=schema, + database=database, + ) + + def list_views( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of views in the database.""" + return self._list_tables_or_views( + views=True, + like=like, + schema=schema, + database=database, + ) + def list_databases( self, like: str | None = None, catalog: str | None = None ) -> list[str]: diff --git a/ibis/backends/oracle/tests/test_client.py b/ibis/backends/oracle/tests/test_client.py index 5da761552eea..43f7ceb54fb2 100644 --- a/ibis/backends/oracle/tests/test_client.py +++ b/ibis/backends/oracle/tests/test_client.py @@ -62,12 +62,12 @@ def stats_one_way_anova(x, y, value: str) -> int: def test_list_tables_schema_warning_refactor(con): - assert con.list_tables() + assert con.list() with pytest.raises(exc.IbisInputError): - con.list_tables(database="not none", schema="not none") + con.list(database="not none", schema="not none") with pytest.warns(FutureWarning): - assert con.list_tables(schema="SYS", like="EXU8OPT") == ["EXU8OPT"] + assert con.list(schema="SYS", like="EXU8OPT") == ["EXU8OPT"] - assert con.list_tables(database="SYS", like="EXU8OPT") == ["EXU8OPT"] + assert con.list(database="SYS", like="EXU8OPT") == ["EXU8OPT"] diff --git a/ibis/backends/pandas/__init__.py b/ibis/backends/pandas/__init__.py index 646bb0c49764..8a419296e073 100644 --- a/ibis/backends/pandas/__init__.py +++ b/ibis/backends/pandas/__init__.py @@ -148,8 +148,25 @@ def read_parquet( def version(self) -> str: return pd.__version__ + def list(self, like=None, database=None): + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern in Python's regex format. + database + Unused in the pandas backend. + + Returns + ------- + list[str] + The list of table names that match the pattern `like`. + """ + return self.list_tables() + def list_tables(self, like=None, database=None): - """Return the list of table names in the current database. + """List the names of tables in the database. Parameters ---------- @@ -229,6 +246,9 @@ def create_view( database: str | None = None, overwrite: bool = False, ) -> ir.Table: + # TODO (mehmet): Should we remove this to decouple + # views from tables across all backends? + # E.g., `polars` backend does not implement this. return self.create_table( name, obj=obj, temp=None, database=database, overwrite=overwrite ) diff --git a/ibis/backends/pandas/executor.py b/ibis/backends/pandas/executor.py index 7e1886b408ed..e3f052cfda5d 100644 --- a/ibis/backends/pandas/executor.py +++ b/ibis/backends/pandas/executor.py @@ -290,6 +290,7 @@ def visit(cls, op: ops.ArgMin | ops.ArgMax, arg, key, where): def agg(df): indices = func(df[key.name]) return df[arg.name].iloc[indices] + else: def agg(df): @@ -316,6 +317,7 @@ def visit(cls, op: ops.Correlation, left, right, where, how): def agg(df): return df[left.name].corr(df[right.name]) + else: def agg(df): @@ -333,6 +335,7 @@ def visit(cls, op: ops.Covariance, left, right, where, how): def agg(df): return df[left.name].cov(df[right.name], ddof=ddof) + else: def agg(df): @@ -349,6 +352,7 @@ def visit(cls, op: ops.GroupConcat, arg, sep, where): def agg(df): return sep.join(df[arg.name].astype(str)) + else: def agg(df): diff --git a/ibis/backends/pandas/helpers.py b/ibis/backends/pandas/helpers.py index bb8550b7c8bc..4f0434f7d21d 100644 --- a/ibis/backends/pandas/helpers.py +++ b/ibis/backends/pandas/helpers.py @@ -72,6 +72,7 @@ def agg(cls, func, arg_column, where_column): def applier(df): return func(df[arg_column.name]) + else: def applier(df): diff --git a/ibis/backends/polars/__init__.py b/ibis/backends/polars/__init__.py index 72e80dc7ae7a..08a4103af927 100644 --- a/ibis/backends/polars/__init__.py +++ b/ibis/backends/polars/__init__.py @@ -68,7 +68,41 @@ def disconnect(self) -> None: def version(self) -> str: return pl.__version__ + def list(self, like=None, database=None): + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern in Python's regex format. + database + Unused in the polars backend. + + Returns + ------- + list[str] + The list of table names that match the pattern `like`. + """ + # TODO (mehmet): Added this function explicitly for docs + # purposes. Would it be cleaner to defined it as `self.list = + # self.list_tables`? + return self.list_tables(like=like, database=database) + def list_tables(self, like=None, database=None): + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern in Python's regex format. + database + Unused in the polars backend. + + Returns + ------- + list[str] + The list of table names that match the pattern `like`. + """ return self._filter_with_like(list(self._tables.keys()), like) def table(self, name: str, _schema: sch.Schema | None = None) -> ir.Table: diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py index 1deaa654b895..8aef0d39d340 100644 --- a/ibis/backends/postgres/__init__.py +++ b/ibis/backends/postgres/__init__.py @@ -120,7 +120,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: ) # only register if we haven't already done so - if (name := op.name) not in self.list_tables(): + if (name := op.name) not in self.list(): quoted = self.compiler.quoted column_defs = [ sg.exp.ColumnDef( @@ -297,35 +297,15 @@ def do_connect( with self.begin() as cur: cur.execute("SET TIMEZONE = UTC") - def list_tables( + def _list_tables_and_views_or_only_views( self, + views_only: bool, like: str | None = None, schema: str | None = None, database: tuple[str, str] | str | None = None, ) -> list[str]: - """List the tables in the database. - - ::: {.callout-note} - ## Ibis does not use the word `schema` to refer to database hierarchy. - - A collection of tables is referred to as a `database`. - A collection of `database` is referred to as a `catalog`. + """Helper function for `list/list_tables/list_views()`.""" - These terms are mapped onto the corresponding features in each - backend (where available), regardless of whether the backend itself - uses the same terminology. - ::: - - Parameters - ---------- - like - A pattern to use for listing tables. - schema - [deprecated] The schema to perform the list against. - database - Database to list tables from. Default behavior is to show tables in - the current database. - """ if schema is not None: self._warn_schema() @@ -358,9 +338,10 @@ def list_tables( catalog = catalog.sql(dialect=self.name) conditions.append(C.table_catalog.eq(sge.convert(catalog))) + table = "views" if views_only else "tables" sql = ( sg.select("table_name") - .from_(sg.table("tables", db="information_schema")) + .from_(sg.table(table, db="information_schema")) .distinct() .where(*conditions) .sql(self.dialect) @@ -376,6 +357,115 @@ def list_tables( return self._filter_with_like(map(itemgetter(0), out), like) + def list( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + + return self._list_tables_and_views_or_only_views( + views_only=False, + like=like, + schema=schema, + database=database, + ) + + def list_tables( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + schema + [deprecated] The schema to perform the list against. + database + Database to list tables from. Default behavior is to show tables in + the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + tables_and_views = self.list(like=like, schema=schema, database=database) + views = self.list_views(like=like, schema=schema, database=database) + + # Note: Postgres does not allow creating a view with + # the name of an existing table. + return list(set(tables_and_views) - set(views)) + + def list_views( + self, + like: str | None = None, + schema: str | None = None, + database: tuple[str, str] | str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + schema + [deprecated] The schema to perform the list against. + database + Database to list views from. Default behavior is to show views in + the current database. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + """ + return self._list_tables_and_views_or_only_views( + views_only=True, + like=like, + schema=schema, + database=database, + ) + def _fetch_temp_tables(self): # postgres temporary tables are stored in a separate schema # so we need to independently grab them and return them along with diff --git a/ibis/backends/postgres/tests/test_client.py b/ibis/backends/postgres/tests/test_client.py index 6a2d635786d4..a98d7fabbf9e 100644 --- a/ibis/backends/postgres/tests/test_client.py +++ b/ibis/backends/postgres/tests/test_client.py @@ -61,15 +61,15 @@ def test_simple_aggregate_execute(alltypes): assert isinstance(v, float) -def test_list_tables(con): +def test_list(con): assert len(con.list_tables(like="functional")) == 1 assert {"astronauts", "batting", "diamonds"} <= set(con.list_tables()) _ = con.create_table("tempy", schema=ibis.schema(dict(id="int")), temp=True) - assert "tempy" in con.list_tables() + assert "tempy" in con.list() # temp tables only show up when database='public' (or default) - assert "tempy" not in con.list_tables(database="tiger") + assert "tempy" not in con.list(database="tiger") def test_compile_toplevel(assert_sql): @@ -192,7 +192,7 @@ def test_unknown_column_type(con, col): def test_insert_with_cte(con): X = con.create_table("X", schema=ibis.schema(dict(id="int")), temp=True) - assert "X" in con.list_tables() + assert "X" in con.list() expr = X.join(X.mutate(a=X["id"] + 1), ["id"]) Y = con.create_table("Y", expr, temp=True) assert Y.execute().empty diff --git a/ibis/backends/pyspark/__init__.py b/ibis/backends/pyspark/__init__.py index 98f03c53ec7d..fccb3653418c 100644 --- a/ibis/backends/pyspark/__init__.py +++ b/ibis/backends/pyspark/__init__.py @@ -267,23 +267,43 @@ def list_databases( ] return self._filter_with_like(databases, like) + def list( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + """ + + return self.list_tables(like=like, database=database) + self.list_views( + like=like, database=database + ) + def list_tables( - self, like: str | None = None, database: str | None = None + self, + like: str | None = None, + database: str | None = None, ) -> list[str]: - """List the tables in the database. + """List the names of tables in the database. Parameters ---------- like A pattern to use for listing tables. database - Database to list tables from. Default behavior is to show tables in - the current catalog and database. - - To specify a table in a separate catalog, you can pass in the - catalog and database as a string `"catalog.database"`, or as a tuple of - strings `("catalog", "database")`. + Database to list tables from. Default behavior is to show + tables in the current database. """ + view_set = set(self.list_views(database=database)) + table_loc = self._to_sqlglot_table(database) catalog, db = self._to_catalog_db_tuple(table_loc) with self._active_catalog(catalog): @@ -292,9 +312,39 @@ def list_tables( for row in self._session.sql( f"SHOW TABLES IN {db or self.current_database}" ).collect() + if row.tableName not in view_set ] + return self._filter_with_like(tables, like) + def list_views( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + database + Database to list views from. Default behavior is to show + views in the current database. + """ + + view_loc = self._to_sqlglot_table(database) + catalog, db = self._to_catalog_db_tuple(view_loc) + with self._active_catalog(catalog): + views = [ + row.viewName + for row in self._session.sql( + f"SHOW VIEWS IN {db or self.current_database}" + ).collect() + ] + + return self._filter_with_like(views, like) + def _wrap_udf_to_return_pandas(self, func, output_dtype): def wrapper(*args): return _coerce_to_series(func(*args), output_dtype) diff --git a/ibis/backends/pyspark/converter.py b/ibis/backends/pyspark/converter.py index 3258a1c1cb50..99ddb06989c4 100644 --- a/ibis/backends/pyspark/converter.py +++ b/ibis/backends/pyspark/converter.py @@ -30,6 +30,7 @@ def converter(value): return value.astimezone(tz).replace(tzinfo=None) except TypeError: return value.tz_localize(tz).replace(tzinfo=None) + else: tz = normalize_timezone(dtype.timezone) diff --git a/ibis/backends/pyspark/tests/conftest.py b/ibis/backends/pyspark/tests/conftest.py index 79664c5752a1..93058387cbbb 100644 --- a/ibis/backends/pyspark/tests/conftest.py +++ b/ibis/backends/pyspark/tests/conftest.py @@ -35,8 +35,20 @@ def _load_data(self, **_: Any) -> None: t = s.read.parquet(path).repartition(num_partitions) if (sort_col := sort_cols.get(name)) is not None: t = t.sort(sort_col) + # TODO (mehmet): Why are all created as views here? + # Why not use `self.connection.create_table()`? + # Update: It seems temporary tables are not allowed + # in Spark and creating tables here instead of views + # would require deleting them at the end of the tests. + # Is this the reason they were created as views in + # the first place? t.createOrReplaceTempView(name) + # TODO (mehmet): Getting the dataframe with `t.toPandas()` + # to maintain the outcome of `repartition()` and `sort()` + # performed above. Is this really necessary? + # self.connection.create_table(name=name, obj=t.toPandas(), overwrite=True) + s.createDataFrame([(1, "a")], ["foo", "bar"]).createOrReplaceTempView("simple") s.createDataFrame( diff --git a/ibis/backends/pyspark/tests/test_ddl.py b/ibis/backends/pyspark/tests/test_ddl.py index 834b61aa517a..8f0002eacd3a 100644 --- a/ibis/backends/pyspark/tests/test_ddl.py +++ b/ibis/backends/pyspark/tests/test_ddl.py @@ -26,7 +26,7 @@ def test_create_exists_view(con, alltypes, temp_view): t1 = alltypes.group_by("string_col").size() t2 = con.create_view(temp_view, t1) - assert temp_view in con.list_tables() + assert temp_view in con.list_views() # just check it works for now assert t2.execute() is not None diff --git a/ibis/backends/snowflake/__init__.py b/ibis/backends/snowflake/__init__.py index db227cfe9789..60634cba5d7c 100644 --- a/ibis/backends/snowflake/__init__.py +++ b/ibis/backends/snowflake/__init__.py @@ -578,13 +578,50 @@ def list_databases( return self._filter_with_like(schemata, like) + def list( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Table location. If not passed, uses the current catalog and database. + + To specify a table in a separate Snowflake catalog, you can pass in the + catalog and database as a string `"catalog.database"`, or as a tuple of + strings `("catalog", "database")`. + + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of tables/views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema inside `database` to perform the list against. + """ + + return self.list_tables( + like=like, database=database, schema=schema + ) + self.list_views(like=like, database=database, schema=schema) + def list_tables( self, like: str | None = None, database: tuple[str, str] | str | None = None, schema: str | None = None, ) -> list[str]: - """List the tables in the database. + """List the names of tables in the database. ::: {.callout-note} ## Ibis does not use the word `schema` to refer to database hierarchy. @@ -611,20 +648,56 @@ def list_tables( [deprecated] The schema inside `database` to perform the list against. """ table_loc = self._warn_and_create_table_loc(database, schema) + query = "SHOW TABLES" + if table_loc is not None: + query += f" IN {table_loc}" - tables_query = "SHOW TABLES" - views_query = "SHOW VIEWS" + with self.con.cursor() as cur: + tables = list(map(itemgetter(1), cur.execute(query))) + + return self._filter_with_like(tables, like=like) + + def list_views( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + database + Table location. If not passed, uses the current catalog and database. + + To specify a table in a separate Snowflake catalog, you can pass in the + catalog and database as a string `"catalog.database"`, or as a tuple of + strings `("catalog", "database")`. + ::: {.callout-note} + ## Ibis does not use the word `schema` to refer to database hierarchy. + + A collection of views is referred to as a `database`. + A collection of `database` is referred to as a `catalog`. + + These terms are mapped onto the corresponding features in each + backend (where available), regardless of whether the backend itself + uses the same terminology. + ::: + schema + [deprecated] The schema inside `database` to perform the list against. + """ + table_loc = self._warn_and_create_table_loc(database, schema) + query = "SHOW VIEWS" if table_loc is not None: - tables_query += f" IN {table_loc}" - views_query += f" IN {table_loc}" + query += f" IN {table_loc}" with self.con.cursor() as cur: - # TODO: considering doing this with a single query using information_schema - tables = list(map(itemgetter(1), cur.execute(tables_query))) - views = list(map(itemgetter(1), cur.execute(views_query))) + views = list(map(itemgetter(1), cur.execute(query))) - return self._filter_with_like(tables + views, like=like) + return self._filter_with_like(views, like=like) def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: import pyarrow.parquet as pq diff --git a/ibis/backends/sqlite/__init__.py b/ibis/backends/sqlite/__init__.py index 960729ea894d..d2690cdfb61b 100644 --- a/ibis/backends/sqlite/__init__.py +++ b/ibis/backends/sqlite/__init__.py @@ -123,21 +123,14 @@ def list_databases(self, like: str | None = None) -> list[str]: return sorted(self._filter_with_like(results, like)) - def list_tables( + def _list_tables_or_views( self, + views: bool, like: str | None = None, database: str | None = None, ) -> list[str]: - """List the tables in the database. + """Helper function for `list_tables/views()`.""" - Parameters - ---------- - like - A pattern to use for listing tables. - database - Database to list tables from. Default behavior is to show tables in - the current database. - """ if database is None: database = "main" @@ -146,7 +139,7 @@ def list_tables( .from_(F.pragma_table_list()) .where( C.schema.eq(sge.convert(database)), - C.type.isin(sge.convert("table"), sge.convert("view")), + C.type.isin(sge.convert("view") if views else sge.convert("table")), ~( C.name.isin( sge.convert("sqlite_schema"), @@ -163,6 +156,70 @@ def list_tables( return sorted(self._filter_with_like(results, like)) + def list( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + Database to list tables/views from. Default behavior is to + show tables/views in the current database. + """ + + return self.list_tables(like=like, database=database) + self.list_views( + like=like, database=database + ) + + def list_tables( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of tables in the database. + + Parameters + ---------- + like + A pattern to use for listing tables. + database + Database to list tables from. Default behavior is to show + tables in the current database. + """ + + return self._list_tables_or_views( + views=False, + like=like, + database=database, + ) + + def list_views( + self, + like: str | None = None, + database: str | None = None, + ) -> list[str]: + """List the names of views in the database. + + Parameters + ---------- + like + A pattern to use for listing views. + database + Database to list views from. Default behavior is to show + views in the current database. + """ + + return self._list_tables_or_views( + views=True, + like=like, + database=database, + ) + def _parse_type(self, typ: str, nullable: bool) -> dt.DataType: typ = typ.lower() try: diff --git a/ibis/backends/tests/test_api.py b/ibis/backends/tests/test_api.py index 4b71bdb4ffee..7e298828bdf8 100644 --- a/ibis/backends/tests/test_api.py +++ b/ibis/backends/tests/test_api.py @@ -59,15 +59,80 @@ def test_catalog_consistency(backend, con): assert current_catalog in catalogs -def test_list_tables(con): - tables = con.list_tables() - assert isinstance(tables, list) - # only table that is guaranteed to be in all backends +def test_list(con): + tabulars = con.list() + assert isinstance(tabulars, list) + + # Only tabular that is guaranteed to be in all backends key = "functional_alltypes" - assert key in tables or key.upper() in tables - assert all(isinstance(table, str) for table in tables) + assert key in tabulars or key.upper() in tabulars + assert all(isinstance(tabular, str) for tabular in tabulars) + + assert set(con.list_tables()) <= set(tabulars) + + +@pytest.mark.broken( + ["bigquery"], + raises=AssertionError, + reason=( + "Bigquery does not allow for listing only the views. " + "See the TODO added in the docstring of `list_tables()` " + "for more context" + ), +) +@pytest.mark.broken( + ["pyspark"], + raises=AssertionError, + reason=( + "All test tables, including `functional_alltypes` are " + "created as views for pyspark backend." + ), +) +@pytest.mark.broken( + ["flink"], + raises=AssertionError, + reason="In Flink backend, we create in-memory objects as views", +) +@pytest.mark.notimpl( + ["dask", "datafusion", "druid", "impala", "pandas", "polars", "trino"], + raises=AttributeError, + reason="Backend does not implement `list_views()`", +) +def test_list_tables(ddl_con): + table_name = "functional_alltypes" + tables = ddl_con.list_tables() + assert isinstance(tables, list) + assert table_name in tables + + assert table_name not in ddl_con.list_views() +@pytest.mark.notimpl( + ["dask", "datafusion", "impala", "pandas", "polars", "trino"], + raises=AttributeError, + reason="Backend does not implement `list_views()`", +) +@pytest.mark.notimpl( + ["druid"], + raises=PyDruidProgrammingError, + reason="Druid does not allow creating views", +) +def test_list_views(ddl_con, temp_view): + expr = ddl_con.table("functional_alltypes") + ddl_con.create_view(temp_view, expr) + + views = ddl_con.list_views() + assert isinstance(views, list) + assert temp_view in views + + assert temp_view not in ddl_con.list_tables() + + +@pytest.mark.broken( + ["flink", "pyspark"], + raises=AssertionError, + reason="Backend creates the (in-memory) test tables as views", +) def test_tables_accessor_mapping(con): if con.name == "snowflake": pytest.skip("snowflake sometimes counts more tables than are around") @@ -99,6 +164,11 @@ def test_tables_accessor_getattr(con): con.tables._private_attr # noqa: B018 +@pytest.mark.broken( + ["flink", "pyspark"], + raises=AssertionError, + reason="Backend creates the (in-memory) test tables as views", +) def test_tables_accessor_tab_completion(con): name = "functional_alltypes" attrs = dir(con.tables) @@ -109,6 +179,11 @@ def test_tables_accessor_tab_completion(con): assert name in keys +@pytest.mark.broken( + ["flink", "pyspark"], + raises=AssertionError, + reason="Backend creates the (in-memory) test tables as views", +) def test_tables_accessor_repr(con): name = "functional_alltypes" result = repr(con.tables) diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index 5bfd29f52d7d..46fc300182f6 100644 --- a/ibis/backends/tests/test_client.py +++ b/ibis/backends/tests/test_client.py @@ -398,9 +398,9 @@ def test_nullable_input_output(con, temp_table): def test_create_drop_view(ddl_con, temp_view): # setup table_name = "functional_alltypes" - tables = ddl_con.list_tables() + tabulars = ddl_con.list() - if table_name in tables or (table_name := table_name.upper()) in tables: + if table_name in tabulars or (table_name := table_name.upper()) in tabulars: expr = ddl_con.table(table_name) else: raise ValueError(f"table `{table_name}` does not exist") @@ -410,7 +410,7 @@ def test_create_drop_view(ddl_con, temp_view): # create a new view ddl_con.create_view(temp_view, expr) # check if the view was created - assert temp_view in ddl_con.list_tables() + assert temp_view in ddl_con.list() t_expr = ddl_con.table(table_name) v_expr = ddl_con.table(temp_view) diff --git a/ibis/backends/tests/test_register.py b/ibis/backends/tests/test_register.py index 64d8b23792b9..00346c69cf80 100644 --- a/ibis/backends/tests/test_register.py +++ b/ibis/backends/tests/test_register.py @@ -103,7 +103,7 @@ def test_register_csv(con, data_dir, fname, in_table_name, out_table_name): with pytest.warns(FutureWarning, match="v9.1"): table = con.register(fname, table_name=in_table_name) - assert any(out_table_name in t for t in con.list_tables()) + assert any(out_table_name in t for t in con.list()) if con.name != "datafusion": table.count().execute() @@ -226,7 +226,7 @@ def test_register_parquet( with pytest.warns(FutureWarning, match="v9.1"): table = con.register(f"parquet://{fname.name}", table_name=in_table_name) - assert any(out_table_name in t for t in con.list_tables()) + assert any(out_table_name in t for t in con.list()) if con.name != "datafusion": table.count().execute() @@ -273,7 +273,7 @@ def test_register_iterator_parquet( table_name=None, ) - assert any("ibis_read_parquet" in t for t in con.list_tables()) + assert any("ibis_read_parquet" in t for t in con.list()) assert table.count().execute() diff --git a/ibis/backends/trino/__init__.py b/ibis/backends/trino/__init__.py index 89eb08519c63..3921015dd98f 100644 --- a/ibis/backends/trino/__init__.py +++ b/ibis/backends/trino/__init__.py @@ -206,30 +206,61 @@ def list_databases( databases = cur.fetchall() return self._filter_with_like(list(map(itemgetter(0), databases)), like) + def list( + self, + like: str | None = None, + database: tuple[str, str] | str | None = None, + schema: str | None = None, + ) -> list[str]: + """List the names of tables and views in the database. + + Parameters + ---------- + like + A pattern to use for listing tables/views. + database + The database location to perform the list against. + + By default uses the current `database` (`self.current_database`) and + `catalog` (`self.current_catalog`). + + To specify a table/view in a separate catalog, you can pass in the + catalog and database as a string `"catalog.database"`, or as a tuple of + strings `("catalog", "database")`. + schema + [deprecated] The schema inside `database` to perform the list against. + """ + return self.list_tables(like=like, database=database, schema=schema) + def list_tables( self, like: str | None = None, database: tuple[str, str] | str | None = None, schema: str | None = None, ) -> list[str]: - """List the tables in the database. + """List the names of tables and views in the database. Parameters ---------- like - A pattern to use for listing tables. + A pattern to use for listing tables/views. database The database location to perform the list against. By default uses the current `database` (`self.current_database`) and `catalog` (`self.current_catalog`). - To specify a table in a separate catalog, you can pass in the + To specify a table/view in a separate catalog, you can pass in the catalog and database as a string `"catalog.database"`, or as a tuple of strings `("catalog", "database")`. schema [deprecated] The schema inside `database` to perform the list against. """ + + # TODO (mehmet): Trino does not seem to allow for showing + # only the views. + # Ref: https://trino.io/docs/current/sql/show-tables.html + table_loc = self._warn_and_create_table_loc(database, schema) query = "SHOW TABLES" diff --git a/ibis/examples/tests/test_examples.py b/ibis/examples/tests/test_examples.py index 40d1e2bf4afe..e08e736adab8 100644 --- a/ibis/examples/tests/test_examples.py +++ b/ibis/examples/tests/test_examples.py @@ -84,7 +84,7 @@ def test_non_example(): def test_backend_arg(): con = ibis.duckdb.connect() t = ibis.examples.penguins.fetch(backend=con) - assert t.get_name() in con.list_tables() + assert t.get_name() in con.list() @pytest.mark.duckdb