Skip to content

Commit 46b5741

Browse files
committed
improving performance of Column copying
1 parent 284854b commit 46b5741

File tree

5 files changed

+107
-15
lines changed

5 files changed

+107
-15
lines changed

piccolo/columns/base.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ def resolved_references(self) -> t.Type[Table]:
9090
"LazyTableReference instance."
9191
)
9292

93+
def copy(self) -> ForeignKeyMeta:
94+
kwargs = self.__dict__.copy()
95+
kwargs.update(proxy_columns=self.proxy_columns.copy())
96+
return self.__class__(**kwargs)
97+
98+
def __copy__(self) -> ForeignKeyMeta:
99+
return self.copy()
100+
101+
def __deepcopy__(self, memo) -> ForeignKeyMeta:
102+
"""
103+
We override deepcopy, as it's too slow if it has to recreate
104+
everything.
105+
"""
106+
return self.copy()
107+
93108

94109
@dataclass
95110
class ColumnMeta:
@@ -164,6 +179,23 @@ def get_full_name(self, just_alias=False) -> str:
164179
else:
165180
return f'{alias} AS "{column_name}"'
166181

182+
def copy(self) -> ColumnMeta:
183+
kwargs = self.__dict__.copy()
184+
kwargs.update(
185+
params=self.params.copy(), call_chain=self.call_chain.copy(),
186+
)
187+
return self.__class__(**kwargs)
188+
189+
def __copy__(self) -> ColumnMeta:
190+
return self.copy()
191+
192+
def __deepcopy__(self, memo) -> ColumnMeta:
193+
"""
194+
We override deepcopy, as it's too slow if it has to recreate
195+
everything.
196+
"""
197+
return self.copy()
198+
167199

168200
class Selectable(metaclass=ABCMeta):
169201
@abstractmethod
@@ -455,6 +487,18 @@ def querystring(self) -> QueryString:
455487

456488
return QueryString(query)
457489

490+
def copy(self) -> Column:
491+
column: Column = copy.copy(self)
492+
column._meta = self._meta.copy()
493+
return column
494+
495+
def __deepcopy__(self, memo) -> Column:
496+
"""
497+
We override deepcopy, as it's too slow if it has to recreate
498+
everything.
499+
"""
500+
return self.copy()
501+
458502
def __str__(self):
459503
return self.querystring.__str__()
460504

piccolo/columns/column_types.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import uuid
77
from datetime import date, datetime, time, timedelta
88

9-
from piccolo.columns.base import Column, ForeignKeyMeta, OnDelete, OnUpdate
9+
from piccolo.columns.base import (
10+
Column,
11+
ForeignKeyMeta,
12+
OnDelete,
13+
OnUpdate,
14+
)
1015
from piccolo.columns.defaults.date import DateArg, DateCustom, DateNow
1116
from piccolo.columns.defaults.interval import IntervalArg, IntervalCustom
1217
from piccolo.columns.defaults.time import TimeArg, TimeCustom, TimeNow
@@ -1041,6 +1046,12 @@ def __init__(
10411046
Table, OnDelete.cascade, OnUpdate.cascade
10421047
)
10431048

1049+
def copy(self) -> ForeignKey:
1050+
column: ForeignKey = copy.copy(self)
1051+
column._meta = self._meta.copy()
1052+
column._foreign_key_meta = self._foreign_key_meta.copy()
1053+
return column
1054+
10441055
def set_proxy_columns(self):
10451056
"""
10461057
In order to allow a fluent interface, where tables can be traversed
@@ -1050,7 +1061,7 @@ def set_proxy_columns(self):
10501061
"""
10511062
_foreign_key_meta = object.__getattribute__(self, "_foreign_key_meta")
10521063
for column in _foreign_key_meta.resolved_references._meta.columns:
1053-
_column: Column = copy.deepcopy(column)
1064+
_column: Column = column.copy()
10541065
setattr(self, _column._meta.name, _column)
10551066
_foreign_key_meta.proxy_columns.append(_column)
10561067

@@ -1081,11 +1092,12 @@ def __getattribute__(self, name: str):
10811092
except AttributeError:
10821093
raise AttributeError
10831094

1084-
foreignkey_class = object.__getattribute__(self, "__class__")
1095+
foreignkey_class: t.Type[ForeignKey] = object.__getattribute__(
1096+
self, "__class__"
1097+
)
10851098

10861099
if isinstance(value, foreignkey_class): # i.e. a ForeignKey
1087-
new_column = copy.deepcopy(value)
1088-
new_column._meta.call_chain = copy.copy(self._meta.call_chain)
1100+
new_column = value.copy()
10891101
new_column._meta.call_chain.append(self)
10901102

10911103
# We have to set limits to the call chain because Table 1 can
@@ -1094,7 +1106,7 @@ def __getattribute__(self, name: str):
10941106
# When querying a call chain more than 10 levels deep, an error
10951107
# will be raised. Often there are more effective ways of
10961108
# structuring a query than joining so many tables anyway.
1097-
if len(new_column._meta.call_chain) > 10:
1109+
if len(new_column._meta.call_chain) >= 10:
10981110
raise Exception("Call chain too long!")
10991111

11001112
for proxy_column in self._foreign_key_meta.proxy_columns:
@@ -1106,10 +1118,10 @@ def __getattribute__(self, name: str):
11061118
for (
11071119
column
11081120
) in value._foreign_key_meta.resolved_references._meta.columns:
1109-
_column: Column = copy.deepcopy(column)
1110-
_column._meta.call_chain = copy.copy(
1111-
new_column._meta.call_chain
1112-
)
1121+
_column: Column = column.copy()
1122+
_column._meta.call_chain = [
1123+
i for i in new_column._meta.call_chain
1124+
]
11131125
_column._meta.call_chain.append(new_column)
11141126
if _column._meta.name == "id":
11151127
continue
@@ -1118,8 +1130,8 @@ def __getattribute__(self, name: str):
11181130

11191131
return new_column
11201132
elif issubclass(type(value), Column):
1121-
new_column = copy.deepcopy(value)
1122-
new_column._meta.call_chain = copy.copy(self._meta.call_chain)
1133+
new_column = value.copy()
1134+
new_column._meta.call_chain = self._meta.call_chain.copy()
11231135
new_column._meta.call_chain.append(self)
11241136
return new_column
11251137
else:

piccolo/table.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from __future__ import annotations
2-
import copy
32
from dataclasses import dataclass, field
43
import inspect
54
import itertools
@@ -435,7 +434,7 @@ def ref(cls, column_name: str) -> Column:
435434
reference_column_name
436435
)
437436

438-
_reference_column = copy.deepcopy(reference_column)
437+
_reference_column = reference_column.copy()
439438
_reference_column._meta.name = (
440439
f"{local_column_name}.{reference_column_name}"
441440
)

tests/columns/test_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,17 @@ def test_like_raises(self):
2525
for arg in ["%guido", "guido%", "%guido%"]:
2626
column.like("%foo")
2727
column.ilike("foo%")
28+
29+
30+
class TestCopy(TestCase):
31+
def test_copy(self):
32+
"""
33+
Try copying columns.
34+
"""
35+
column = MyTable.name
36+
new_column = column.copy()
37+
self.assertNotEqual(id(column), id(new_column))
38+
self.assertNotEqual(id(column._meta), id(new_column._meta))
39+
self.assertNotEqual(
40+
id(column._meta.call_chain), id(new_column._meta.call_chain)
41+
)

tests/columns/test_foreignkey.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from unittest import TestCase
2+
import time
23

34
from piccolo.table import Table
4-
from piccolo.columns import ForeignKey, Varchar, LazyTableReference
5+
from piccolo.columns import Column, ForeignKey, Varchar, LazyTableReference
56

67

78
class Manager(Table):
@@ -117,3 +118,25 @@ def test_attribute_access(self):
117118
self.assertTrue(isinstance(Band1.manager.name, Varchar))
118119
self.assertTrue(isinstance(Band2.manager.name, Varchar))
119120
self.assertTrue(isinstance(Band3.manager.name, Varchar))
121+
122+
def test_recursion_limit(self):
123+
"""
124+
When a table has a ForeignKey to itself, an Exception should be raised
125+
if the call chain is too large.
126+
"""
127+
# Should be fine:
128+
column: Column = Manager.manager.name
129+
self.assertTrue(len(column._meta.call_chain), 1)
130+
self.assertTrue(isinstance(column, Varchar))
131+
132+
with self.assertRaises(Exception):
133+
Manager.manager.manager.manager.manager.manager.manager.manager.manager.manager.manager.manager.name # noqa
134+
135+
def test_recursion_time(self):
136+
"""
137+
Make sure that a really large call chain doesn't take too long.
138+
"""
139+
start = time.time()
140+
Manager.manager.manager.manager.manager.manager.manager.name
141+
end = time.time()
142+
self.assertTrue(end - start < 1.0)

0 commit comments

Comments
 (0)