Skip to content

Commit 4785789

Browse files
authored
create multiple tables at once (piccolo-orm#235)
* added 'create_tables' function for creating multiple tables at once * added an unittest for 'create_tables' function * added a unit test for 'create_tables' function * fix for 'CASCADE' problem in 'create_tables' test * moved sort_table_classes, _get_graph and create_tables to piccolo/table.py * add 'create_tables' to docs
1 parent 3af0ac4 commit 4785789

File tree

7 files changed

+110
-82
lines changed

7 files changed

+110
-82
lines changed

docs/src/piccolo/query_types/create_table.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ To prevent an error from being raised if the table already exists:
1919
2020
>>> Band.create_table(if_not_exists=True).run_sync()
2121
[]
22+
23+
Also, you can create multiple tables at once.
24+
25+
This function will automatically sort tables based on their foreign keys so they're created in the right order:
26+
27+
.. code-block:: python
28+
29+
>>> from piccolo.table import create_tables
30+
>>> create_tables(Band, Manager, if_not_exists=True)
31+

piccolo/apps/fixture/commands/dump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
FixtureConfig,
77
create_pydantic_fixture_model,
88
)
9-
from piccolo.apps.migrations.auto.migration_manager import sort_table_classes
109
from piccolo.conf.apps import Finder
10+
from piccolo.table import sort_table_classes
1111

1212

1313
async def get_dump(

piccolo/apps/migrations/auto/migration_manager.py

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
from piccolo.apps.migrations.auto.serialisation import deserialise_params
1515
from piccolo.columns import Column, column_types
1616
from piccolo.engine import engine_finder
17-
from piccolo.table import Table, create_table_class
18-
from piccolo.utils.graphlib import TopologicalSorter
17+
from piccolo.table import Table, create_table_class, sort_table_classes
1918

2019

2120
@dataclass
@@ -118,76 +117,6 @@ def table_class_names(self) -> t.List[str]:
118117
return list(set([i.table_class_name for i in self.alter_columns]))
119118

120119

121-
def _get_graph(
122-
table_classes: t.List[t.Type[Table]],
123-
iterations: int = 0,
124-
max_iterations: int = 5,
125-
) -> t.Dict[str, t.Set[str]]:
126-
"""
127-
Analyses the tables based on their foreign keys, and returns a data
128-
structure like:
129-
130-
.. code-block:: python
131-
132-
{'band': {'manager'}, 'concert': {'band', 'venue'}, 'manager': set()}
133-
134-
The keys are tablenames, and the values are tablenames directly connected
135-
to it via a foreign key.
136-
137-
"""
138-
output: t.Dict[str, t.Set[str]] = {}
139-
140-
if iterations >= max_iterations:
141-
return output
142-
143-
for table_class in table_classes:
144-
dependents: t.Set[str] = set()
145-
for fk in table_class._meta.foreign_key_columns:
146-
dependents.add(
147-
fk._foreign_key_meta.resolved_references._meta.tablename
148-
)
149-
150-
# We also recursively check the related tables to get a fuller
151-
# picture of the schema and relationships.
152-
referenced_table = fk._foreign_key_meta.resolved_references
153-
output.update(
154-
_get_graph(
155-
[referenced_table],
156-
iterations=iterations + 1,
157-
)
158-
)
159-
160-
output[table_class._meta.tablename] = dependents
161-
162-
return output
163-
164-
165-
def sort_table_classes(
166-
table_classes: t.List[t.Type[Table]],
167-
) -> t.List[t.Type[Table]]:
168-
"""
169-
Sort the table classes based on their foreign keys, so they can be created
170-
in the correct order.
171-
"""
172-
table_class_dict = {
173-
table_class._meta.tablename: table_class
174-
for table_class in table_classes
175-
}
176-
177-
graph = _get_graph(table_classes)
178-
179-
sorter = TopologicalSorter(graph)
180-
ordered_tablenames = tuple(sorter.static_order())
181-
182-
output: t.List[t.Type[Table]] = []
183-
for tablename in ordered_tablenames:
184-
table_class = table_class_dict.get(tablename, None)
185-
if table_class is not None:
186-
output.append(table_class)
187-
188-
return output
189-
190-
191120
@dataclass
192121
class MigrationManager:
193122
"""

piccolo/apps/schema/commands/generate.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import black
77
from typing_extensions import Literal
88

9-
from piccolo.apps.migrations.auto.migration_manager import sort_table_classes
109
from piccolo.apps.migrations.auto.serialisation import serialise_params
1110
from piccolo.columns.base import Column
1211
from piccolo.columns.column_types import (
@@ -31,7 +30,7 @@
3130
)
3231
from piccolo.engine.finder import engine_finder
3332
from piccolo.engine.postgres import PostgresEngine
34-
from piccolo.table import Table, create_table_class
33+
from piccolo.table import Table, create_table_class, sort_table_classes
3534
from piccolo.utils.naming import _snake_to_camel
3635

3736
if t.TYPE_CHECKING: # pragma: no cover

piccolo/table.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
from piccolo.query.methods.indexes import Indexes
4141
from piccolo.querystring import QueryString, Unquoted
4242
from piccolo.utils import _camel_to_snake
43+
from piccolo.utils.graphlib import TopologicalSorter
4344
from piccolo.utils.sql_values import convert_to_sql_value
4445

4546
if t.TYPE_CHECKING:
4647
from piccolo.columns import Selectable
4748

48-
4949
PROTECTED_TABLENAMES = ("user",)
5050

5151

@@ -126,7 +126,6 @@ def __str__(cls):
126126

127127

128128
class Table(metaclass=TableMetaclass):
129-
130129
# These are just placeholder values, so type inference isn't confused - the
131130
# actual values are set in __init_subclass__.
132131
_meta = TableMeta()
@@ -975,3 +974,82 @@ def create_table_class(
975974
kwds=class_kwargs,
976975
exec_body=lambda namespace: namespace.update(class_members),
977976
)
977+
978+
979+
def create_tables(*args: t.Type[Table], if_not_exists: bool = False) -> None:
980+
"""
981+
Creates multiple tables that passed to it.
982+
"""
983+
sorted_table_classes = sort_table_classes(list(args))
984+
for table in sorted_table_classes:
985+
Create(table=table, if_not_exists=if_not_exists).run_sync()
986+
987+
988+
def sort_table_classes(
989+
table_classes: t.List[t.Type[Table]],
990+
) -> t.List[t.Type[Table]]:
991+
"""
992+
Sort the table classes based on their foreign keys, so they can be created
993+
in the correct order.
994+
"""
995+
table_class_dict = {
996+
table_class._meta.tablename: table_class
997+
for table_class in table_classes
998+
}
999+
1000+
graph = _get_graph(table_classes)
1001+
1002+
sorter = TopologicalSorter(graph)
1003+
ordered_tablenames = tuple(sorter.static_order())
1004+
1005+
output: t.List[t.Type[Table]] = []
1006+
for tablename in ordered_tablenames:
1007+
table_class = table_class_dict.get(tablename, None)
1008+
if table_class is not None:
1009+
output.append(table_class)
1010+
1011+
return output
1012+
1013+
1014+
def _get_graph(
1015+
table_classes: t.List[t.Type[Table]],
1016+
iterations: int = 0,
1017+
max_iterations: int = 5,
1018+
) -> t.Dict[str, t.Set[str]]:
1019+
"""
1020+
Analyses the tables based on their foreign keys, and returns a data
1021+
structure like:
1022+
1023+
.. code-block:: python
1024+
1025+
{'band': {'manager'}, 'concert': {'band', 'venue'}, 'manager': set()}
1026+
1027+
The keys are tablenames, and the values are tablenames directly connected
1028+
to it via a foreign key.
1029+
1030+
"""
1031+
output: t.Dict[str, t.Set[str]] = {}
1032+
1033+
if iterations >= max_iterations:
1034+
return output
1035+
1036+
for table_class in table_classes:
1037+
dependents: t.Set[str] = set()
1038+
for fk in table_class._meta.foreign_key_columns:
1039+
dependents.add(
1040+
fk._foreign_key_meta.resolved_references._meta.tablename
1041+
)
1042+
1043+
# We also recursively check the related tables to get a fuller
1044+
# picture of the schema and relationships.
1045+
referenced_table = fk._foreign_key_meta.resolved_references
1046+
output.update(
1047+
_get_graph(
1048+
[referenced_table],
1049+
iterations=iterations + 1,
1050+
)
1051+
)
1052+
1053+
output[table_class._meta.tablename] = dependents
1054+
1055+
return output

tests/apps/migrations/auto/test_migration_manager.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
from unittest import TestCase
33
from unittest.mock import MagicMock, patch
44

5-
from piccolo.apps.migrations.auto.migration_manager import (
6-
MigrationManager,
7-
sort_table_classes,
8-
)
5+
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
96
from piccolo.apps.migrations.commands.base import BaseMigrationManager
107
from piccolo.columns import Text, Varchar
118
from piccolo.columns.base import OnDelete, OnUpdate
129
from piccolo.columns.column_types import ForeignKey
1310
from piccolo.conf.apps import AppConfig
14-
from piccolo.table import Table
11+
from piccolo.table import Table, sort_table_classes
1512
from piccolo.utils.lazy_loader import LazyLoader
1613
from tests.base import DBTestCase, postgres_only, set_mock_return_value
1714
from tests.example_apps.music.tables import Band, Concert, Manager, Venue

tests/table/test_create_tables.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from unittest import TestCase
2+
3+
from piccolo.table import create_tables
4+
from tests.example_apps.music.tables import Band, Manager
5+
6+
7+
class TestCreateTables(TestCase):
8+
def tearDown(self) -> None:
9+
Band.alter().drop_table(if_exists=True).run_sync()
10+
Manager.alter().drop_table(if_exists=True).run_sync()
11+
12+
def test_create_tables(self):
13+
create_tables(Manager, Band, if_not_exists=False)
14+
self.assertTrue(Manager.table_exists().run_sync())
15+
self.assertTrue(Band.table_exists().run_sync())

0 commit comments

Comments
 (0)