Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Changelog
Added
^^^^^
- ``Q.__bool__()`` so ``Q`` objects with no filters/children (including nested empty ``Q`` children) are falsy.
- ``db_collation`` argument on fields to set a column collation, emitted as a ``COLLATE`` clause in the generated schema. (#686)

1.1.8
-----
Expand Down
12 changes: 12 additions & 0 deletions tests/schema/models_collation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
This example demonstrates SQL Schema generation for fields that set a db_collation.
"""

from tortoise import fields
from tortoise.models import Model


class Account(Model):
name = fields.CharField(max_length=50, db_collation="NOCASE")
bio = fields.TextField(db_collation="NOCASE")
plain = fields.CharField(max_length=20)
19 changes: 19 additions & 0 deletions tests/schema/test_generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,25 @@ async def test_schema_no_db_constraint():
await _teardown_tortoise()


@pytest.mark.asyncio
async def test_schema_db_collation():
await _reset_tortoise()
try:
await _init_for_sqlite("tests.schema.models_collation")
sql = get_schema_sql(connections.get("default"), safe=False)
assert (
sql.strip()
== """CREATE TABLE "account" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" VARCHAR(50) COLLATE NOCASE NOT NULL,
"bio" TEXT COLLATE NOCASE NOT NULL,
"plain" VARCHAR(20) NOT NULL
);"""
)
finally:
await _teardown_tortoise()


@pytest.mark.asyncio
async def test_schema():
await _reset_tortoise()
Expand Down
13 changes: 12 additions & 1 deletion tortoise/backends/base/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
class BaseSchemaGenerator(SchemaQuotingMixin):
DIALECT = "sql"
TABLE_CREATE_TEMPLATE = "CREATE TABLE {exists}{table_name} ({fields}){extra}{comment};"
FIELD_TEMPLATE = '"{name}" {type}{nullable}{unique}{primary}{default}{comment}'
FIELD_TEMPLATE = '"{name}" {type}{collate}{nullable}{unique}{primary}{default}{comment}'
INDEX_CREATE_TEMPLATE = (
'CREATE {index_type}INDEX {exists}"{index_name}" ON {table_name} ({fields}){extra};'
)
Expand All @@ -52,12 +52,14 @@ def _create_string(
is_primary_key: bool,
comment: str,
default: str,
collation: str = "",
) -> str:
# children can override this function to customize their sql queries

return self.FIELD_TEMPLATE.format(
name=db_column,
type=field_type,
collate=collation,
nullable=nullable,
unique="" if is_primary_key else unique,
comment=comment if self.client.capabilities.inline_comment else "",
Expand Down Expand Up @@ -103,6 +105,11 @@ def _column_comment_generator(self, table: str, column: str, comment: str) -> st
# needs to be implemented for each supported client
raise NotImplementedError() # pragma: nocoverage

def _column_collation_generator(self, collation: str) -> str:
# The collation name is a bare identifier for most dialects. Backends that
# need it quoted (e.g. Postgres) override this.
return f" COLLATE {collation}"

def _post_table_hook(self) -> str:
# This method provides a mechanism where you can perform a set of
# operation on the database table after it's initialized. This method
Expand Down Expand Up @@ -248,6 +255,8 @@ def _get_field_sql_and_related_table(
nullable = " NOT NULL" if not field_object.null else ""
unique = " UNIQUE" if field_object.unique else ""
field_type = field_object.get_for_dialect(self.DIALECT, "SQL_TYPE")
db_collation = getattr(field_object, "db_collation", None)
collation = self._column_collation_generator(db_collation) if db_collation else ""
qualified_table_name = self._qualify_table_name(table_name, schema)

field_creation_string, related_table_name = "", ""
Expand All @@ -270,6 +279,7 @@ def _get_field_sql_and_related_table(
is_primary_key=field_object.pk,
comment="",
default=default,
collation=collation,
) + self._create_fk_string(
constraint_name=self._get_fk_name(
table_name,
Expand All @@ -292,6 +302,7 @@ def _get_field_sql_and_related_table(
is_primary_key=field_object.pk,
comment=comment,
default=default,
collation=collation,
)
return field_creation_string, related_table_name

Expand Down
4 changes: 4 additions & 0 deletions tortoise/backends/base_postgres/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def _column_comment_generator(self, table: str, column: str, comment: str) -> st
self.comments_array.append(comment)
return ""

def _column_collation_generator(self, collation: str) -> str:
# Postgres collation names are identifiers and need double quoting.
return f' COLLATE "{collation}"'

def _post_table_hook(self) -> str:
val = "\n".join(self.comments_array)
self.comments_array = []
Expand Down
4 changes: 3 additions & 1 deletion tortoise/backends/mssql/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class MSSQLSchemaGenerator(MSSQLQuotingMixin, BaseSchemaGenerator):
DIALECT = "mssql"
TABLE_CREATE_TEMPLATE = "CREATE TABLE {table_name} ({fields}){extra};"
FIELD_TEMPLATE = "[{name}] {type}{nullable}{unique}{primary}{default}"
FIELD_TEMPLATE = "[{name}] {type}{collate}{nullable}{unique}{primary}{default}"
INDEX_CREATE_TEMPLATE = "CREATE INDEX [{index_name}] ON {table_name} ({fields});"
UNIQUE_CONSTRAINT_CREATE_TEMPLATE = "CONSTRAINT [{index_name}] UNIQUE ({fields})"
GENERATED_PK_TEMPLATE = "[{field_name}] {generated_sql}"
Expand Down Expand Up @@ -109,6 +109,7 @@ def _create_string(
is_primary_key: bool,
comment: str,
default: str,
collation: str = "",
) -> str:
if nullable == "":
unique = ""
Expand All @@ -120,6 +121,7 @@ def _create_string(
is_primary_key=is_primary_key,
comment=comment,
default=default,
collation=collation,
)

def _get_inner_statements(self) -> list[str]:
Expand Down
2 changes: 1 addition & 1 deletion tortoise/backends/mysql/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MySQLSchemaGenerator(MySQLQuotingMixin, BaseSchemaGenerator):
INDEX_CREATE_TEMPLATE = "{index_type}KEY `{index_name}` ({fields}){extra}"
UNIQUE_CONSTRAINT_CREATE_TEMPLATE = "UNIQUE KEY `{index_name}` ({fields})"
UNIQUE_INDEX_CREATE_TEMPLATE = UNIQUE_CONSTRAINT_CREATE_TEMPLATE
FIELD_TEMPLATE = "`{name}` {type}{nullable}{unique}{primary}{comment}{default}"
FIELD_TEMPLATE = "`{name}` {type}{collate}{nullable}{unique}{primary}{comment}{default}"
GENERATED_PK_TEMPLATE = "`{field_name}` {generated_sql}{comment}"
FK_TEMPLATE = (
"{constraint}FOREIGN KEY (`{db_column}`)"
Expand Down
2 changes: 1 addition & 1 deletion tortoise/backends/oracle/schema_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class OracleSchemaGenerator(BaseSchemaGenerator):
DIALECT = "oracle"
TABLE_CREATE_TEMPLATE = "CREATE TABLE {table_name} ({fields}){extra};"
FIELD_TEMPLATE = '"{name}" {type}{default}{nullable}{unique}{primary}'
FIELD_TEMPLATE = '"{name}" {type}{collate}{default}{nullable}{unique}{primary}'
TABLE_COMMENT_TEMPLATE = "COMMENT ON TABLE {table} IS '{comment}';"
COLUMN_COMMENT_TEMPLATE = "COMMENT ON COLUMN {table}.\"{column}\" IS '{comment}';"
INDEX_CREATE_TEMPLATE = 'CREATE INDEX "{index_name}" ON {table_name} ({fields});'
Expand Down
6 changes: 6 additions & 0 deletions tortoise/fields/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class Field(Generic[VALUE], metaclass=_FieldMeta):
:param description: Field description. Will also appear in ``Tortoise.describe_model()``
and as DB comments in the generated DDL.
:param validators: Validators for this field.
:param db_collation: Set a database collation for the column, emitted as ``COLLATE`` in the
generated DDL. Only meaningful for text based columns and the value is passed through to
the database as given, so use a collation the target database knows (for example
``NOCASE`` on SQLite or ``utf8mb4_unicode_ci`` on MySQL).

**Class Attributes:**
These attributes needs to be defined when defining an actual field type.
Expand Down Expand Up @@ -230,6 +234,7 @@ def __init__(
description: str | None = None,
model: Model | None = None,
validators: list[Validator | Callable] | None = None,
db_collation: str | None = None,
**kwargs: Any,
) -> None:
if (index := kwargs.pop("index", None)) is not None:
Expand Down Expand Up @@ -284,6 +289,7 @@ def __init__(
self.index = bool(db_index)
self.model_field_name = ""
self.description = description
self.db_collation = db_collation
self.docstring: str | None = None
self.validators: list[Validator | Callable] = validators or []
# TODO: consider making this not be set from constructor
Expand Down
Loading