Skip to content

Commit 356ef91

Browse files
committed
refactor(api): duckdb ddl accessor implementation
1 parent 85c1cb6 commit 356ef91

12 files changed

+308
-189
lines changed

ibis/backends/__init__.py

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import keyword
88
import re
99
import urllib.parse
10-
import weakref
1110
from pathlib import Path
1211
from typing import TYPE_CHECKING, Any, ClassVar
1312

@@ -35,12 +34,6 @@
3534
class TablesAccessor(collections.abc.Mapping):
3635
"""A mapping-like object for accessing tables off a backend.
3736
38-
::: {.callout-note}
39-
## The `tables` accessor is tied to the lifetime of the backend.
40-
41-
If the backend goes out of scope, the `tables` accessor is no longer valid.
42-
:::
43-
4437
Tables may be accessed by name using either index or attribute access:
4538
4639
Examples
@@ -53,6 +46,42 @@ class TablesAccessor(collections.abc.Mapping):
5346
def __init__(self, backend: BaseBackend) -> None:
5447
self._backend = backend
5548

49+
def _execute_if_exists(
50+
self, method_name: str, database=None, like=None
51+
) -> list[str]:
52+
"""Executes method if it exists and it doesn't raise a NotImplementedError, else returns an empty list."""
53+
method = getattr(self._backend.ddl, method_name)
54+
if callable(method):
55+
try:
56+
return method(database=database, like=like)
57+
except NotImplementedError:
58+
pass
59+
return []
60+
61+
def _gather_tables(self, database=None, like=None) -> list[str]:
62+
"""Gathers table names using the list_* methods available on the backend."""
63+
# TODO: break this down into views/tables to be more explicit in repr (see #9859)
64+
# list_* methods that might exist on a given backends.
65+
list_methods = [
66+
"list_tables",
67+
"list_temp_tables",
68+
"list_views",
69+
"list_temp_views",
70+
]
71+
tables = []
72+
for method_name in list_methods:
73+
tables.extend(
74+
self._execute_if_exists(method_name, database=database, like=like)
75+
)
76+
return list(set(tables))
77+
78+
def __call__(self, database=None, like=None):
79+
return self._gather_tables(database, like)
80+
81+
@property
82+
def _tables(self) -> list[str]:
83+
return self._gather_tables()
84+
5685
def __getitem__(self, name) -> ir.Table:
5786
try:
5887
return self._backend.table(name)
@@ -68,29 +97,70 @@ def __getattr__(self, name) -> ir.Table:
6897
raise AttributeError(name) from exc
6998

7099
def __iter__(self) -> Iterator[str]:
71-
return iter(sorted(self._backend.list_tables()))
100+
return iter(sorted(self._tables))
72101

73102
def __len__(self) -> int:
74-
return len(self._backend.list_tables())
103+
return len(self._tables)
75104

76105
def __dir__(self) -> list[str]:
77106
o = set()
78107
o.update(dir(type(self)))
79108
o.update(
80109
name
81-
for name in self._backend.list_tables()
110+
for name in self._tables
82111
if name.isidentifier() and not keyword.iskeyword(name)
83112
)
84113
return list(o)
85114

86115
def __repr__(self) -> str:
87-
tables = self._backend.list_tables()
88116
rows = ["Tables", "------"]
89-
rows.extend(f"- {name}" for name in sorted(tables))
117+
rows.extend(f"- {name}" for name in sorted(self._tables))
90118
return "\n".join(rows)
91119

92120
def _ipython_key_completions_(self) -> list[str]:
93-
return self._backend.list_tables()
121+
return self._tables
122+
123+
124+
class DDLAccessor:
125+
"""ddl accessor list views."""
126+
127+
def __init__(self, backend: BaseBackend) -> None:
128+
self._backend = backend
129+
130+
def _raise_if_not_implemented(self, method_name: str):
131+
method = getattr(self._backend, method_name)
132+
if not callable(method):
133+
raise NotImplementedError(
134+
f"The method {method_name} is not implemented for the {self._backend.name} backend"
135+
)
136+
137+
def list_tables(
138+
self, like: str | None = None, database: tuple[str, str] | str | None = None
139+
) -> list[str]:
140+
"""Return the list of table names via the backend's implementation."""
141+
self._raise_if_not_implemented("_list_tables")
142+
return self._backend._list_tables(like=like, database=database)
143+
144+
def list_temp_tables(
145+
self, like: str | None = None, database: tuple[str, str] | str | None = None
146+
) -> list[str]:
147+
"""Return the list of temporary table names via the backend's implementation."""
148+
self._raise_if_not_implemented("_list_temp_tables")
149+
return self._backend._list_temp_tables(like=like, database=database)
150+
151+
def list_views(
152+
self, like: str | None = None, database: tuple[str, str] | str | None = None
153+
) -> list[str]:
154+
"""Return the list of view names via the backend's implementation."""
155+
self._raise_if_not_implemented("_list_views")
156+
return self._backend._list_views(like=like, database=database)
157+
158+
def list_temp_views(
159+
self, like: str | None = None, database: tuple[str, str] | str | None = None
160+
) -> list[str]:
161+
"""Return the list of temp view names via the backend's implementation."""
162+
self._raise_if_not_implemented("_list_temp_views")
163+
return self._backend._list_temp_views(like=like, database=database)
94164

95165

96166
class _FileIOHandler:
@@ -811,7 +881,12 @@ def __init__(self, *args, **kwargs):
811881
self._con_args: tuple[Any] = args
812882
self._con_kwargs: dict[str, Any] = kwargs
813883
self._can_reconnect: bool = True
814-
self._query_cache = RefCountedCache(weakref.proxy(self))
884+
# expression cache
885+
self._query_cache = RefCountedCache(
886+
populate=self._load_into_cache,
887+
lookup=lambda name: self.table(name).op(),
888+
finalize=self._clean_up_cached_table,
889+
)
815890

816891
@property
817892
@abc.abstractmethod
@@ -933,44 +1008,6 @@ def _filter_with_like(values: Iterable[str], like: str | None = None) -> list[st
9331008
pattern = re.compile(like)
9341009
return sorted(filter(pattern.findall, values))
9351010

936-
@abc.abstractmethod
937-
def list_tables(
938-
self, like: str | None = None, database: tuple[str, str] | str | None = None
939-
) -> list[str]:
940-
"""Return the list of table names in the current database.
941-
942-
For some backends, the tables may be files in a directory,
943-
or other equivalent entities in a SQL database.
944-
945-
::: {.callout-note}
946-
## Ibis does not use the word `schema` to refer to database hierarchy.
947-
948-
A collection of tables is referred to as a `database`.
949-
A collection of `database` is referred to as a `catalog`.
950-
951-
These terms are mapped onto the corresponding features in each
952-
backend (where available), regardless of whether the backend itself
953-
uses the same terminology.
954-
:::
955-
956-
Parameters
957-
----------
958-
like
959-
A pattern in Python's regex format.
960-
database
961-
The database from which to list tables.
962-
If not provided, the current database is used.
963-
For backends that support multi-level table hierarchies, you can
964-
pass in a dotted string path like `"catalog.database"` or a tuple of
965-
strings like `("catalog", "database")`.
966-
967-
Returns
968-
-------
969-
list[str]
970-
The list of the table names that match the pattern `like`.
971-
972-
"""
973-
9741011
@abc.abstractmethod
9751012
def table(
9761013
self, name: str, database: tuple[str, str] | str | None = None
@@ -1019,7 +1056,12 @@ def tables(self):
10191056
>>> people = con.tables.people # access via attribute
10201057
10211058
"""
1022-
return TablesAccessor(weakref.proxy(self))
1059+
return TablesAccessor(self)
1060+
1061+
@property
1062+
def ddl(self):
1063+
"""A ddl accessor."""
1064+
return DDLAccessor(self)
10231065

10241066
@property
10251067
@abc.abstractmethod

0 commit comments

Comments
 (0)