Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/sqlalchemy/sqlfunc.py : 63%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/sqlalchemy/sqlfunc.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Functions to operate on SQL clauses for SQLAlchemy Core.**
27"""
29from typing import NoReturn, TYPE_CHECKING
31from sqlalchemy.ext.compiler import compiles
32# noinspection PyProtectedMember
33from sqlalchemy.sql.expression import FunctionElement
34from sqlalchemy.sql.sqltypes import Numeric
36from cardinal_pythonlib.sqlalchemy.dialect import SqlaDialectName
37from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
39if TYPE_CHECKING:
40 from sqlalchemy.sql.elements import ClauseElement, ClauseList
41 from sqlalchemy.sql.compiler import SQLCompiler
43log = get_brace_style_log_with_null_handler(__name__)
46# =============================================================================
47# Support functions
48# =============================================================================
50def fail_unknown_dialect(compiler: "SQLCompiler", task: str) -> NoReturn:
51 """
52 Raise :exc:`NotImplementedError` in relation to a dialect for which a
53 function hasn't been implemented (with a helpful error message).
54 """
55 raise NotImplementedError(
56 f"Don't know how to {task} on dialect {compiler.dialect!r}. "
57 f"(Check also: if you printed the SQL before it was bound to an "
58 f"engine, you will be trying to use a dialect like StrSQLCompiler, "
59 f"which could be a reason for failure.)"
60 )
63def fetch_processed_single_clause(element: "ClauseElement",
64 compiler: "SQLCompiler") -> str:
65 """
66 Takes a clause element that must have a single clause, and converts it
67 to raw SQL text.
68 """
69 if len(element.clauses) != 1:
70 raise TypeError(f"Only one argument supported; "
71 f"{len(element.clauses)} were passed")
72 clauselist = element.clauses # type: ClauseList
73 first = clauselist.get_children()[0]
74 return compiler.process(first)
77# =============================================================================
78# Extract year from a DATE/DATETIME etc.
79# =============================================================================
81# noinspection PyPep8Naming
82class extract_year(FunctionElement):
83 """
84 Implements an SQLAlchemy :func:`extract_year` function, to extract the
85 year from a date/datetime column.
87 ``YEAR``, or ``func.year()``, is specific to some DBs, e.g. MySQL.
88 So is ``EXTRACT``, or ``func.extract()``;
89 http://modern-sql.com/feature/extract.
91 This function therefore implements an ``extract_year`` function across
92 multiple databases.
94 Use this as:
96 .. code-block:: python
98 from cardinal_pythonlib.sqlalchemy.sqlfunc import extract_year
100 ... then use :func:`extract_year` in an SQLAlchemy ``SELECT`` expression.
102 Here's an example from CamCOPS:
104 .. code-block:: python
106 select_fields = [
107 literal(cls.__tablename__).label("task"),
108 extract_year(cls._when_added_batch_utc).label("year"),
109 extract_month(cls._when_added_batch_utc).label("month"),
110 func.count().label("num_tasks_added"),
111 ]
113 """
114 type = Numeric()
115 name = 'extract_year'
118# noinspection PyUnusedLocal
119@compiles(extract_year)
120def extract_year_default(element: "ClauseElement",
121 compiler: "SQLCompiler", **kw) -> NoReturn:
122 """
123 Default implementation of :func:, which raises :exc:`NotImplementedError`.
124 """
125 fail_unknown_dialect(compiler, "extract year from date")
128# noinspection PyUnusedLocal
129@compiles(extract_year, SqlaDialectName.SQLSERVER)
130@compiles(extract_year, SqlaDialectName.MYSQL)
131def extract_year_year(element: "ClauseElement",
132 compiler: "SQLCompiler", **kw) -> str:
133 # https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_year # noqa
134 # https://docs.microsoft.com/en-us/sql/t-sql/functions/year-transact-sql
135 clause = fetch_processed_single_clause(element, compiler)
136 return f"YEAR({clause})"
139# noinspection PyUnusedLocal
140@compiles(extract_year, SqlaDialectName.ORACLE)
141@compiles(extract_year, SqlaDialectName.POSTGRES)
142def extract_year_extract(element: "ClauseElement",
143 compiler: "SQLCompiler", **kw) -> str:
144 # https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
145 clause = fetch_processed_single_clause(element, compiler)
146 return f"EXTRACT(YEAR FROM {clause})"
149# noinspection PyUnusedLocal
150@compiles(extract_year, SqlaDialectName.SQLITE)
151def extract_year_strftime(element: "ClauseElement",
152 compiler: "SQLCompiler", **kw) -> str:
153 # https://sqlite.org/lang_datefunc.html
154 clause = fetch_processed_single_clause(element, compiler)
155 return f"STRFTIME('%Y', {clause})"
158# =============================================================================
159# Extract month from a DATE/DATETIME etc.
160# =============================================================================
162# noinspection PyPep8Naming
163class extract_month(FunctionElement):
164 """
165 Implements an SQLAlchemy :func:`extract_month` function. See
166 :func:`extract_year`.
167 """
168 type = Numeric()
169 name = 'extract_month'
172# noinspection PyUnusedLocal
173@compiles(extract_month)
174def extract_month_default(element: "ClauseElement",
175 compiler: "SQLCompiler", **kw) -> NoReturn:
176 fail_unknown_dialect(compiler, "extract month from date")
179# noinspection PyUnusedLocal
180@compiles(extract_month, SqlaDialectName.SQLSERVER)
181@compiles(extract_month, SqlaDialectName.MYSQL)
182def extract_month_month(element: "ClauseElement",
183 compiler: "SQLCompiler", **kw) -> str:
184 clause = fetch_processed_single_clause(element, compiler)
185 return f"MONTH({clause})"
188# noinspection PyUnusedLocal
189@compiles(extract_month, SqlaDialectName.ORACLE)
190@compiles(extract_year, SqlaDialectName.POSTGRES)
191def extract_month_extract(element: "ClauseElement",
192 compiler: "SQLCompiler", **kw) -> str:
193 clause = fetch_processed_single_clause(element, compiler)
194 return f"EXTRACT(MONTH FROM {clause})"
197# noinspection PyUnusedLocal
198@compiles(extract_month, SqlaDialectName.SQLITE)
199def extract_month_strftime(element: "ClauseElement",
200 compiler: "SQLCompiler", **kw) -> str:
201 clause = fetch_processed_single_clause(element, compiler)
202 return f"STRFTIME('%m', {clause})"
205# =============================================================================
206# Extract day (day of month, not day of week) from a DATE/DATETIME etc.
207# =============================================================================
209# noinspection PyPep8Naming
210class extract_day_of_month(FunctionElement):
211 """
212 Implements an SQLAlchemy :func:`extract_day` function (to extract the day
213 of the month from a date/datetime expression). See :func:`extract_year`.
214 """
215 type = Numeric()
216 name = 'extract_day'
219# noinspection PyUnusedLocal
220@compiles(extract_day_of_month)
221def extract_day_of_month_default(element: "ClauseElement",
222 compiler: "SQLCompiler", **kw) -> NoReturn:
223 fail_unknown_dialect(compiler, "extract day-of-month from date")
226# noinspection PyUnusedLocal
227@compiles(extract_day_of_month, SqlaDialectName.SQLSERVER)
228@compiles(extract_day_of_month, SqlaDialectName.MYSQL)
229def extract_day_of_month_day(element: "ClauseElement",
230 compiler: "SQLCompiler", **kw) -> str:
231 clause = fetch_processed_single_clause(element, compiler)
232 return f"DAY({clause})"
235# noinspection PyUnusedLocal
236@compiles(extract_day_of_month, SqlaDialectName.ORACLE)
237@compiles(extract_year, SqlaDialectName.POSTGRES)
238def extract_day_of_month_extract(element: "ClauseElement",
239 compiler: "SQLCompiler", **kw) -> str:
240 clause = fetch_processed_single_clause(element, compiler)
241 return f"EXTRACT(DAY FROM {clause})"
244# noinspection PyUnusedLocal
245@compiles(extract_day_of_month, SqlaDialectName.SQLITE)
246def extract_day_of_month_strftime(element: "ClauseElement",
247 compiler: "SQLCompiler", **kw) -> str:
248 clause = fetch_processed_single_clause(element, compiler)
249 return f"STRFTIME('%d', {clause})"