Skip to content

Commit

Permalink
856 Be able to extract parts of timestamps / dates (#1023)
Browse files Browse the repository at this point in the history
* added extract function

* add `Strftime` and database agnostic functions

* reduce repetition

* move alias to outer

* add tests

* add tests for `Extract` and `Strftime`
  • Loading branch information
dantownsend authored Jun 14, 2024
1 parent 2e40e68 commit bf36f50
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 2 deletions.
67 changes: 67 additions & 0 deletions docs/src/piccolo/functions/datetime.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Datetime functions
==================

.. currentmodule:: piccolo.query.functions.datetime

Postgres / Cockroach
--------------------

Extract
~~~~~~~

.. autoclass:: Extract


SQLite
------

Strftime
~~~~~~~~

.. autoclass:: Strftime


Database agnostic
-----------------

These convenience functions work consistently across database engines.

They all work very similarly, for example:

.. code-block:: python
>>> from piccolo.query.functions import Year
>>> await Concert.select(
... Year(Concert.starts, alias="start_year")
... )
[{"start_year": 2024}]
Year
~~~~

.. autofunction:: Year

Month
~~~~~

.. autofunction:: Month

Day
~~~

.. autofunction:: Day

Hour
~~~~

.. autofunction:: Hour

Minute
~~~~~~

.. autofunction:: Minute

Second
~~~~~~

.. autofunction:: Second
1 change: 1 addition & 0 deletions docs/src/piccolo/functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ Functions can be used to modify how queries are run, and what is returned.
./basic_usage
./string
./math
./datetime
./type_conversion
./aggregate
9 changes: 9 additions & 0 deletions piccolo/query/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .aggregate import Avg, Count, Max, Min, Sum
from .datetime import Day, Extract, Hour, Month, Second, Strftime, Year
from .math import Abs, Ceil, Floor, Round
from .string import Length, Lower, Ltrim, Reverse, Rtrim, Upper
from .type_conversion import Cast
Expand All @@ -9,15 +10,23 @@
"Cast",
"Ceil",
"Count",
"Day",
"Extract",
"Extract",
"Floor",
"Hour",
"Length",
"Lower",
"Ltrim",
"Max",
"Min",
"Month",
"Reverse",
"Round",
"Rtrim",
"Second",
"Strftime",
"Sum",
"Upper",
"Year",
)
260 changes: 260 additions & 0 deletions piccolo/query/functions/datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import typing as t

from piccolo.columns.base import Column
from piccolo.columns.column_types import (
Date,
Integer,
Time,
Timestamp,
Timestamptz,
)
from piccolo.querystring import QueryString

from .type_conversion import Cast

###############################################################################
# Postgres / Cockroach

ExtractComponent = t.Literal[
"century",
"day",
"decade",
"dow",
"doy",
"epoch",
"hour",
"isodow",
"isoyear",
"julian",
"microseconds",
"millennium",
"milliseconds",
"minute",
"month",
"quarter",
"second",
"timezone",
"timezone_hour",
"timezone_minute",
"week",
"year",
]


class Extract(QueryString):
def __init__(
self,
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
datetime_component: ExtractComponent,
alias: t.Optional[str] = None,
):
"""
.. note:: This is for Postgres / Cockroach only.
Extract a date or time component from a ``Date`` / ``Time`` /
``Timestamp`` / ``Timestamptz`` column. For example, getting the month
from a timestamp:
.. code-block:: python
>>> from piccolo.query.functions import Extract
>>> await Concert.select(
... Extract(Concert.starts, "month", alias="start_month")
... )
[{"start_month": 12}]
:param identifier:
Identifies the column.
:param datetime_component:
The date or time component to extract from the column.
"""
if datetime_component.lower() not in t.get_args(ExtractComponent):
raise ValueError("The date time component isn't recognised.")

super().__init__(
f"EXTRACT({datetime_component} FROM {{}})",
identifier,
alias=alias,
)


###############################################################################
# SQLite


class Strftime(QueryString):
def __init__(
self,
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
datetime_format: str,
alias: t.Optional[str] = None,
):
"""
.. note:: This is for SQLite only.
Format a datetime value. For example:
.. code-block:: python
>>> from piccolo.query.functions import Strftime
>>> await Concert.select(
... Strftime(Concert.starts, "%Y", alias="start_year")
... )
[{"start_month": "2024"}]
:param identifier:
Identifies the column.
:param datetime_format:
A string describing the output format (see SQLite's
`documentation <https://www.sqlite.org/lang_datefunc.html>`_
for more info).
"""
super().__init__(
f"strftime('{datetime_format}', {{}})",
identifier,
alias=alias,
)


###############################################################################
# Database agnostic


def _get_engine_type(identifier: t.Union[Column, QueryString]) -> str:
if isinstance(identifier, Column):
return identifier._meta.engine_type
elif isinstance(identifier, QueryString) and (
columns := identifier.columns
):
return columns[0]._meta.engine_type
else:
raise ValueError("Unable to determine the engine type")


def _extract_component(
identifier: t.Union[Date, Time, Timestamp, Timestamptz, QueryString],
sqlite_format: str,
postgres_format: ExtractComponent,
alias: t.Optional[str],
):
engine_type = _get_engine_type(identifier=identifier)

return Cast(
(
Strftime(
identifier=identifier,
datetime_format=sqlite_format,
)
if engine_type == "sqlite"
else Extract(
identifier=identifier,
datetime_component=postgres_format,
)
),
Integer(),
alias=alias,
)


def Year(
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the year as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%Y",
postgres_format="year",
alias=alias,
)


def Month(
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the month as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%m",
postgres_format="month",
alias=alias,
)


def Day(
identifier: t.Union[Date, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the day as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%d",
postgres_format="day",
alias=alias,
)


def Hour(
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the hour as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%H",
postgres_format="hour",
alias=alias,
)


def Minute(
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the minute as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%M",
postgres_format="minute",
alias=alias,
)


def Second(
identifier: t.Union[Time, Timestamp, Timestamptz, QueryString],
alias: t.Optional[str] = None,
):
"""
Extract the second as an integer.
"""
return _extract_component(
identifier=identifier,
sqlite_format="%S",
postgres_format="second",
alias=alias,
)


__all__ = (
"Extract",
"Strftime",
"Year",
"Month",
"Day",
"Hour",
"Minute",
"Second",
)
4 changes: 2 additions & 2 deletions requirements/doc-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Sphinx==5.1.1
piccolo-theme>=0.12.0
Sphinx==7.3.7
piccolo-theme==0.22.0
sphinx-autobuild==2021.3.14
Loading

0 comments on commit bf36f50

Please sign in to comment.