Skip to content

Commit

Permalink
1010 Add support for CAST to convert between types (#1011)
Browse files Browse the repository at this point in the history
* add `cast` function

* fix typo

* revert accidental `test_array.py` changes

they weren't meant to be in this branch
  • Loading branch information
dantownsend authored Jun 7, 2024
1 parent acaa750 commit 9d1ab2d
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 14 deletions.
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

0 comments on commit 9d1ab2d

Please sign in to comment.