Skip to content

Commit

Permalink
Names ForeignKey constraints in a consistent way
Browse files Browse the repository at this point in the history
  • Loading branch information
grigi committed Oct 1, 2019
1 parent e0fea4f commit 11473ad
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 26 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Changelog
=========
0.13.10
-------
- Names ForeignKey constraints in a consistent way

0.13.9
------
Expand Down
4 changes: 2 additions & 2 deletions tests/models_schema_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class Tournament(Model):
tid = fields.SmallIntField(pk=True)
name = fields.TextField(description="Tournament name", index=True)
name = fields.CharField(max_length=100, description="Tournament name", index=True)
created = fields.DatetimeField(auto_now_add=True, description="Created */'`/* datetime")

class Meta:
Expand All @@ -16,7 +16,7 @@ class Meta:

class Event(Model):
id = fields.BigIntField(pk=True, description="Event ID")
name = fields.TextField(unique=True)
name = fields.TextField()
tournament = fields.ForeignKeyField(
"models.Tournament", related_name="events", description="FK to tournament"
)
Expand Down
37 changes: 18 additions & 19 deletions tests/test_generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,13 @@ async def test_schema(self):
) /* The TEAMS! */;
CREATE TABLE "tournament" (
"tid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" TEXT NOT NULL /* Tournament name */,
"name" VARCHAR(100) NOT NULL /* Tournament name */,
"created" TIMESTAMP NOT NULL /* Created *\\/'`\\/* datetime */
) /* What Tournaments *\\/'`\\/* we have */;
CREATE INDEX "tournament_name_116110_idx" ON "tournament" (name);
CREATE TABLE "event" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL /* Event ID */,
"name" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"modified" TIMESTAMP NOT NULL,
"prize" VARCHAR(40),
"token" VARCHAR(100) NOT NULL UNIQUE /* Unique token */,
Expand Down Expand Up @@ -216,13 +216,13 @@ async def test_schema_safe(self):
) /* The TEAMS! */;
CREATE TABLE IF NOT EXISTS "tournament" (
"tid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" TEXT NOT NULL /* Tournament name */,
"name" VARCHAR(100) NOT NULL /* Tournament name */,
"created" TIMESTAMP NOT NULL /* Created *\\/'`\\/* datetime */
) /* What Tournaments *\\/'`\\/* we have */;
CREATE INDEX IF NOT EXISTS "tournament_name_116110_idx" ON "tournament" (name);
CREATE TABLE IF NOT EXISTS "event" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL /* Event ID */,
"name" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"modified" TIMESTAMP NOT NULL,
"prize" VARCHAR(40),
"token" VARCHAR(100) NOT NULL UNIQUE /* Unique token */,
Expand Down Expand Up @@ -323,28 +323,28 @@ async def test_schema(self):
`sometable_id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`some_chars_table` VARCHAR(255) NOT NULL,
`fk_sometable` INT,
FOREIGN KEY (`fk_sometable`) REFERENCES `sometable` (`sometable_id`) ON DELETE CASCADE,
CONSTRAINT `fk_sometabl_sometabl_6efae9bd` FOREIGN KEY (`fk_sometable`) REFERENCES `sometable` (`sometable_id`) ON DELETE CASCADE,
KEY `sometable_some_ch_115115_idx` (`some_chars_table`)
) CHARACTER SET utf8mb4;
CREATE TABLE `team` (
`name` VARCHAR(50) NOT NULL PRIMARY KEY COMMENT 'The TEAM name (and PK)',
`manager_id` VARCHAR(50),
FOREIGN KEY (`manager_id`) REFERENCES `team` (`name`) ON DELETE CASCADE
CONSTRAINT `fk_team_team_9c77cd8f` FOREIGN KEY (`manager_id`) REFERENCES `team` (`name`) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COMMENT='The TEAMS!';
CREATE TABLE `tournament` (
`tid` SMALLINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` TEXT NOT NULL COMMENT 'Tournament name',
`name` VARCHAR(100) NOT NULL COMMENT 'Tournament name',
`created` DATETIME(6) NOT NULL COMMENT 'Created */\\'`/* datetime',
KEY `tournament_name_116110_idx` (`name`)
) CHARACTER SET utf8mb4 COMMENT='What Tournaments */\\'`/* we have';
CREATE TABLE `event` (
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Event ID',
`name` TEXT NOT NULL UNIQUE,
`name` TEXT NOT NULL,
`modified` DATETIME(6) NOT NULL,
`prize` DECIMAL(10,2),
`token` VARCHAR(100) NOT NULL UNIQUE COMMENT 'Unique token',
`tournament_id` SMALLINT NOT NULL COMMENT 'FK to tournament',
FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`tid`) ON DELETE CASCADE
CONSTRAINT `fk_event_tourname_51c2b82d` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`tid`) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COMMENT='This table contains a list of all the events';
CREATE TABLE `sometable_self` (
`backward_sts` INT NOT NULL,
Expand Down Expand Up @@ -384,28 +384,28 @@ async def test_schema_safe(self):
`sometable_id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`some_chars_table` VARCHAR(255) NOT NULL,
`fk_sometable` INT,
FOREIGN KEY (`fk_sometable`) REFERENCES `sometable` (`sometable_id`) ON DELETE CASCADE,
CONSTRAINT `fk_sometabl_sometabl_6efae9bd` FOREIGN KEY (`fk_sometable`) REFERENCES `sometable` (`sometable_id`) ON DELETE CASCADE,
KEY `sometable_some_ch_115115_idx` (`some_chars_table`)
) CHARACTER SET utf8mb4;
CREATE TABLE IF NOT EXISTS `team` (
`name` VARCHAR(50) NOT NULL PRIMARY KEY COMMENT 'The TEAM name (and PK)',
`manager_id` VARCHAR(50),
FOREIGN KEY (`manager_id`) REFERENCES `team` (`name`) ON DELETE CASCADE
CONSTRAINT `fk_team_team_9c77cd8f` FOREIGN KEY (`manager_id`) REFERENCES `team` (`name`) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COMMENT='The TEAMS!';
CREATE TABLE IF NOT EXISTS `tournament` (
`tid` SMALLINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` TEXT NOT NULL COMMENT 'Tournament name',
`name` VARCHAR(100) NOT NULL COMMENT 'Tournament name',
`created` DATETIME(6) NOT NULL COMMENT 'Created */\\'`/* datetime',
KEY `tournament_name_116110_idx` (`name`)
) CHARACTER SET utf8mb4 COMMENT='What Tournaments */\\'`/* we have';
CREATE TABLE IF NOT EXISTS `event` (
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Event ID',
`name` TEXT NOT NULL UNIQUE,
`name` TEXT NOT NULL,
`modified` DATETIME(6) NOT NULL,
`prize` DECIMAL(10,2),
`token` VARCHAR(100) NOT NULL UNIQUE COMMENT 'Unique token',
`tournament_id` SMALLINT NOT NULL COMMENT 'FK to tournament',
FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`tid`) ON DELETE CASCADE
CONSTRAINT `fk_event_tourname_51c2b82d` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`tid`) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COMMENT='This table contains a list of all the events';
CREATE TABLE IF NOT EXISTS `sometable_self` (
`backward_sts` INT NOT NULL,
Expand All @@ -425,7 +425,6 @@ async def test_schema_safe(self):
FOREIGN KEY (`event_id`) REFERENCES `event` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`team_id`) REFERENCES `team` (`name`) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COMMENT='How participants relate';
""".strip(), # noqa
)

Expand Down Expand Up @@ -498,7 +497,7 @@ async def test_schema(self):
COMMENT ON TABLE team IS 'The TEAMS!';
CREATE TABLE "tournament" (
"tid" SMALLSERIAL NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"name" VARCHAR(100) NOT NULL,
"created" TIMESTAMP NOT NULL
);
CREATE INDEX "tournament_name_116110_idx" ON "tournament" (name);
Expand All @@ -507,7 +506,7 @@ async def test_schema(self):
COMMENT ON TABLE tournament IS 'What Tournaments */''`/* we have';
CREATE TABLE "event" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"modified" TIMESTAMP NOT NULL,
"prize" DECIMAL(10,2),
"token" VARCHAR(100) NOT NULL UNIQUE,
Expand Down Expand Up @@ -559,7 +558,7 @@ async def test_schema_safe(self):
COMMENT ON TABLE team IS 'The TEAMS!';
CREATE TABLE IF NOT EXISTS "tournament" (
"tid" SMALLSERIAL NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"name" VARCHAR(100) NOT NULL,
"created" TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS "tournament_name_116110_idx" ON "tournament" (name);
Expand All @@ -568,7 +567,7 @@ async def test_schema_safe(self):
COMMENT ON TABLE tournament IS 'What Tournaments */''`/* we have';
CREATE TABLE IF NOT EXISTS "event" (
"id" BIGSERIAL NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"modified" TIMESTAMP NOT NULL,
"prize" DECIMAL(10,2),
"token" VARCHAR(100) NOT NULL UNIQUE,
Expand Down
2 changes: 1 addition & 1 deletion tortoise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,4 +669,4 @@ async def do_stuff():
loop.run_until_complete(Tortoise.close_connections())


__version__ = "0.13.9"
__version__ = "0.13.10"
33 changes: 32 additions & 1 deletion tortoise/backends/base/schema_generator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from hashlib import sha256
from typing import List, Optional, Set # noqa

from tortoise import fields
Expand Down Expand Up @@ -57,7 +58,13 @@ def _create_string(
).strip()

def _create_fk_string(
self, db_field: str, table: str, field: str, on_delete: str, comment: str
self,
constraint_name: str,
db_field: str,
table: str,
field: str,
on_delete: str,
comment: str,
) -> str:
return self.FK_TEMPLATE.format(
db_field=db_field, table=table, field=field, on_delete=on_delete, comment=comment
Expand Down Expand Up @@ -101,11 +108,18 @@ def _get_inner_statements(self) -> List[str]:
@staticmethod
def _make_hash(*args: str, length: int) -> str:
# Hash a set of string values and get a digest of the given length.
# TODO: This is super-bad from a cryptographic POV.
# Replace in a major version to minimise unexpected breakages.
letters = [] # type: List[str]
for i_th_letters in zip(*args):
letters.extend(i_th_letters)
return "".join([str(ord(letter)) for letter in letters])[:length]

@staticmethod
def _make_hash2(*args: str, length: int) -> str:
# Hash a set of string values and get a digest of the given length.
return sha256(";".join(args).encode("utf-8")).hexdigest()[:length]

def _generate_index_name(self, model, field_names: List[str]) -> str:
# NOTE: for compatibility, index name should not be longer than 30
# characters (Oracle limit).
Expand All @@ -118,6 +132,17 @@ def _generate_index_name(self, model, field_names: List[str]) -> str:
)
return index_name

def _generate_fk_name(self, from_table, from_field, to_table, to_field) -> str:
# NOTE: for compatibility, index name should not be longer than 30
# characters (Oracle limit).
# That's why we slice some of the strings here.
index_name = "fk_{f}_{t}_{h}".format(
f=from_table[:8],
t=to_table[:8],
h=self._make_hash2(from_table, from_field, to_table, to_field, length=8),
)
return index_name

def _get_index_sql(self, model, field_names: List[str], safe: bool) -> str:
return self.INDEX_CREATE_TEMPLATE.format(
exists="IF NOT EXISTS " if safe else "",
Expand Down Expand Up @@ -188,6 +213,12 @@ def _get_table_sql(self, model, safe=True) -> dict:
is_pk=field_object.pk,
comment="",
) + self._create_fk_string(
constraint_name=self._generate_fk_name(
model._meta.table,
db_field,
field_object.reference.type._meta.table,
field_object.reference.type._meta.db_pk_field,
),
db_field=db_field,
table=field_object.reference.type._meta.table,
field=field_object.reference.type._meta.db_pk_field,
Expand Down
17 changes: 14 additions & 3 deletions tortoise/backends/mysql/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class MySQLSchemaGenerator(BaseSchemaGenerator):
INDEX_CREATE_TEMPLATE = "KEY `{index_name}` ({fields})"
FIELD_TEMPLATE = "`{name}` {type} {nullable} {unique}{primary}{comment}"
FK_TEMPLATE = (
"FOREIGN KEY (`{db_field}`) REFERENCES `{table}` (`{field}`) ON DELETE {on_delete}"
"CONSTRAINT `{constraint_name}` FOREIGN KEY (`{db_field}`)"
" REFERENCES `{table}` (`{field}`) ON DELETE {on_delete}"
)
M2M_TABLE_TEMPLATE = (
"CREATE TABLE {exists}`{table_name}` (\n"
Expand Down Expand Up @@ -66,11 +67,21 @@ def _get_index_sql(self, model, field_names: List[str], safe: bool) -> str:
return ""

def _create_fk_string(
self, db_field: str, table: str, field: str, on_delete: str, comment: str
self,
constraint_name: str,
db_field: str,
table: str,
field: str,
on_delete: str,
comment: str,
) -> str:
self._foreign_keys.append(
self.FK_TEMPLATE.format(
db_field=db_field, table=table, field=field, on_delete=on_delete
constraint_name=constraint_name,
db_field=db_field,
table=table,
field=field,
on_delete=on_delete,
)
)
return comment
Expand Down
7 changes: 7 additions & 0 deletions tortoise/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ class TextField(Field, str): # type: ignore[misc] # noqa

__slots__ = ()

def __init__(self, **kwargs) -> None:
if kwargs.pop("unique", None) or kwargs.pop("index", None):
raise ConfigurationError("TextField can't be indexed")
super().__init__(**kwargs)


class BooleanField(Field):
"""
Expand Down Expand Up @@ -309,6 +314,8 @@ class JSONField(Field, dict, list): # type: ignore[misc] # noqa
__slots__ = ("encoder", "decoder")

def __init__(self, encoder=JSON_DUMPS, decoder=JSON_LOADS, **kwargs) -> None:
if kwargs.pop("unique", None) or kwargs.pop("index", None):
raise ConfigurationError("JSONField can't be indexed")
super().__init__(**kwargs)
self.encoder = encoder
self.decoder = decoder
Expand Down

0 comments on commit 11473ad

Please sign in to comment.