Skip to content

Commit

Permalink
add basic math functions (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
dantownsend authored Jun 13, 2024
1 parent cfb674a commit fb7a5ed
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 238 deletions.
1 change: 1 addition & 0 deletions docs/src/piccolo/functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Functions can be used to modify how queries are run, and what is returned.

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

.. currentmodule:: piccolo.query.functions.math

Abs
---

.. autoclass:: Abs
:class-doc-from: class

Ceil
----

.. autoclass:: Ceil
:class-doc-from: class

Floor
-----

.. autoclass:: Floor
:class-doc-from: class

Round
-----

.. autoclass:: Round
:class-doc-from: class
5 changes: 5 additions & 0 deletions piccolo/query/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from .aggregate import Avg, Count, Max, Min, Sum
from .math import Abs, Ceil, Floor, Round
from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
from .type_conversion import Cast

__all__ = (
"Abs",
"Avg",
"Cast",
"Ceil",
"Count",
"Floor",
"Length",
"Lower",
"Ltrim",
"Max",
"Min",
"Reverse",
"Round",
"Rtrim",
"Sum",
"Upper",
Expand Down
48 changes: 48 additions & 0 deletions piccolo/query/functions/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
These functions mirror their counterparts in the Postgresql docs:
https://www.postgresql.org/docs/current/functions-math.html
"""

from .base import Function


class Abs(Function):
"""
Absolute value.
"""

function_name = "ABS"


class Ceil(Function):
"""
Nearest integer greater than or equal to argument.
"""

function_name = "CEIL"


class Floor(Function):
"""
Nearest integer less than or equal to argument.
"""

function_name = "FLOOR"


class Round(Function):
"""
Rounds to nearest integer.
"""

function_name = "ROUND"


__all__ = (
"Abs",
"Ceil",
"Floor",
"Round",
)
Empty file.
34 changes: 34 additions & 0 deletions tests/query/functions/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import typing as t
from unittest import TestCase

from piccolo.table import Table, create_db_tables_sync, drop_db_tables_sync
from tests.example_apps.music.tables import Band, Manager


class FunctionTest(TestCase):
tables: t.List[t.Type[Table]]

def setUp(self) -> None:
create_db_tables_sync(*self.tables)

def tearDown(self) -> None:
drop_db_tables_sync(*self.tables)


class BandTest(FunctionTest):
tables = [Band, Manager]

def setUp(self) -> None:
super().setUp()

manager = Manager({Manager.name: "Guido"})
manager.save().run_sync()

band = Band(
{
Band.name: "Pythonistas",
Band.manager: manager,
Band.popularity: 1000,
}
)
band.save().run_sync()
64 changes: 64 additions & 0 deletions tests/query/functions/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from piccolo.query.functions import Reverse, Upper
from piccolo.querystring import QueryString
from tests.base import engines_skip
from tests.example_apps.music.tables import Band

from .base import BandTest


@engines_skip("sqlite")
class TestNested(BandTest):
"""
Skip the the test for SQLite, as it doesn't support ``Reverse``.
"""

def test_nested(self):
"""
Make sure we can nest functions.
"""
response = Band.select(Upper(Reverse(Band.name))).run_sync()
self.assertListEqual(response, [{"upper": "SATSINOHTYP"}])

def test_nested_with_joined_column(self):
"""
Make sure nested functions can be used on a column from a joined table.
"""
response = Band.select(Upper(Reverse(Band.manager._.name))).run_sync()
self.assertListEqual(response, [{"upper": "ODIUG"}])

def test_nested_within_querystring(self):
"""
If we wrap a function in a custom QueryString - make sure the columns
are still accessible, so joins are successful.
"""
response = Band.select(
QueryString("CONCAT({}, '!')", Upper(Band.manager._.name)),
).run_sync()

self.assertListEqual(response, [{"concat": "GUIDO!"}])


class TestWhereClause(BandTest):

def test_where(self):
"""
Make sure where clauses work with functions.
"""
response = (
Band.select(Band.name)
.where(Upper(Band.name) == "PYTHONISTAS")
.run_sync()
)
self.assertListEqual(response, [{"name": "Pythonistas"}])

def test_where_with_joined_column(self):
"""
Make sure where clauses work with functions, when a joined column is
used.
"""
response = (
Band.select(Band.name)
.where(Upper(Band.manager._.name) == "GUIDO")
.run_sync()
)
self.assertListEqual(response, [{"name": "Pythonistas"}])
39 changes: 39 additions & 0 deletions tests/query/functions/test_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import decimal

from piccolo.columns import Numeric
from piccolo.query.functions.math import Abs, Ceil, Floor, Round
from piccolo.table import Table

from .base import FunctionTest


class Ticket(Table):
price = Numeric(digits=(5, 2))


class TestMath(FunctionTest):

tables = [Ticket]

def setUp(self):
super().setUp()
self.ticket = Ticket({Ticket.price: decimal.Decimal("36.50")})
self.ticket.save().run_sync()

def test_floor(self):
response = Ticket.select(Floor(Ticket.price, alias="price")).run_sync()
self.assertListEqual(response, [{"price": decimal.Decimal("36.00")}])

def test_ceil(self):
response = Ticket.select(Ceil(Ticket.price, alias="price")).run_sync()
self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])

def test_abs(self):
self.ticket.price = decimal.Decimal("-1.50")
self.ticket.save().run_sync()
response = Ticket.select(Abs(Ticket.price, alias="price")).run_sync()
self.assertListEqual(response, [{"price": decimal.Decimal("1.50")}])

def test_round(self):
response = Ticket.select(Round(Ticket.price, alias="price")).run_sync()
self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}])
25 changes: 25 additions & 0 deletions tests/query/functions/test_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from piccolo.query.functions.string import Upper
from tests.example_apps.music.tables import Band

from .base import BandTest


class TestUpperFunction(BandTest):

def test_column(self):
"""
Make sure we can uppercase a column's value.
"""
response = Band.select(Upper(Band.name)).run_sync()
self.assertListEqual(response, [{"upper": "PYTHONISTAS"}])

def test_alias(self):
response = Band.select(Upper(Band.name, alias="name")).run_sync()
self.assertListEqual(response, [{"name": "PYTHONISTAS"}])

def test_joined_column(self):
"""
Make sure we can uppercase a column's value from a joined table.
"""
response = Band.select(Upper(Band.manager._.name)).run_sync()
self.assertListEqual(response, [{"upper": "GUIDO"}])
Loading

0 comments on commit fb7a5ed

Please sign in to comment.