Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ddl): distinguish views from tables while listing them #8864

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- clickhouse

mysql:
platform: linux/amd64
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
MYSQL_DATABASE: ibis_testing
Expand All @@ -38,6 +39,7 @@ services:
- $PWD/docker/mysql:/docker-entrypoint-initdb.d:ro

postgres:
platform: linux/amd64
user: postgres
environment:
POSTGRES_PASSWORD: postgres
Expand All @@ -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!
Expand Down Expand Up @@ -341,6 +344,7 @@ services:
- druid

oracle:
platform: linux/amd64
image: gvenzl/oracle-free:23.4-slim
environment:
ORACLE_PASSWORD: ibis
Expand Down
104 changes: 89 additions & 15 deletions ibis/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -62,29 +69,65 @@
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())

Check warning on line 75 in ibis/backends/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/__init__.py#L75

Added line #L75 was not covered by tests

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:
Expand Down Expand Up @@ -867,7 +910,7 @@
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.

Expand Down Expand Up @@ -966,6 +1009,22 @@

"""

@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.
Expand All @@ -981,6 +1040,21 @@
"""
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:
Expand Down
57 changes: 54 additions & 3 deletions ibis/backends/bigquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,18 +897,56 @@
]
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)

Check warning on line 936 in ibis/backends/bigquery/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/bigquery/__init__.py#L936

Added line #L936 was not covered by tests

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.

Expand All @@ -922,7 +960,7 @@
::: {.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
Expand All @@ -932,6 +970,19 @@
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)
Expand Down
83 changes: 70 additions & 13 deletions ibis/backends/clickhouse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
Loading
Loading