Skip to content

Commit f9c7acd

Browse files
authored
allow all_columns at the root, and exclude (piccolo-orm#212)
* allow all_columns at the root, and exclude * add an extra test for make_nested
1 parent 240ef5d commit f9c7acd

File tree

8 files changed

+299
-17
lines changed

8 files changed

+299
-17
lines changed

docs/src/piccolo/query_types/select.rst

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Or use an alias to make it shorter:
3131
3232
.. hint:: All of these examples also work with async by using .run() inside coroutines - see :ref:`SyncAndAsync`.
3333

34+
-------------------------------------------------------------------------------
35+
3436
as_alias
3537
--------
3638

@@ -43,6 +45,8 @@ By using ``as_alias``, the name of the row can be overriden in the response.
4345
4446
This is equivalent to ``SELECT name AS title FROM band`` in SQL.
4547

48+
-------------------------------------------------------------------------------
49+
4650
Joins
4751
-----
4852

@@ -51,7 +55,10 @@ One of the most powerful things about ``select`` is it's support for joins.
5155
.. code-block:: python
5256
5357
>>> Band.select(Band.name, Band.manager.name).run_sync()
54-
[{'name': 'Pythonistas', 'manager.name': 'Guido'}, {'name': 'Rustaceans', 'manager.name': 'Graydon'}]
58+
[
59+
{'name': 'Pythonistas', 'manager.name': 'Guido'},
60+
{'name': 'Rustaceans', 'manager.name': 'Graydon'}
61+
]
5562
5663
5764
The joins can go several layers deep.
@@ -61,36 +68,90 @@ The joins can go several layers deep.
6168
>>> Concert.select(Concert.id, Concert.band_1.manager.name).run_sync()
6269
[{'id': 1, 'band_1.manager.name': 'Guido'}]
6370
71+
all_columns
72+
~~~~~~~~~~~
73+
6474
If you want all of the columns from a related table you can use
6575
``all_columns``, which is a useful shortcut which saves you from typing them
6676
all out:
6777

6878
.. code-block:: python
6979
70-
>>> Band.select(Band.name, *Band.manager.all_columns()).run_sync()
80+
>>> Band.select(Band.name, Band.manager.all_columns()).run_sync()
7181
[
7282
{'name': 'Pythonistas', 'manager.id': 1, 'manager.name': 'Guido'},
7383
{'name': 'Rustaceans', 'manager.id': 2, 'manager.name': 'Graydon'}
7484
]
7585
76-
# In Piccolo > 0.41.0 you no longer need to explicitly unpack ``all_columns``.
77-
# This is equivalent:
78-
>>> Band.select(Band.name, Band.manager.all_columns()).run_sync()
86+
87+
In Piccolo < 0.41.0 you had to explicitly unpack ``all_columns``. This is
88+
equivalent to the code above:
89+
90+
.. code-block:: python
91+
92+
>>> Band.select(Band.name, *Band.manager.all_columns()).run_sync()
93+
94+
95+
You can exclude some columns if you like:
96+
97+
.. code-block:: python
98+
99+
>>> Band.select(
100+
>>> Band.name,
101+
>>> Band.manager.all_columns(exclude=[Band.manager.id)
102+
>>> ).run_sync()
103+
[
104+
{'name': 'Pythonistas', 'manager.name': 'Guido'},
105+
{'name': 'Rustaceans', 'manager.name': 'Graydon'}
106+
]
107+
108+
109+
Strings are supported too if you prefer:
110+
111+
.. code-block:: python
112+
113+
>>> Band.select(
114+
>>> Band.name,
115+
>>> Band.manager.all_columns(exclude=['id'])
116+
>>> ).run_sync()
117+
[
118+
{'name': 'Pythonistas', 'manager.name': 'Guido'},
119+
{'name': 'Rustaceans', 'manager.name': 'Graydon'}
120+
]
121+
122+
You can also use ``all_columns`` on the root table, which saves you time if
123+
you have lots of columns. It works identically to related tables:
124+
125+
.. code-block:: python
126+
127+
>>> Band.select(
128+
>>> Band.all_columns(exclude=[Band.id]),
129+
>>> Band.manager.all_columns(exclude=[Band.manager.id])
130+
>>> ).run_sync()
131+
[
132+
{'name': 'Pythonistas', 'popularity': 1000, 'manager.name': 'Guido'},
133+
{'name': 'Rustaceans', 'popularity': 500, 'manager.name': 'Graydon'}
134+
]
135+
136+
Nested
137+
~~~~~~
79138
80139
You can also get the response as nested dictionaries, which can be very useful:
81140
82141
.. code-block:: python
83142
84-
>>> Band.select(Band.name, *Band.manager.all_columns()).output(nested=True).run_sync()
143+
>>> Band.select(Band.name, Band.manager.all_columns()).output(nested=True).run_sync()
85144
[
86145
{'name': 'Pythonistas', 'manager': {'id': 1, 'name': 'Guido'}},
87146
{'name': 'Rustaceans', 'manager': {'id': 2, 'manager.name': 'Graydon'}}
88147
]
89148
149+
-------------------------------------------------------------------------------
150+
90151
String syntax
91152
-------------
92153
93-
Alternatively, you can specify the column names using a string. The
154+
You can specify the column names using a string if you prefer. The
94155
disadvantage is you won't have tab completion, but sometimes it's more
95156
convenient.
96157
@@ -101,6 +162,7 @@ convenient.
101162
# For joins:
102163
Band.select('manager.name').run_sync()
103164
165+
-------------------------------------------------------------------------------
104166
105167
Aggregate functions
106168
-------------------
@@ -189,6 +251,7 @@ And can use aliases for aggregate functions like this:
189251
>>> response["popularity_avg"]
190252
750.0
191253
254+
-------------------------------------------------------------------------------
192255
193256
Query clauses
194257
-------------

piccolo/columns/column_types.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,20 +1220,44 @@ def copy(self) -> ForeignKey:
12201220
column._foreign_key_meta = self._foreign_key_meta.copy()
12211221
return column
12221222

1223-
def all_columns(self):
1223+
def all_columns(
1224+
self, exclude: t.List[t.Union[Column, str]] = []
1225+
) -> t.List[Column]:
12241226
"""
12251227
Allow a user to access all of the columns on the related table.
12261228
12271229
For example:
12281230
1229-
Band.select(Band.name, *Band.manager.all_columns()).run_sync()
1231+
.. code-block:: python
1232+
1233+
Band.select(Band.name, Band.manager.all_columns()).run_sync()
1234+
1235+
To exclude certain columns:
1236+
1237+
.. code-block:: python
1238+
1239+
Band.select(
1240+
Band.name,
1241+
Band.manager.all_columns(
1242+
exclude=[Band.manager.id]
1243+
)
1244+
).run_sync()
1245+
1246+
:param exclude:
1247+
Columns to exclude - can be the name of a column, or a column
1248+
instance. For example ``['id']`` or ``[Band.manager.id]``.
12301249
12311250
"""
12321251
_fk_meta = object.__getattribute__(self, "_foreign_key_meta")
12331252

1253+
excluded_column_names = [
1254+
i._meta.name if isinstance(i, Column) else i for i in exclude
1255+
]
1256+
12341257
return [
12351258
getattr(self, column._meta.name)
12361259
for column in _fk_meta.resolved_references._meta.columns
1260+
if column._meta.name not in excluded_column_names
12371261
]
12381262

12391263
def set_proxy_columns(self):

piccolo/table.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,39 @@ def __repr__(self) -> str:
517517
###########################################################################
518518
# Classmethods
519519

520+
@classmethod
521+
def all_columns(
522+
cls, exclude: t.List[t.Union[str, Column]] = []
523+
) -> t.List[Column]:
524+
"""
525+
Just as we can use ``all_columns`` to retrieve all of the columns from
526+
a related table, we can also use it at the root of our query to get
527+
all of the columns for the root table. For example:
528+
529+
.. code-block:: python
530+
531+
await Band.select(
532+
Band.all_columns(),
533+
Band.manager.all_columns()
534+
).run()
535+
536+
This is mostly useful when the table has a lot of columns, and typing
537+
them out by hand would be tedious.
538+
539+
:param exclude:
540+
You can request all columns, except these.
541+
542+
"""
543+
excluded_column_names = [
544+
i._meta.name if isinstance(i, Column) else i for i in exclude
545+
]
546+
547+
return [
548+
i
549+
for i in cls._meta.columns
550+
if i._meta.name not in excluded_column_names
551+
]
552+
520553
@classmethod
521554
def ref(cls, column_name: str) -> Column:
522555
"""

piccolo/utils/dictionary.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,22 @@ def make_nested(dictionary: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
2727
if len(path) == 1:
2828
output[path[0]] = value
2929
else:
30-
dictionary = output.setdefault(path[0], {})
30+
# Force the root element to be an empty dictionary, if it's some
31+
# other value (most likely an integer). This is because there are
32+
# situations where a query can have `band` and `band.id`.
33+
# For example:
34+
# await Band.select(
35+
# Band.all_columns(),
36+
# Band.manager.all_columns()
37+
# ).run()
38+
# In this situation nesting takes precendence.
39+
root = output.get(path[0], None)
40+
if isinstance(root, dict):
41+
dictionary = root
42+
else:
43+
dictionary = {}
44+
output[path[0]] = dictionary
45+
3146
for path_element in path[1:-1]:
3247
dictionary = dictionary.setdefault(path_element, {})
3348
dictionary[path[-1]] = value

tests/columns/test_foreignkey.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,16 @@ def test_all_columns_deep(self):
204204
all_columns[1]._meta.call_chain,
205205
Concert.band_1.manager.name._meta.call_chain,
206206
)
207+
208+
def test_all_columns_exclude(self):
209+
"""
210+
Make sure you can exclude some columns.
211+
"""
212+
self.assertEqual(
213+
Band.manager.all_columns(exclude=["id"]), [Band.manager.name]
214+
)
215+
216+
self.assertEqual(
217+
Band.manager.all_columns(exclude=[Band.manager.id]),
218+
[Band.manager.name],
219+
)

tests/table/test_all_columns.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from unittest import TestCase
2+
3+
from tests.example_app.tables import Band
4+
5+
6+
class TestAllColumns(TestCase):
7+
def test_all_columns(self):
8+
self.assertEqual(
9+
Band.all_columns(),
10+
[Band.id, Band.name, Band.manager, Band.popularity],
11+
)
12+
self.assertEqual(Band.all_columns(), Band._meta.columns)
13+
14+
def test_all_columns_excluding(self):
15+
self.assertEqual(
16+
Band.all_columns(exclude=[Band.id]),
17+
[Band.name, Band.manager, Band.popularity],
18+
)
19+
20+
self.assertEqual(
21+
Band.all_columns(exclude=["id"]),
22+
[Band.name, Band.manager, Band.popularity],
23+
)

0 commit comments

Comments
 (0)