piccolo 0.114.0__py3-none-any.whl → 0.115.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piccolo/__init__.py +1 -1
- piccolo/apps/fixtures/commands/load.py +47 -6
- piccolo/apps/migrations/auto/migration_manager.py +1 -1
- piccolo/columns/m2m.py +11 -7
- piccolo/query/methods/count.py +7 -3
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/METADATA +1 -1
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/RECORD +18 -15
- tests/apps/fixtures/commands/test_dump_load.py +39 -1
- tests/apps/migrations/auto/integration/test_migrations.py +34 -2
- tests/columns/m2m/__init__.py +0 -0
- tests/columns/{test_m2m.py → m2m/base.py} +55 -426
- tests/columns/m2m/test_m2m.py +436 -0
- tests/columns/m2m/test_m2m_schema.py +48 -0
- tests/table/test_count.py +4 -0
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/LICENSE +0 -0
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/WHEEL +0 -0
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/entry_points.txt +0 -0
- {piccolo-0.114.0.dist-info → piccolo-0.115.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,436 @@
|
|
1
|
+
import asyncio
|
2
|
+
import datetime
|
3
|
+
import decimal
|
4
|
+
import uuid
|
5
|
+
from unittest import TestCase
|
6
|
+
|
7
|
+
from tests.base import engines_skip
|
8
|
+
|
9
|
+
try:
|
10
|
+
from asyncpg.pgproto.pgproto import UUID as asyncpgUUID
|
11
|
+
except ImportError:
|
12
|
+
# In case someone is running the tests for SQLite and doesn't have asyncpg
|
13
|
+
# installed.
|
14
|
+
from uuid import UUID as asyncpgUUID
|
15
|
+
|
16
|
+
from piccolo.columns.column_types import (
|
17
|
+
JSON,
|
18
|
+
JSONB,
|
19
|
+
UUID,
|
20
|
+
Array,
|
21
|
+
BigInt,
|
22
|
+
Boolean,
|
23
|
+
Bytea,
|
24
|
+
Date,
|
25
|
+
DoublePrecision,
|
26
|
+
ForeignKey,
|
27
|
+
Integer,
|
28
|
+
Interval,
|
29
|
+
LazyTableReference,
|
30
|
+
Numeric,
|
31
|
+
Real,
|
32
|
+
SmallInt,
|
33
|
+
Text,
|
34
|
+
Timestamp,
|
35
|
+
Timestamptz,
|
36
|
+
Varchar,
|
37
|
+
)
|
38
|
+
from piccolo.columns.m2m import M2M
|
39
|
+
from piccolo.engine.finder import engine_finder
|
40
|
+
from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
|
41
|
+
|
42
|
+
from .base import M2MBase
|
43
|
+
|
44
|
+
engine = engine_finder()
|
45
|
+
|
46
|
+
|
47
|
+
class Band(Table):
|
48
|
+
name = Varchar()
|
49
|
+
genres = M2M(LazyTableReference("GenreToBand", module_path=__name__))
|
50
|
+
|
51
|
+
|
52
|
+
class Genre(Table):
|
53
|
+
name = Varchar()
|
54
|
+
bands = M2M(LazyTableReference("GenreToBand", module_path=__name__))
|
55
|
+
|
56
|
+
|
57
|
+
class GenreToBand(Table):
|
58
|
+
band = ForeignKey(Band)
|
59
|
+
genre = ForeignKey(Genre)
|
60
|
+
reason = Text(help_text="For testing additional columns on join tables.")
|
61
|
+
|
62
|
+
|
63
|
+
class TestM2M(M2MBase, TestCase):
|
64
|
+
band = Band
|
65
|
+
genre = Genre
|
66
|
+
genre_to_band = GenreToBand
|
67
|
+
all_tables = [Band, Genre, GenreToBand]
|
68
|
+
|
69
|
+
|
70
|
+
###############################################################################
|
71
|
+
|
72
|
+
# A schema using custom primary keys
|
73
|
+
|
74
|
+
|
75
|
+
class Customer(Table):
|
76
|
+
uuid = UUID(primary_key=True)
|
77
|
+
name = Varchar()
|
78
|
+
concerts = M2M(
|
79
|
+
LazyTableReference("CustomerToConcert", module_path=__name__)
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
class Concert(Table):
|
84
|
+
uuid = UUID(primary_key=True)
|
85
|
+
name = Varchar()
|
86
|
+
customers = M2M(
|
87
|
+
LazyTableReference("CustomerToConcert", module_path=__name__)
|
88
|
+
)
|
89
|
+
|
90
|
+
|
91
|
+
class CustomerToConcert(Table):
|
92
|
+
customer = ForeignKey(Customer)
|
93
|
+
concert = ForeignKey(Concert)
|
94
|
+
|
95
|
+
|
96
|
+
CUSTOM_PK_SCHEMA = [Customer, Concert, CustomerToConcert]
|
97
|
+
|
98
|
+
|
99
|
+
class TestM2MCustomPrimaryKey(TestCase):
|
100
|
+
"""
|
101
|
+
Make sure the M2M functionality works correctly when the tables have custom
|
102
|
+
primary key columns.
|
103
|
+
"""
|
104
|
+
|
105
|
+
def setUp(self):
|
106
|
+
create_db_tables_sync(*CUSTOM_PK_SCHEMA, if_not_exists=True)
|
107
|
+
|
108
|
+
bob = Customer.objects().create(name="Bob").run_sync()
|
109
|
+
sally = Customer.objects().create(name="Sally").run_sync()
|
110
|
+
fred = Customer.objects().create(name="Fred").run_sync()
|
111
|
+
|
112
|
+
rockfest = Concert.objects().create(name="Rockfest").run_sync()
|
113
|
+
folkfest = Concert.objects().create(name="Folkfest").run_sync()
|
114
|
+
classicfest = Concert.objects().create(name="Classicfest").run_sync()
|
115
|
+
|
116
|
+
CustomerToConcert.insert(
|
117
|
+
CustomerToConcert(customer=bob, concert=rockfest),
|
118
|
+
CustomerToConcert(customer=bob, concert=classicfest),
|
119
|
+
CustomerToConcert(customer=sally, concert=rockfest),
|
120
|
+
CustomerToConcert(customer=sally, concert=folkfest),
|
121
|
+
CustomerToConcert(customer=fred, concert=classicfest),
|
122
|
+
).run_sync()
|
123
|
+
|
124
|
+
def tearDown(self):
|
125
|
+
drop_db_tables_sync(*CUSTOM_PK_SCHEMA)
|
126
|
+
|
127
|
+
@engines_skip("cockroach")
|
128
|
+
def test_select(self):
|
129
|
+
"""
|
130
|
+
🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
|
131
|
+
""" # noqa: E501
|
132
|
+
response = Customer.select(
|
133
|
+
Customer.name, Customer.concerts(Concert.name, as_list=True)
|
134
|
+
).run_sync()
|
135
|
+
|
136
|
+
self.assertListEqual(
|
137
|
+
response,
|
138
|
+
[
|
139
|
+
{"name": "Bob", "concerts": ["Rockfest", "Classicfest"]},
|
140
|
+
{"name": "Sally", "concerts": ["Rockfest", "Folkfest"]},
|
141
|
+
{"name": "Fred", "concerts": ["Classicfest"]},
|
142
|
+
],
|
143
|
+
)
|
144
|
+
|
145
|
+
# Now try it in reverse.
|
146
|
+
response = Concert.select(
|
147
|
+
Concert.name, Concert.customers(Customer.name, as_list=True)
|
148
|
+
).run_sync()
|
149
|
+
|
150
|
+
self.assertListEqual(
|
151
|
+
response,
|
152
|
+
[
|
153
|
+
{"name": "Rockfest", "customers": ["Bob", "Sally"]},
|
154
|
+
{"name": "Folkfest", "customers": ["Sally"]},
|
155
|
+
{"name": "Classicfest", "customers": ["Bob", "Fred"]},
|
156
|
+
],
|
157
|
+
)
|
158
|
+
|
159
|
+
def test_add_m2m(self):
|
160
|
+
"""
|
161
|
+
Make sure we can add items to the joining table.
|
162
|
+
"""
|
163
|
+
customer: Customer = (
|
164
|
+
Customer.objects().get(Customer.name == "Bob").run_sync()
|
165
|
+
)
|
166
|
+
customer.add_m2m(
|
167
|
+
Concert(name="Jazzfest"), m2m=Customer.concerts
|
168
|
+
).run_sync()
|
169
|
+
|
170
|
+
self.assertTrue(
|
171
|
+
Concert.exists().where(Concert.name == "Jazzfest").run_sync()
|
172
|
+
)
|
173
|
+
|
174
|
+
self.assertEqual(
|
175
|
+
CustomerToConcert.count()
|
176
|
+
.where(
|
177
|
+
CustomerToConcert.customer.name == "Bob",
|
178
|
+
CustomerToConcert.concert.name == "Jazzfest",
|
179
|
+
)
|
180
|
+
.run_sync(),
|
181
|
+
1,
|
182
|
+
)
|
183
|
+
|
184
|
+
def test_add_m2m_within_transaction(self):
|
185
|
+
"""
|
186
|
+
Make sure we can add items to the joining table, when within an
|
187
|
+
existing transaction.
|
188
|
+
|
189
|
+
https://github.com/piccolo-orm/piccolo/issues/674
|
190
|
+
|
191
|
+
"""
|
192
|
+
engine = Customer._meta.db
|
193
|
+
|
194
|
+
async def add_m2m_in_transaction():
|
195
|
+
async with engine.transaction():
|
196
|
+
customer: Customer = await Customer.objects().get(
|
197
|
+
Customer.name == "Bob"
|
198
|
+
)
|
199
|
+
await customer.add_m2m(
|
200
|
+
Concert(name="Jazzfest"), m2m=Customer.concerts
|
201
|
+
)
|
202
|
+
|
203
|
+
asyncio.run(add_m2m_in_transaction())
|
204
|
+
|
205
|
+
self.assertTrue(
|
206
|
+
Concert.exists().where(Concert.name == "Jazzfest").run_sync()
|
207
|
+
)
|
208
|
+
|
209
|
+
self.assertEqual(
|
210
|
+
CustomerToConcert.count()
|
211
|
+
.where(
|
212
|
+
CustomerToConcert.customer.name == "Bob",
|
213
|
+
CustomerToConcert.concert.name == "Jazzfest",
|
214
|
+
)
|
215
|
+
.run_sync(),
|
216
|
+
1,
|
217
|
+
)
|
218
|
+
|
219
|
+
def test_get_m2m(self):
|
220
|
+
"""
|
221
|
+
Make sure we can get related items via the joining table.
|
222
|
+
"""
|
223
|
+
customer: Customer = (
|
224
|
+
Customer.objects().get(Customer.name == "Bob").run_sync()
|
225
|
+
)
|
226
|
+
|
227
|
+
concerts = customer.get_m2m(Customer.concerts).run_sync()
|
228
|
+
|
229
|
+
self.assertTrue(all(isinstance(i, Table) for i in concerts))
|
230
|
+
|
231
|
+
self.assertCountEqual(
|
232
|
+
[i.name for i in concerts], ["Rockfest", "Classicfest"]
|
233
|
+
)
|
234
|
+
|
235
|
+
|
236
|
+
###############################################################################
|
237
|
+
|
238
|
+
# Test a very complex schema
|
239
|
+
|
240
|
+
|
241
|
+
class SmallTable(Table):
|
242
|
+
varchar_col = Varchar()
|
243
|
+
mega_rows = M2M(LazyTableReference("SmallToMega", module_path=__name__))
|
244
|
+
|
245
|
+
|
246
|
+
if engine.engine_type != "cockroach": # type: ignore
|
247
|
+
|
248
|
+
class MegaTable(Table): # type: ignore
|
249
|
+
"""
|
250
|
+
A table containing all of the column types and different column kwargs
|
251
|
+
"""
|
252
|
+
|
253
|
+
array_col = Array(Varchar())
|
254
|
+
bigint_col = BigInt()
|
255
|
+
boolean_col = Boolean()
|
256
|
+
bytea_col = Bytea()
|
257
|
+
date_col = Date()
|
258
|
+
double_precision_col = DoublePrecision()
|
259
|
+
integer_col = Integer()
|
260
|
+
interval_col = Interval()
|
261
|
+
json_col = JSON()
|
262
|
+
jsonb_col = JSONB()
|
263
|
+
numeric_col = Numeric(digits=(5, 2))
|
264
|
+
real_col = Real()
|
265
|
+
smallint_col = SmallInt()
|
266
|
+
text_col = Text()
|
267
|
+
timestamp_col = Timestamp()
|
268
|
+
timestamptz_col = Timestamptz()
|
269
|
+
uuid_col = UUID()
|
270
|
+
varchar_col = Varchar()
|
271
|
+
|
272
|
+
else:
|
273
|
+
|
274
|
+
class MegaTable(Table): # type: ignore
|
275
|
+
"""
|
276
|
+
Special version for Cockroach.
|
277
|
+
A table containing all of the column types and different column kwargs
|
278
|
+
"""
|
279
|
+
|
280
|
+
array_col = Array(Varchar())
|
281
|
+
bigint_col = BigInt()
|
282
|
+
boolean_col = Boolean()
|
283
|
+
bytea_col = Bytea()
|
284
|
+
date_col = Date()
|
285
|
+
double_precision_col = DoublePrecision()
|
286
|
+
integer_col = BigInt()
|
287
|
+
interval_col = Interval()
|
288
|
+
json_col = JSONB()
|
289
|
+
jsonb_col = JSONB()
|
290
|
+
numeric_col = Numeric(digits=(5, 2))
|
291
|
+
real_col = Real()
|
292
|
+
smallint_col = SmallInt()
|
293
|
+
text_col = Text()
|
294
|
+
timestamp_col = Timestamp()
|
295
|
+
timestamptz_col = Timestamptz()
|
296
|
+
uuid_col = UUID()
|
297
|
+
varchar_col = Varchar()
|
298
|
+
|
299
|
+
|
300
|
+
class SmallToMega(Table):
|
301
|
+
small = ForeignKey(MegaTable)
|
302
|
+
mega = ForeignKey(SmallTable)
|
303
|
+
|
304
|
+
|
305
|
+
COMPLEX_SCHEMA = [MegaTable, SmallTable, SmallToMega]
|
306
|
+
|
307
|
+
|
308
|
+
class TestM2MComplexSchema(TestCase):
|
309
|
+
"""
|
310
|
+
By using a very complex schema containing every column type, we can catch
|
311
|
+
more edge cases.
|
312
|
+
"""
|
313
|
+
|
314
|
+
def setUp(self):
|
315
|
+
create_db_tables_sync(*COMPLEX_SCHEMA, if_not_exists=True)
|
316
|
+
|
317
|
+
small_table = SmallTable(varchar_col="Test")
|
318
|
+
small_table.save().run_sync()
|
319
|
+
|
320
|
+
mega_table = MegaTable(
|
321
|
+
array_col=["bob", "sally"],
|
322
|
+
bigint_col=1,
|
323
|
+
boolean_col=True,
|
324
|
+
bytea_col="hello".encode("utf8"),
|
325
|
+
date_col=datetime.date(year=2021, month=1, day=1),
|
326
|
+
double_precision_col=1.344,
|
327
|
+
integer_col=1,
|
328
|
+
interval_col=datetime.timedelta(seconds=10),
|
329
|
+
json_col={"a": 1},
|
330
|
+
jsonb_col={"a": 1},
|
331
|
+
numeric_col=decimal.Decimal("1.1"),
|
332
|
+
real_col=1.1,
|
333
|
+
smallint_col=1,
|
334
|
+
text_col="hello",
|
335
|
+
timestamp_col=datetime.datetime(year=2021, month=1, day=1),
|
336
|
+
timestamptz_col=datetime.datetime(
|
337
|
+
year=2021, month=1, day=1, tzinfo=datetime.timezone.utc
|
338
|
+
),
|
339
|
+
uuid_col=uuid.UUID("12783854-c012-4c15-8183-8eecb46f2c4e"),
|
340
|
+
varchar_col="hello",
|
341
|
+
)
|
342
|
+
mega_table.save().run_sync()
|
343
|
+
|
344
|
+
SmallToMega(small=small_table, mega=mega_table).save().run_sync()
|
345
|
+
|
346
|
+
self.mega_table = mega_table
|
347
|
+
|
348
|
+
def tearDown(self):
|
349
|
+
drop_db_tables_sync(*COMPLEX_SCHEMA)
|
350
|
+
|
351
|
+
@engines_skip("cockroach")
|
352
|
+
def test_select_all(self):
|
353
|
+
"""
|
354
|
+
Fetch all of the columns from the related table to make sure they're
|
355
|
+
returned correctly.
|
356
|
+
"""
|
357
|
+
"""
|
358
|
+
🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
|
359
|
+
""" # noqa: E501
|
360
|
+
response = SmallTable.select(
|
361
|
+
SmallTable.varchar_col, SmallTable.mega_rows(load_json=True)
|
362
|
+
).run_sync()
|
363
|
+
|
364
|
+
self.assertEqual(len(response), 1)
|
365
|
+
mega_rows = response[0]["mega_rows"]
|
366
|
+
|
367
|
+
self.assertEqual(len(mega_rows), 1)
|
368
|
+
mega_row = mega_rows[0]
|
369
|
+
|
370
|
+
for key, value in mega_row.items():
|
371
|
+
# Make sure that every value in the response matches what we saved.
|
372
|
+
self.assertAlmostEqual(
|
373
|
+
getattr(self.mega_table, key),
|
374
|
+
value,
|
375
|
+
msg=f"{key} doesn't match",
|
376
|
+
)
|
377
|
+
|
378
|
+
@engines_skip("cockroach")
|
379
|
+
def test_select_single(self):
|
380
|
+
"""
|
381
|
+
Make sure each column can be selected one at a time.
|
382
|
+
"""
|
383
|
+
"""
|
384
|
+
🐛 Cockroach bug: https://github.com/cockroachdb/cockroach/issues/71908 "could not decorrelate subquery" error under asyncpg
|
385
|
+
""" # noqa: E501
|
386
|
+
for column in MegaTable._meta.columns:
|
387
|
+
response = SmallTable.select(
|
388
|
+
SmallTable.varchar_col,
|
389
|
+
SmallTable.mega_rows(column, load_json=True),
|
390
|
+
).run_sync()
|
391
|
+
|
392
|
+
data = response[0]["mega_rows"][0]
|
393
|
+
column_name = column._meta.name
|
394
|
+
|
395
|
+
original_value = getattr(self.mega_table, column_name)
|
396
|
+
returned_value = data[column_name]
|
397
|
+
|
398
|
+
if type(column) == UUID:
|
399
|
+
self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
|
400
|
+
else:
|
401
|
+
self.assertEqual(
|
402
|
+
type(original_value),
|
403
|
+
type(returned_value),
|
404
|
+
msg=f"{column_name} type isn't correct",
|
405
|
+
)
|
406
|
+
|
407
|
+
self.assertAlmostEqual(
|
408
|
+
original_value,
|
409
|
+
returned_value,
|
410
|
+
msg=f"{column_name} doesn't match",
|
411
|
+
)
|
412
|
+
|
413
|
+
# Test it as a list too
|
414
|
+
response = SmallTable.select(
|
415
|
+
SmallTable.varchar_col,
|
416
|
+
SmallTable.mega_rows(column, as_list=True, load_json=True),
|
417
|
+
).run_sync()
|
418
|
+
|
419
|
+
original_value = getattr(self.mega_table, column_name)
|
420
|
+
returned_value = response[0]["mega_rows"][0]
|
421
|
+
|
422
|
+
if type(column) == UUID:
|
423
|
+
self.assertIn(type(returned_value), (uuid.UUID, asyncpgUUID))
|
424
|
+
self.assertEqual(str(original_value), str(returned_value))
|
425
|
+
else:
|
426
|
+
self.assertEqual(
|
427
|
+
type(original_value),
|
428
|
+
type(returned_value),
|
429
|
+
msg=f"{column_name} type isn't correct",
|
430
|
+
)
|
431
|
+
|
432
|
+
self.assertAlmostEqual(
|
433
|
+
original_value,
|
434
|
+
returned_value,
|
435
|
+
msg=f"{column_name} doesn't match",
|
436
|
+
)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from unittest import TestCase
|
2
|
+
|
3
|
+
from piccolo.columns.column_types import (
|
4
|
+
ForeignKey,
|
5
|
+
LazyTableReference,
|
6
|
+
Text,
|
7
|
+
Varchar,
|
8
|
+
)
|
9
|
+
from piccolo.columns.m2m import M2M
|
10
|
+
from piccolo.schema import SchemaManager
|
11
|
+
from piccolo.table import Table
|
12
|
+
from tests.base import engines_skip
|
13
|
+
|
14
|
+
from .base import M2MBase
|
15
|
+
|
16
|
+
|
17
|
+
class Band(Table, schema="schema_1"):
|
18
|
+
name = Varchar()
|
19
|
+
genres = M2M(LazyTableReference("GenreToBand", module_path=__name__))
|
20
|
+
|
21
|
+
|
22
|
+
class Genre(Table, schema="schema_1"):
|
23
|
+
name = Varchar()
|
24
|
+
bands = M2M(LazyTableReference("GenreToBand", module_path=__name__))
|
25
|
+
|
26
|
+
|
27
|
+
class GenreToBand(Table, schema="schema_1"):
|
28
|
+
band = ForeignKey(Band)
|
29
|
+
genre = ForeignKey(Genre)
|
30
|
+
reason = Text(help_text="For testing additional columns on join tables.")
|
31
|
+
|
32
|
+
|
33
|
+
@engines_skip("sqlite")
|
34
|
+
class TestM2MWithSchema(M2MBase, TestCase):
|
35
|
+
"""
|
36
|
+
Make sure that when the tables exist in a non-public schema, that M2M still
|
37
|
+
works.
|
38
|
+
"""
|
39
|
+
|
40
|
+
band = Band
|
41
|
+
genre = Genre
|
42
|
+
genre_to_band = GenreToBand
|
43
|
+
all_tables = [Band, Genre, GenreToBand]
|
44
|
+
|
45
|
+
def tearDown(self):
|
46
|
+
SchemaManager().drop_schema(
|
47
|
+
schema_name="schema_1", cascade=True
|
48
|
+
).run_sync()
|
tests/table/test_count.py
CHANGED
@@ -50,6 +50,10 @@ class TestCount(TestCase):
|
|
50
50
|
|
51
51
|
self.assertEqual(response, 3)
|
52
52
|
|
53
|
+
# Test the method also works
|
54
|
+
response = Band.count().distinct([Band.popularity]).run_sync()
|
55
|
+
self.assertEqual(response, 3)
|
56
|
+
|
53
57
|
def test_count_distinct_multiple(self):
|
54
58
|
Band.insert(
|
55
59
|
Band(name="Pythonistas", popularity=10),
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|