Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1010 Add support for CAST to convert between types #1011

Merged
merged 3 commits into from
Jun 7, 2024
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
1 change: 1 addition & 0 deletions docs/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ batteries included.
piccolo/getting_started/index
piccolo/query_types/index
piccolo/query_clauses/index
piccolo/functions/index
piccolo/schema/index
piccolo/projects_and_apps/index
piccolo/engines/index
Expand Down
34 changes: 34 additions & 0 deletions docs/src/piccolo/functions/aggregate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Aggregate functions
===================

.. currentmodule:: piccolo.query.functions.aggregate

Avg
---

.. autoclass:: Avg
:class-doc-from: class

Count
-----

.. autoclass:: Count
:class-doc-from: class

Min
---

.. autoclass:: Min
:class-doc-from: class

Max
---

.. autoclass:: Max
:class-doc-from: class

Sum
---

.. autoclass:: Sum
:class-doc-from: class
53 changes: 53 additions & 0 deletions docs/src/piccolo/functions/basic_usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Basic Usage
===========

Select queries
--------------

Functions can be used in ``select`` queries - here's an example, where we
convert the values to uppercase:

.. code-block:: python

>>> from piccolo.query.functions import Upper

>>> await Band.select(
... Upper(Band.name, alias="name")
... )

[{"name": "PYTHONISTAS"}]

Where clauses
-------------

Functions can also be used in ``where`` clauses.

.. code-block:: python

>>> from piccolo.query.functions import Length

>>> await Band.select(
... Band.name
... ).where(
... Length(Band.name) > 10
... )

[{"name": "Pythonistas"}]

Update queries
--------------

And even in ``update`` queries:

.. code-block:: python

>>> from piccolo.query.functions import Upper

>>> await Band.update(
... {Band.name: Upper(Band.name)},
... force=True
... ).returning(Band.name)

[{"name": "PYTHONISTAS"}, {"name": "RUSTACEANS"}, {"name": "C-SHARPS"}]

Pretty much everywhere.
12 changes: 12 additions & 0 deletions docs/src/piccolo/functions/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Functions
=========

Functions can be used to modify how queries are run, and what is returned.

.. toctree::
:maxdepth: 1

./basic_usage
./string
./type_conversion
./aggregate
40 changes: 40 additions & 0 deletions docs/src/piccolo/functions/string.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
String functions
================

.. currentmodule:: piccolo.query.functions.string

Length
------

.. autoclass:: Length
:class-doc-from: class

Lower
-----

.. autoclass:: Lower
:class-doc-from: class

Ltrim
-----

.. autoclass:: Ltrim
:class-doc-from: class

Reverse
-------

.. autoclass:: Reverse
:class-doc-from: class

Rtrim
-----

.. autoclass:: Rtrim
:class-doc-from: class

Upper
-----

.. autoclass:: Upper
:class-doc-from: class
25 changes: 25 additions & 0 deletions docs/src/piccolo/functions/type_conversion.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Type conversion functions
=========================

Cast
----

.. currentmodule:: piccolo.query.functions.type_conversion

.. autoclass:: Cast

Notes on databases
------------------

Postgres and CockroachDB have very rich type systems, and you can convert
between most types. SQLite is more limited.

The following query will work in Postgres / Cockroach, but you might get
unexpected results in SQLite, because it doesn't have a native ``TIME`` column
type:

.. code-block:: python

>>> from piccolo.columns import Time
>>> from piccolo.query.functions import Cast
>>> await Concert.select(Cast(Concert.starts, Time()))
4 changes: 4 additions & 0 deletions piccolo/columns/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ def table(self) -> t.Type[Table]:
)
return self._table

@table.setter
def table(self, value: t.Type[Table]):
self._table = value

###########################################################################

# Used by Foreign Keys:
Expand Down
2 changes: 2 additions & 0 deletions piccolo/query/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from .aggregate import Avg, Count, Max, Min, Sum
from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
from .type_conversion import Cast

__all__ = (
"Avg",
"Cast",
"Count",
"Length",
"Lower",
Expand Down
24 changes: 12 additions & 12 deletions piccolo/query/functions/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ class Avg(Function):

.. code-block:: python

await Band.select(Avg(Band.popularity)).run()
await Band.select(Avg(Band.popularity))

# We can use an alias. These two are equivalent:

await Band.select(
Avg(Band.popularity, alias="popularity_avg")
).run()
)

await Band.select(
Avg(Band.popularity).as_alias("popularity_avg")
).run()
)

"""

Expand Down Expand Up @@ -103,17 +103,17 @@ class Min(Function):

.. code-block:: python

await Band.select(Min(Band.popularity)).run()
await Band.select(Min(Band.popularity))

# We can use an alias. These two are equivalent:

await Band.select(
Min(Band.popularity, alias="popularity_min")
).run()
)

await Band.select(
Min(Band.popularity).as_alias("popularity_min")
).run()
)

"""

Expand All @@ -128,17 +128,17 @@ class Max(Function):

await Band.select(
Max(Band.popularity)
).run()
)

# We can use an alias. These two are equivalent:

await Band.select(
Max(Band.popularity, alias="popularity_max")
).run()
)

await Band.select(
Max(Band.popularity).as_alias("popularity_max")
).run()
)

"""

Expand All @@ -153,17 +153,17 @@ class Sum(Function):

await Band.select(
Sum(Band.popularity)
).run()
)

# We can use an alias. These two are equivalent:

await Band.select(
Sum(Band.popularity, alias="popularity_sum")
).run()
)

await Band.select(
Sum(Band.popularity).as_alias("popularity_sum")
).run()
)

"""

Expand Down
82 changes: 82 additions & 0 deletions piccolo/query/functions/type_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import typing as t

from piccolo.columns.base import Column
from piccolo.querystring import QueryString


class Cast(QueryString):
def __init__(
self,
identifier: t.Union[Column, QueryString],
as_type: Column,
alias: t.Optional[str] = None,
):
"""
Cast a value to a different type. For example::

>>> from piccolo.query.functions import Cast

>>> await Concert.select(
... Cast(Concert.starts, Time(), "start_time")
... )
[{"start_time": datetime.time(19, 0)}]

:param identifier:
Identifies what is being converted (e.g. a column).
:param as_type:
The type to be converted to.

"""
# Make sure the identifier is a supported type.

if not isinstance(identifier, (Column, QueryString)):
raise ValueError(
"The identifier is an unsupported type - only Column and "
"QueryString instances are allowed."
)

#######################################################################
# Convert `as_type` to a string which can be used in the query.

if not isinstance(as_type, Column):
raise ValueError("The `as_type` value must be a Column instance.")

# We need to give the column a reference to a table, and hence
# the database engine, as the column type is sometimes dependent
# on which database is being used.
from piccolo.table import Table, create_table_class

table: t.Optional[t.Type[Table]] = None

if isinstance(identifier, Column):
table = identifier._meta.table
elif isinstance(identifier, QueryString):
table = (
identifier.columns[0]._meta.table
if identifier.columns
else None
)

as_type._meta.table = table or create_table_class("Table")
as_type_string = as_type.column_type

#######################################################################
# Preserve the original alias from the column.

if isinstance(identifier, Column):
alias = (
alias
or identifier._alias
or identifier._meta.get_default_alias()
)

#######################################################################

super().__init__(
f"CAST({{}} AS {as_type_string})",
identifier,
alias=alias,
)


__all__ = ("Cast",)
12 changes: 12 additions & 0 deletions piccolo/querystring.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ def __add__(self, value) -> QueryString:
def __sub__(self, value) -> QueryString:
return QueryString("{} - {}", self, value)

def __gt__(self, value) -> QueryString:
return QueryString("{} > {}", self, value)

def __ge__(self, value) -> QueryString:
return QueryString("{} >= {}", self, value)

def __lt__(self, value) -> QueryString:
return QueryString("{} < {}", self, value)

def __le__(self, value) -> QueryString:
return QueryString("{} <= {}", self, value)

def is_in(self, value) -> QueryString:
return QueryString("{} IN {}", self, value)

Expand Down
Loading
Loading