Skip to content

Commit

Permalink
feat(ddl): make list_tables() and list_views() explicit
Browse files Browse the repository at this point in the history
  • Loading branch information
mfatihaktas committed Jun 17, 2024
1 parent 2597829 commit ec6c4e9
Show file tree
Hide file tree
Showing 35 changed files with 1,445 additions and 271 deletions.
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 @@ 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())

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

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

0 comments on commit ec6c4e9

Please sign in to comment.