Skip to content

Commit d253b7a

Browse files
authored
1107 Fix prefetch when a ForeignKey has db_column_name set (#1111)
* fix prefetch when a `ForeignKey` has `db_column_name` set * Update run.py * add Signing table * add option to match on db_column_name * add a test * Revert "fix prefetch when a `ForeignKey` has `db_column_name` set" This reverts commit 2de72c7. * sort imports * Update run.py * Update test_apps.py * Update test_run.py * Create music_2026_02_22t00_41_01_493867.py * Update test_forwards_backwards.py * fix linter warning
1 parent fc3530c commit d253b7a

10 files changed

Lines changed: 192 additions & 6 deletions

File tree

piccolo/apps/playground/commands/run.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Serial,
2626
Text,
2727
Timestamp,
28+
Timestamptz,
2829
Varchar,
2930
)
3031
from piccolo.columns.readable import Readable
@@ -184,6 +185,13 @@ class GenreToBand(Table):
184185
reason = Text(null=True, default=None)
185186

186187

188+
class Signing(Table):
189+
id: Serial
190+
address = Text()
191+
with_ = ForeignKey(Band, db_column_name="with")
192+
starts = Timestamptz()
193+
194+
187195
TABLES = (
188196
Manager,
189197
Band,
@@ -196,6 +204,7 @@ class GenreToBand(Table):
196204
Album,
197205
Genre,
198206
GenreToBand,
207+
Signing,
199208
)
200209

201210

@@ -334,6 +343,19 @@ def populate():
334343
GenreToBand(band=c_sharps.id, genre=genres[1]["id"]),
335344
).run_sync()
336345

346+
Signing.insert(
347+
Signing(
348+
with_=pythonistas,
349+
address="Awesome Music Store, London",
350+
starts=datetime.datetime(2026, 12, 20, 10),
351+
),
352+
Signing(
353+
with_=pythonistas,
354+
address="Awesome Music Store, Liverpool",
355+
starts=datetime.datetime(2026, 11, 25, 12),
356+
),
357+
).run_sync()
358+
337359

338360
def run(
339361
engine: str = "sqlite",

piccolo/table.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,38 @@ def refresh_db(self) -> None:
149149
raise ValueError("The engine can't be found")
150150
self.db = engine
151151

152-
def get_column_by_name(self, name: str) -> Column:
152+
def get_column_by_name(
153+
self,
154+
name: str,
155+
match_db_column_name: bool = False,
156+
) -> Column:
153157
"""
154158
Returns a column which matches the given name. It will try and follow
155159
foreign keys too, for example if the name is 'foo.bar', where foo is
156160
a foreign key, and bar is a column on the referenced table.
161+
162+
:param match_db_column_name:
163+
If ``True``, we also check the column's ``db_column_name`` for a
164+
match.
165+
157166
"""
158167
components = name.split(".")
159168
column_name = components[0]
160-
column = [i for i in self.columns if i._meta.name == column_name]
161-
if len(column) != 1:
169+
column_object = next(
170+
(
171+
i
172+
for i in self.columns
173+
if (i._meta.name == column_name)
174+
or (
175+
i._meta.db_column_name == column_name
176+
if match_db_column_name
177+
else False
178+
)
179+
),
180+
None,
181+
)
182+
if column_object is None:
162183
raise ValueError(f"No matching column found with name == {name}")
163-
column_object = column[0]
164184

165185
if len(components) > 1:
166186
for reference_name in components[1:]:

piccolo/utils/objects.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ def make_nested_object(row: dict[str, Any], table_class: type[Table]) -> Table:
4141
for key, value in row.items():
4242
if isinstance(value, dict):
4343
# This is probably a related table.
44-
fk_column = table_class._meta.get_column_by_name(key)
44+
fk_column = table_class._meta.get_column_by_name(
45+
key,
46+
match_db_column_name=True,
47+
)
4548

4649
if isinstance(fk_column, ForeignKey):
4750
related_table_class = (

tests/apps/migrations/commands/test_forwards_backwards.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Poster,
1919
RecordingStudio,
2020
Shirt,
21+
Signing,
2122
Ticket,
2223
Venue,
2324
)
@@ -35,6 +36,7 @@
3536
Shirt,
3637
RecordingStudio,
3738
Instrument,
39+
Signing,
3840
]
3941

4042

@@ -215,6 +217,7 @@ def test_forwards_fake(self):
215217
"2021-11-13T14:01:46:114725",
216218
"2024-05-28T23:15:41:018844",
217219
"2024-06-19T18:11:05:793132",
220+
"2026-02-22T00:41:01:493867",
218221
],
219222
)
220223

tests/apps/shell/commands/test_run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_run(self, print_: MagicMock, start_ipython_shell: MagicMock):
2525
call("- Poster"),
2626
call("- RecordingStudio"),
2727
call("- Shirt"),
28+
call("- Signing"),
2829
call("- Ticket"),
2930
call("- Venue"),
3031
call("Importing mega tables:"),

tests/conf/test_apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Poster,
2222
RecordingStudio,
2323
Shirt,
24+
Signing,
2425
Ticket,
2526
Venue,
2627
)
@@ -126,6 +127,7 @@ def test_table_finder(self):
126127
"Poster",
127128
"RecordingStudio",
128129
"Shirt",
130+
"Signing",
129131
"Ticket",
130132
"Venue",
131133
],
@@ -153,6 +155,7 @@ def test_table_finder_coercion(self):
153155
"Poster",
154156
"RecordingStudio",
155157
"Shirt",
158+
"Signing",
156159
"Ticket",
157160
"Venue",
158161
],
@@ -196,6 +199,7 @@ def test_exclude_tags(self):
196199
"Manager",
197200
"RecordingStudio",
198201
"Shirt",
202+
"Signing",
199203
"Ticket",
200204
"Venue",
201205
],
@@ -245,6 +249,7 @@ def test_get_table_classes(self):
245249
Poster,
246250
RecordingStudio,
247251
Shirt,
252+
Signing,
248253
SmallTable,
249254
Ticket,
250255
Venue,
@@ -264,6 +269,7 @@ def test_get_table_classes(self):
264269
Poster,
265270
RecordingStudio,
266271
Shirt,
272+
Signing,
267273
Ticket,
268274
Venue,
269275
],

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async def drop_tables():
2020
"instrument",
2121
"shirt",
2222
"instrument",
23+
"signing",
2324
"mega_table",
2425
"small_table",
2526
]
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
2+
from piccolo.columns.base import OnDelete, OnUpdate
3+
from piccolo.columns.column_types import ForeignKey, Serial, Text, Timestamptz
4+
from piccolo.columns.defaults.timestamptz import TimestamptzNow
5+
from piccolo.columns.indexes import IndexMethod
6+
from piccolo.table import Table
7+
8+
9+
class Band(Table, tablename="band", schema=None):
10+
id = Serial(
11+
null=False,
12+
primary_key=True,
13+
unique=False,
14+
index=False,
15+
index_method=IndexMethod.btree,
16+
choices=None,
17+
db_column_name="id",
18+
secret=False,
19+
)
20+
21+
22+
ID = "2026-02-22T00:41:01:493867"
23+
VERSION = "1.32.0"
24+
DESCRIPTION = "Add Signing table"
25+
26+
27+
async def forwards():
28+
manager = MigrationManager(
29+
migration_id=ID, app_name="music", description=DESCRIPTION
30+
)
31+
32+
manager.add_table(
33+
class_name="Signing", tablename="signing", schema=None, columns=None
34+
)
35+
36+
manager.add_column(
37+
table_class_name="Signing",
38+
tablename="signing",
39+
column_name="address",
40+
db_column_name="address",
41+
column_class_name="Text",
42+
column_class=Text,
43+
params={
44+
"default": "",
45+
"null": False,
46+
"primary_key": False,
47+
"unique": False,
48+
"index": False,
49+
"index_method": IndexMethod.btree,
50+
"choices": None,
51+
"db_column_name": None,
52+
"secret": False,
53+
},
54+
schema=None,
55+
)
56+
57+
manager.add_column(
58+
table_class_name="Signing",
59+
tablename="signing",
60+
column_name="with_",
61+
db_column_name="with",
62+
column_class_name="ForeignKey",
63+
column_class=ForeignKey,
64+
params={
65+
"references": Band,
66+
"on_delete": OnDelete.cascade,
67+
"on_update": OnUpdate.cascade,
68+
"target_column": None,
69+
"null": True,
70+
"primary_key": False,
71+
"unique": False,
72+
"index": False,
73+
"index_method": IndexMethod.btree,
74+
"choices": None,
75+
"db_column_name": "with",
76+
"secret": False,
77+
},
78+
schema=None,
79+
)
80+
81+
manager.add_column(
82+
table_class_name="Signing",
83+
tablename="signing",
84+
column_name="starts",
85+
db_column_name="starts",
86+
column_class_name="Timestamptz",
87+
column_class=Timestamptz,
88+
params={
89+
"default": TimestamptzNow(),
90+
"null": False,
91+
"primary_key": False,
92+
"unique": False,
93+
"index": False,
94+
"index_method": IndexMethod.btree,
95+
"choices": None,
96+
"db_column_name": None,
97+
"secret": False,
98+
},
99+
schema=None,
100+
)
101+
102+
return manager

tests/example_apps/music/tables.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Numeric,
1010
Serial,
1111
Text,
12+
Timestamptz,
1213
Varchar,
1314
)
1415
from piccolo.columns.readable import Readable
@@ -125,3 +126,14 @@ class Instrument(Table):
125126
id: Serial
126127
name = Varchar()
127128
recording_studio = ForeignKey(RecordingStudio)
129+
130+
131+
class Signing(Table):
132+
"""
133+
Used for testing ``db_column_name``.
134+
"""
135+
136+
id: Serial
137+
address = Text()
138+
with_ = ForeignKey(Band, db_column_name="with")
139+
starts = Timestamptz()

tests/table/test_join.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Band,
77
Concert,
88
Manager,
9+
Signing,
910
Ticket,
1011
Venue,
1112
)
@@ -23,7 +24,7 @@ def test_create_join(self):
2324

2425

2526
class TestJoin(TestCase):
26-
tables = [Manager, Band, Venue, Concert, Ticket]
27+
tables = [Manager, Band, Venue, Concert, Ticket, Signing]
2728

2829
def setUp(self):
2930
for table in self.tables:
@@ -52,6 +53,9 @@ def setUp(self):
5253
ticket = Ticket(concert=concert, price=decimal.Decimal(50.0))
5354
ticket.save().run_sync()
5455

56+
signing = Signing(with_=band_1)
57+
signing.save().run_sync()
58+
5559
def tearDown(self):
5660
for table in reversed(self.tables):
5761
table.alter().drop_table().run_sync()
@@ -486,3 +490,15 @@ def test_objects_prefetch_multiple_intermediate(self):
486490

487491
self.assertIsInstance(ticket.concert.band_2.manager.id, int)
488492
self.assertIsInstance(ticket.concert.band_2.manager.name, str)
493+
494+
def test_objects_prefetch_db_column_name(self):
495+
"""
496+
Make sure that ``prefetch`` works with foreign keys with
497+
``db_column_name`` defined.
498+
499+
https://github.com/piccolo-orm/piccolo/issues/1107
500+
501+
"""
502+
signing = Signing.objects().prefetch(Signing.with_).first().run_sync()
503+
assert signing is not None
504+
self.assertIsInstance(signing.with_, Band)

0 commit comments

Comments
 (0)