Hide keyboard shortcuts

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 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

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 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

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. 

22 

23=============================================================================== 

24 

25**Functions to operate on SQL clauses for SQLAlchemy Core.** 

26 

27""" 

28 

29from typing import NoReturn, TYPE_CHECKING 

30 

31from sqlalchemy.ext.compiler import compiles 

32# noinspection PyProtectedMember 

33from sqlalchemy.sql.expression import FunctionElement 

34from sqlalchemy.sql.sqltypes import Numeric 

35 

36from cardinal_pythonlib.sqlalchemy.dialect import SqlaDialectName 

37from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler 

38 

39if TYPE_CHECKING: 

40 from sqlalchemy.sql.elements import ClauseElement, ClauseList 

41 from sqlalchemy.sql.compiler import SQLCompiler 

42 

43log = get_brace_style_log_with_null_handler(__name__) 

44 

45 

46# ============================================================================= 

47# Support functions 

48# ============================================================================= 

49 

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 ) 

61 

62 

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) 

75 

76 

77# ============================================================================= 

78# Extract year from a DATE/DATETIME etc. 

79# ============================================================================= 

80 

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. 

86 

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. 

90 

91 This function therefore implements an ``extract_year`` function across 

92 multiple databases. 

93 

94 Use this as: 

95 

96 .. code-block:: python 

97 

98 from cardinal_pythonlib.sqlalchemy.sqlfunc import extract_year 

99 

100 ... then use :func:`extract_year` in an SQLAlchemy ``SELECT`` expression. 

101 

102 Here's an example from CamCOPS: 

103 

104 .. code-block:: python 

105 

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 ] 

112 

113 """ 

114 type = Numeric() 

115 name = 'extract_year' 

116 

117 

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") 

126 

127 

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})" 

137 

138 

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})" 

147 

148 

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})" 

156 

157 

158# ============================================================================= 

159# Extract month from a DATE/DATETIME etc. 

160# ============================================================================= 

161 

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' 

170 

171 

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") 

177 

178 

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})" 

186 

187 

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})" 

195 

196 

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})" 

203 

204 

205# ============================================================================= 

206# Extract day (day of month, not day of week) from a DATE/DATETIME etc. 

207# ============================================================================= 

208 

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' 

217 

218 

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") 

224 

225 

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})" 

233 

234 

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})" 

242 

243 

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})"