Skip to content

Commit 6f3e4f9

Browse files
support-custom-field-primary-key (piccolo-orm#136)
* draft * restore PrimaryKey with deprecation * fix lint and skip test_primary_key * add support for sqlite versions before 3.35.0 * bump coverages * modify sqlite coverage * reverting coverage changes * check version using sqlite3 * fix minor comments * change example app migration files The example app had some new migration files added by mistake. I've removed these, and made sure migrations exist for all tables. * add failing tests * add custom primary key definition * revert insert callback * Fix lint and some tests * fix more tests * fix remaining tests * fix circular import error When testing this branch with some Piccolo apps, I sometimes got a circular import error. * make sure id column is assigned to Table class * make sure foreign keys reference the correct primary key name * fix bug with convert_to_sql_value * black tweaks Adding an explicit black version to `dev-requirements.txt`, which definitely supports Python 3.9. * fixing a bug with `remove` * improve the `remove` unittest It was pretty terrible before, and let a bug slip through. * making sure the foreign key type matches the referenced table's primary key type * merge the primary key tests into a single file * replace `primary` and `key` kwargs with `primary_key` Seems to work fine in terms of being backwards compatible with existing migrations. * draft migration change * add automatic `id` to `non_default_columns` too * create table with columns, and avoid adding columns * add `id_column` to migration_manager_tests * fix `test_migrations_manager` adding columns after create table * add new migrations from `example_app` in `tests` * undo latest `example_app` migration * add `primary_key` docs * add automatic `id` column to `default_columns` * ignore lgtm error Co-authored-by: Daniel Townsend <[email protected]>
1 parent 809190f commit 6f3e4f9

34 files changed

+615
-131
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dist/
1212
piccolo.sqlite
1313
_build/
1414
.coverage
15+
coverage.xml
1516
*.sqlite
1617
htmlcov/
1718
prof/

docs/src/piccolo/schema/defining.rst

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,40 @@ For a full list of columns, see :ref:`ColumnTypes`.
2323

2424
-------------------------------------------------------------------------------
2525

26-
Default columns
26+
Primary Key
2727
---------------
2828

29-
id
30-
~~
31-
32-
Each table is automatically given a ``PrimaryKey`` column called ``id``, which
33-
is an auto incrementing integer.
29+
You can specify your ``PrimaryKey`` with any column type by passing ``primary_key`` to the ``Column``.
3430

3531
It is used to uniquely identify a row, and is referenced by ``ForeignKey``
3632
columns on other tables.
3733

38-
If you specify your own ``id`` column, you may get unexpected behaviour, so
39-
it's not recommended at the moment.
34+
.. code-block:: python
35+
36+
# tables.py
37+
from piccolo.table import Table
38+
from piccolo.columns import UUID, Varchar
39+
40+
41+
class Band(Table):
42+
id = UUID(primary_key=True)
43+
name = Varchar(length=100)
44+
45+
If you don't specify a ``PrimaryKey``, the table is automatically given a ``PrimaryKey`` column called ``id``, which
46+
is an auto incrementing integer.
47+
48+
This is equivalent to:
49+
50+
.. code-block:: python
51+
52+
# tables.py
53+
from piccolo.table import Table
54+
from piccolo.columns import Serial, Varchar
55+
56+
57+
class Band(Table):
58+
id = Serial(primary_key=True)
59+
name = Varchar(length=100)
4060
4161
-------------------------------------------------------------------------------
4262

piccolo/apps/migrations/auto/migration_manager.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -569,20 +569,23 @@ async def _run_add_tables(self, backwards=False):
569569
).run()
570570
else:
571571
for add_table in self.add_tables:
572+
add_columns: t.List[
573+
AddColumnClass
574+
] = self.add_columns.for_table_class_name(add_table.class_name)
572575
_Table: t.Type[Table] = create_table_class(
573576
class_name=add_table.class_name,
574577
class_kwargs={"tablename": add_table.tablename},
575578
class_members={
576-
column._meta.name: column
577-
for column in add_table.columns
579+
add_column.column._meta.name: add_column.column
580+
for add_column in add_columns
578581
},
579582
)
580583

581584
await _Table.create_table().run()
582585

583586
async def _run_add_columns(self, backwards=False):
584587
"""
585-
Add columns, which belong to new and existing tables
588+
Add columns, which belong to existing tables
586589
"""
587590
if backwards:
588591
for add_column in self.add_columns.add_columns:
@@ -601,6 +604,9 @@ async def _run_add_columns(self, backwards=False):
601604
await _Table.alter().drop_column(add_column.column).run()
602605
else:
603606
for table_class_name in self.add_columns.table_class_names:
607+
if table_class_name in [i.class_name for i in self.add_tables]:
608+
continue # No need to add columns to new tables
609+
604610
add_columns: t.List[
605611
AddColumnClass
606612
] = self.add_columns.for_table_class_name(table_class_name)

piccolo/apps/user/tables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ async def login(cls, username: str, password: str) -> t.Optional[int]:
134134
"""
135135
query = (
136136
cls.select()
137-
.columns(cls.id, cls.password)
137+
.columns(cls._meta.primary_key, cls.password)
138138
.where((cls.username == username))
139139
.first()
140140
)

piccolo/columns/base.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,7 @@ class ColumnMeta:
121121

122122
# General attributes:
123123
null: bool = False
124-
primary: bool = False
125-
key: bool = False
124+
primary_key: bool = False
126125
unique: bool = False
127126
index: bool = False
128127
index_method: IndexMethod = IndexMethod.btree
@@ -253,12 +252,9 @@ class Column(Selectable):
253252
:param null:
254253
Whether the column is nullable.
255254
256-
:param primary:
255+
:param primary_key:
257256
If set, the column is used as a primary key.
258257
259-
:param key:
260-
If set, the column is treated as a key.
261-
262258
:param default:
263259
The column value to use if not specified by the user.
264260
@@ -291,8 +287,7 @@ class Column(Selectable):
291287
def __init__(
292288
self,
293289
null: bool = False,
294-
primary: bool = False,
295-
key: bool = False,
290+
primary_key: bool = False,
296291
unique: bool = False,
297292
index: bool = False,
298293
index_method: IndexMethod = IndexMethod.btree,
@@ -301,6 +296,12 @@ def __init__(
301296
choices: t.Optional[t.Type[Enum]] = None,
302297
**kwargs,
303298
) -> None:
299+
# This is for backwards compatibility - originally there were two
300+
# separate arguments `primary` and `key`, but they have now been merged
301+
# into `primary_key`.
302+
if (kwargs.get("primary") is True) and (kwargs.get("key") is True):
303+
primary_key = True
304+
304305
# Used for migrations.
305306
# We deliberately omit 'required', and 'help_text' as they don't effect
306307
# the actual schema.
@@ -309,8 +310,7 @@ def __init__(
309310
kwargs.update(
310311
{
311312
"null": null,
312-
"primary": primary,
313-
"key": key,
313+
"primary_key": primary_key,
314314
"unique": unique,
315315
"index": index,
316316
"index_method": index_method,
@@ -329,8 +329,7 @@ def __init__(
329329

330330
self._meta = ColumnMeta(
331331
null=null,
332-
primary=primary,
333-
key=key,
332+
primary_key=primary_key,
334333
unique=unique,
335334
index=index,
336335
index_method=index_method,
@@ -575,10 +574,8 @@ def querystring(self) -> QueryString:
575574
Used when creating tables.
576575
"""
577576
query = f'"{self._meta.name}" {self.column_type}'
578-
if self._meta.primary:
579-
query += " PRIMARY"
580-
if self._meta.key:
581-
query += " KEY"
577+
if self._meta.primary_key:
578+
query += " PRIMARY KEY"
582579
if self._meta.unique:
583580
query += " UNIQUE"
584581
if not self._meta.null:
@@ -588,16 +585,18 @@ def querystring(self) -> QueryString:
588585
self, "_foreign_key_meta", None
589586
)
590587
if foreign_key_meta:
591-
tablename = foreign_key_meta.resolved_references._meta.tablename
588+
references = foreign_key_meta.resolved_references
589+
tablename = references._meta.tablename
592590
on_delete = foreign_key_meta.on_delete.value
593591
on_update = foreign_key_meta.on_update.value
592+
primary_key_name = references._meta.primary_key._meta.name
594593
query += (
595-
f" REFERENCES {tablename} (id)"
594+
f" REFERENCES {tablename} ({primary_key_name})"
596595
f" ON DELETE {on_delete}"
597596
f" ON UPDATE {on_update}"
598597
)
599598

600-
if not self._meta.primary:
599+
if not self._meta.primary_key:
601600
default = self.get_default_value()
602601
sql_value = self.get_sql_value(value=default)
603602
# Escape the value if it contains a pair of curly braces, otherwise

piccolo/columns/column_types.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import decimal
5+
import inspect
56
import typing as t
67
import uuid
78
from datetime import date, datetime, time, timedelta
@@ -28,6 +29,7 @@
2829
from piccolo.columns.reference import LazyTableReference
2930
from piccolo.querystring import QueryString, Unquoted
3031
from piccolo.utils.encoding import dump_json
32+
from piccolo.utils.warnings import colored_warning
3133

3234
if t.TYPE_CHECKING: # pragma: no cover
3335
from piccolo.columns.base import ColumnMeta
@@ -493,20 +495,15 @@ def column_type(self):
493495
###############################################################################
494496

495497

498+
DEFAULT = Unquoted("DEFAULT")
499+
NULL = Unquoted("null")
500+
501+
496502
class Serial(Column):
497503
"""
498504
An alias to an autoincrementing integer column in Postgres.
499505
"""
500506

501-
def __init__(self, **kwargs) -> None:
502-
super().__init__(**kwargs)
503-
504-
505-
DEFAULT = Unquoted("DEFAULT")
506-
NULL = Unquoted("null")
507-
508-
509-
class PrimaryKey(Column):
510507
@property
511508
def column_type(self):
512509
engine_type = self._meta.table._meta.db.engine_type
@@ -524,10 +521,19 @@ def default(self):
524521
return NULL
525522
raise Exception("Unrecognized engine type")
526523

524+
525+
class PrimaryKey(Serial):
527526
def __init__(self, **kwargs) -> None:
528527
# Set the index to False, as a database should automatically create
529528
# an index for a PrimaryKey column.
530-
kwargs.update({"primary": True, "key": True, "index": False})
529+
kwargs.update({"primary_key": True, "index": False})
530+
531+
colored_warning(
532+
"`PrimaryKey` is deprecated and "
533+
"will be removed in future versions.",
534+
category=DeprecationWarning,
535+
)
536+
531537
super().__init__(**kwargs)
532538

533539

@@ -974,9 +980,10 @@ class Float(Real):
974980
###############################################################################
975981

976982

977-
class ForeignKey(Integer):
983+
class ForeignKey(Column): # lgtm [py/missing-equals]
978984
"""
979-
Used to reference another table. Uses the ``int`` type for values.
985+
Used to reference another table. Uses the same type as the primary key
986+
column on the table it references.
980987
981988
**Example**
982989
@@ -1139,33 +1146,60 @@ class Band(Table):
11391146
11401147
""" # noqa: E501
11411148

1142-
column_type = "INTEGER"
1149+
_foreign_key_meta: ForeignKeyMeta
1150+
1151+
@property
1152+
def column_type(self):
1153+
"""
1154+
A ForeignKey column needs to have the same type as the primary key
1155+
column of the table being referenced.
1156+
"""
1157+
referenced_table = self._foreign_key_meta.resolved_references
1158+
pk_column = referenced_table._meta.primary_key
1159+
if isinstance(pk_column, Serial):
1160+
return Integer().column_type
1161+
else:
1162+
return pk_column.column_type
11431163

11441164
def __init__(
11451165
self,
11461166
references: t.Union[t.Type[Table], LazyTableReference, str],
1147-
default: t.Union[int, Enum, None] = None,
1167+
default: t.Any = None,
11481168
null: bool = True,
11491169
on_delete: OnDelete = OnDelete.cascade,
11501170
on_update: OnUpdate = OnUpdate.cascade,
11511171
**kwargs,
11521172
) -> None:
1153-
self._validate_default(default, (int, None))
1173+
from piccolo.table import Table
1174+
1175+
if inspect.isclass(references):
1176+
references = t.cast(t.Type, references)
1177+
if issubclass(references, Table):
1178+
# Using this to validate the default value - will raise a
1179+
# ValueError if incorrect.
1180+
if isinstance(references._meta.primary_key, Serial):
1181+
Integer(default=default, null=null)
1182+
else:
1183+
references._meta.primary_key.__class__(
1184+
default=default, null=null
1185+
)
1186+
1187+
self.default = default
11541188

11551189
kwargs.update(
11561190
{
11571191
"references": references,
11581192
"on_delete": on_delete,
11591193
"on_update": on_update,
1194+
"null": null,
11601195
}
11611196
)
1162-
super().__init__(default=default, null=null, **kwargs)
1197+
1198+
super().__init__(**kwargs)
11631199

11641200
# This is here just for type inference - the actual value is set by
11651201
# the Table metaclass. We can't set the actual value here, as
11661202
# only the metaclass has access to the table.
1167-
from piccolo.table import Table
1168-
11691203
self._foreign_key_meta = ForeignKeyMeta(
11701204
Table, OnDelete.cascade, OnUpdate.cascade
11711205
)

piccolo/engine/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import typing as t
44
from abc import ABCMeta, abstractmethod
55

6+
from piccolo.querystring import QueryString
67
from piccolo.utils.sync import run_sync
78
from piccolo.utils.warnings import Level, colored_warning
89

@@ -41,6 +42,10 @@ async def prep_database(self):
4142
async def batch(self, query: Query, batch_size: int = 100) -> Batch:
4243
pass
4344

45+
@abstractmethod
46+
async def run_querystring(self, querystring: QueryString, in_pool: bool):
47+
pass
48+
4449
async def check_version(self):
4550
"""
4651
Warn if the database version isn't supported.

0 commit comments

Comments
 (0)