Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/src/piccolo/schema/column_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ UUID
Text
****

====
Char
====

.. autoclass:: Char

======
Secret
======
Expand Down
4 changes: 3 additions & 1 deletion piccolo/apps/playground/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
UUID,
Array,
Boolean,
Char,
Date,
ForeignKey,
Integer,
Expand Down Expand Up @@ -73,6 +74,7 @@ class Venue(Table):
name = Varchar(length=100)
capacity = Integer(default=0)
address = Text(null=True)
country_code = Char(length=2)

@classmethod
def get_readable(cls) -> Readable:
Expand Down Expand Up @@ -246,7 +248,7 @@ def populate():
c_sharps = Band(name="C-Sharps", popularity=700, manager=anders.id)
c_sharps.save().run_sync()

venue = Venue(name="Amazing Venue", capacity=5000)
venue = Venue(name="Amazing Venue", capacity=5000, country_code="GB")
venue.save().run_sync()

concert = Concert(
Expand Down
3 changes: 3 additions & 0 deletions piccolo/apps/schema/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
BigInt,
Boolean,
Bytea,
Char,
Date,
DoublePrecision,
ForeignKey,
Expand Down Expand Up @@ -291,6 +292,7 @@ def __add__(self, value: OutputSchema) -> OutputSchema:
"bigint": BigInt,
"boolean": Boolean,
"bytea": Bytea,
"char": Char,
"character varying": Varchar,
"date": Date,
"integer": Integer,
Expand Down Expand Up @@ -318,6 +320,7 @@ def __add__(self, value: OutputSchema) -> OutputSchema:
Boolean: re.compile(r"^(?P<value>true|false)$"),
Bytea: re.compile(r"'(?P<value>.*)'::bytea$"),
DoublePrecision: re.compile(r"(?P<value>[+-]?(?:[0-9]*[.])?[0-9]+)"),
Char: re.compile(r"^'(?P<value>.*)'::bpchar$"),
Varchar: re.compile(r"^'(?P<value>.*)'::character varying$"),
Date: re.compile(r"^(?P<value>(?:\d{4}-\d{2}-\d{2})|CURRENT_DATE)$"),
Integer: re.compile(r"^(?P<value>-?\d+)$"),
Expand Down
2 changes: 2 additions & 0 deletions piccolo/columns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BigSerial,
Boolean,
Bytea,
Char,
Date,
Decimal,
DoublePrecision,
Expand Down Expand Up @@ -46,6 +47,7 @@
"BigSerial",
"Boolean",
"Bytea",
"Char",
"Date",
"Decimal",
"DoublePrecision",
Expand Down
42 changes: 42 additions & 0 deletions piccolo/columns/column_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,48 @@ class Email(Varchar):
pass


class Char(Varchar):
"""
Used for storing text when you want to enforce character length limits.
Uses the ``str`` type for values.

.. hint::
:class:`Varchar` is generally recommended over :class:`Char`. However,
:class:`Char` may have slight performance advantages, and makes sense
for certain fields like country codes where you know the values will be
an exact length.

In Postgres the values are 'blank padded'. For example, if the length is 2,
and you store ``'X'``, then you will get ``'X '`` back from the database.

.. note::
In SQLite :class:`Char` and :class:`Varchar` behave identically.

**Example**

.. code-block:: python

class Venue(Table):
name = Varchar()
country_code = Char(length=2)

# Create
>>> await Venue(name='Amazing Venue', country_code='GB').save()

# Query
>>> await Venue.select(Venue.country_code)
{'country_code': 'GB'}

:param length:
The maximum number of characters allowed.

"""

@property
def column_type(self):
return f"CHAR({self.length})" if self.length else "CHAR"


class Secret(Varchar):
"""
This is just an alias to ``Varchar(secret=True)``. It's here for backwards
Expand Down
20 changes: 20 additions & 0 deletions tests/apps/migrations/auto/integration/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
BigInt,
BigSerial,
Boolean,
Char,
Date,
Decimal,
DoublePrecision,
Expand Down Expand Up @@ -271,6 +272,25 @@ def test_varchar_column(self):
),
)

@engines_skip("cockroach")
def test_char_column(self):
self._test_migrations(
table_snapshots=[
[self.table(column)]
for column in [
Char(default="GB", length=2, null=True),
Char(default="GB", length=2, null=False),
]
],
test_function=lambda x: all(
[
x.data_type == "character",
x.is_nullable == "NO",
x.column_default in ("'GB'::bpchar", "'GB':::STRING"),
]
),
)

def test_text_column(self):
self._test_migrations(
table_snapshots=[
Expand Down
37 changes: 37 additions & 0 deletions tests/columns/test_char.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from piccolo.columns.column_types import Char
from piccolo.table import Table
from piccolo.testing.test_case import AsyncTableTest
from tests.base import engines_only


class Venue(Table):
country_code = Char(length=2)


@engines_only("postgres", "cockroach")
class TestChar(AsyncTableTest):
"""
SQLite doesn't enforce any constraints on max character length.

https://www.sqlite.org/faq.html#q9
"""

tables = [Venue]

async def test_length(self):
row = Venue(country_code="GB")
await row.save()

with self.assertRaises(Exception):
row.country_code = "XXX"
await row.save()

async def test_padding(self):
"""
Postgres returns ``Char`` columns padded with blank spaces.
"""
row = Venue(country_code="G")
await row.save()

await row.refresh()
self.assertEqual(row.country_code, "G ")
Loading