Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lookups.py 24KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. import itertools
  2. import math
  3. from django.core.exceptions import EmptyResultSet
  4. from django.db.models.expressions import Case, Expression, Func, Value, When
  5. from django.db.models.fields import (
  6. BooleanField,
  7. CharField,
  8. DateTimeField,
  9. Field,
  10. IntegerField,
  11. UUIDField,
  12. )
  13. from django.db.models.query_utils import RegisterLookupMixin
  14. from django.utils.datastructures import OrderedSet
  15. from django.utils.functional import cached_property
  16. from django.utils.hashable import make_hashable
  17. class Lookup(Expression):
  18. lookup_name = None
  19. prepare_rhs = True
  20. can_use_none_as_rhs = False
  21. def __init__(self, lhs, rhs):
  22. self.lhs, self.rhs = lhs, rhs
  23. self.rhs = self.get_prep_lookup()
  24. self.lhs = self.get_prep_lhs()
  25. if hasattr(self.lhs, "get_bilateral_transforms"):
  26. bilateral_transforms = self.lhs.get_bilateral_transforms()
  27. else:
  28. bilateral_transforms = []
  29. if bilateral_transforms:
  30. # Warn the user as soon as possible if they are trying to apply
  31. # a bilateral transformation on a nested QuerySet: that won't work.
  32. from django.db.models.sql.query import Query # avoid circular import
  33. if isinstance(rhs, Query):
  34. raise NotImplementedError(
  35. "Bilateral transformations on nested querysets are not implemented."
  36. )
  37. self.bilateral_transforms = bilateral_transforms
  38. def apply_bilateral_transforms(self, value):
  39. for transform in self.bilateral_transforms:
  40. value = transform(value)
  41. return value
  42. def __repr__(self):
  43. return f"{self.__class__.__name__}({self.lhs!r}, {self.rhs!r})"
  44. def batch_process_rhs(self, compiler, connection, rhs=None):
  45. if rhs is None:
  46. rhs = self.rhs
  47. if self.bilateral_transforms:
  48. sqls, sqls_params = [], []
  49. for p in rhs:
  50. value = Value(p, output_field=self.lhs.output_field)
  51. value = self.apply_bilateral_transforms(value)
  52. value = value.resolve_expression(compiler.query)
  53. sql, sql_params = compiler.compile(value)
  54. sqls.append(sql)
  55. sqls_params.extend(sql_params)
  56. else:
  57. _, params = self.get_db_prep_lookup(rhs, connection)
  58. sqls, sqls_params = ["%s"] * len(params), params
  59. return sqls, sqls_params
  60. def get_source_expressions(self):
  61. if self.rhs_is_direct_value():
  62. return [self.lhs]
  63. return [self.lhs, self.rhs]
  64. def set_source_expressions(self, new_exprs):
  65. if len(new_exprs) == 1:
  66. self.lhs = new_exprs[0]
  67. else:
  68. self.lhs, self.rhs = new_exprs
  69. def get_prep_lookup(self):
  70. if not self.prepare_rhs or hasattr(self.rhs, "resolve_expression"):
  71. return self.rhs
  72. if hasattr(self.lhs, "output_field"):
  73. if hasattr(self.lhs.output_field, "get_prep_value"):
  74. return self.lhs.output_field.get_prep_value(self.rhs)
  75. elif self.rhs_is_direct_value():
  76. return Value(self.rhs)
  77. return self.rhs
  78. def get_prep_lhs(self):
  79. if hasattr(self.lhs, "resolve_expression"):
  80. return self.lhs
  81. return Value(self.lhs)
  82. def get_db_prep_lookup(self, value, connection):
  83. return ("%s", [value])
  84. def process_lhs(self, compiler, connection, lhs=None):
  85. lhs = lhs or self.lhs
  86. if hasattr(lhs, "resolve_expression"):
  87. lhs = lhs.resolve_expression(compiler.query)
  88. sql, params = compiler.compile(lhs)
  89. if isinstance(lhs, Lookup):
  90. # Wrapped in parentheses to respect operator precedence.
  91. sql = f"({sql})"
  92. return sql, params
  93. def process_rhs(self, compiler, connection):
  94. value = self.rhs
  95. if self.bilateral_transforms:
  96. if self.rhs_is_direct_value():
  97. # Do not call get_db_prep_lookup here as the value will be
  98. # transformed before being used for lookup
  99. value = Value(value, output_field=self.lhs.output_field)
  100. value = self.apply_bilateral_transforms(value)
  101. value = value.resolve_expression(compiler.query)
  102. if hasattr(value, "as_sql"):
  103. sql, params = compiler.compile(value)
  104. # Ensure expression is wrapped in parentheses to respect operator
  105. # precedence but avoid double wrapping as it can be misinterpreted
  106. # on some backends (e.g. subqueries on SQLite).
  107. if sql and sql[0] != "(":
  108. sql = "(%s)" % sql
  109. return sql, params
  110. else:
  111. return self.get_db_prep_lookup(value, connection)
  112. def rhs_is_direct_value(self):
  113. return not hasattr(self.rhs, "as_sql")
  114. def get_group_by_cols(self, alias=None):
  115. cols = []
  116. for source in self.get_source_expressions():
  117. cols.extend(source.get_group_by_cols())
  118. return cols
  119. def as_oracle(self, compiler, connection):
  120. # Oracle doesn't allow EXISTS() and filters to be compared to another
  121. # expression unless they're wrapped in a CASE WHEN.
  122. wrapped = False
  123. exprs = []
  124. for expr in (self.lhs, self.rhs):
  125. if connection.ops.conditional_expression_supported_in_where_clause(expr):
  126. expr = Case(When(expr, then=True), default=False)
  127. wrapped = True
  128. exprs.append(expr)
  129. lookup = type(self)(*exprs) if wrapped else self
  130. return lookup.as_sql(compiler, connection)
  131. @cached_property
  132. def output_field(self):
  133. return BooleanField()
  134. @property
  135. def identity(self):
  136. return self.__class__, self.lhs, self.rhs
  137. def __eq__(self, other):
  138. if not isinstance(other, Lookup):
  139. return NotImplemented
  140. return self.identity == other.identity
  141. def __hash__(self):
  142. return hash(make_hashable(self.identity))
  143. def resolve_expression(
  144. self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
  145. ):
  146. c = self.copy()
  147. c.is_summary = summarize
  148. c.lhs = self.lhs.resolve_expression(
  149. query, allow_joins, reuse, summarize, for_save
  150. )
  151. if hasattr(self.rhs, "resolve_expression"):
  152. c.rhs = self.rhs.resolve_expression(
  153. query, allow_joins, reuse, summarize, for_save
  154. )
  155. return c
  156. def select_format(self, compiler, sql, params):
  157. # Wrap filters with a CASE WHEN expression if a database backend
  158. # (e.g. Oracle) doesn't support boolean expression in SELECT or GROUP
  159. # BY list.
  160. if not compiler.connection.features.supports_boolean_expr_in_select_clause:
  161. sql = f"CASE WHEN {sql} THEN 1 ELSE 0 END"
  162. return sql, params
  163. class Transform(RegisterLookupMixin, Func):
  164. """
  165. RegisterLookupMixin() is first so that get_lookup() and get_transform()
  166. first examine self and then check output_field.
  167. """
  168. bilateral = False
  169. arity = 1
  170. @property
  171. def lhs(self):
  172. return self.get_source_expressions()[0]
  173. def get_bilateral_transforms(self):
  174. if hasattr(self.lhs, "get_bilateral_transforms"):
  175. bilateral_transforms = self.lhs.get_bilateral_transforms()
  176. else:
  177. bilateral_transforms = []
  178. if self.bilateral:
  179. bilateral_transforms.append(self.__class__)
  180. return bilateral_transforms
  181. class BuiltinLookup(Lookup):
  182. def process_lhs(self, compiler, connection, lhs=None):
  183. lhs_sql, params = super().process_lhs(compiler, connection, lhs)
  184. field_internal_type = self.lhs.output_field.get_internal_type()
  185. db_type = self.lhs.output_field.db_type(connection=connection)
  186. lhs_sql = connection.ops.field_cast_sql(db_type, field_internal_type) % lhs_sql
  187. lhs_sql = (
  188. connection.ops.lookup_cast(self.lookup_name, field_internal_type) % lhs_sql
  189. )
  190. return lhs_sql, list(params)
  191. def as_sql(self, compiler, connection):
  192. lhs_sql, params = self.process_lhs(compiler, connection)
  193. rhs_sql, rhs_params = self.process_rhs(compiler, connection)
  194. params.extend(rhs_params)
  195. rhs_sql = self.get_rhs_op(connection, rhs_sql)
  196. return "%s %s" % (lhs_sql, rhs_sql), params
  197. def get_rhs_op(self, connection, rhs):
  198. return connection.operators[self.lookup_name] % rhs
  199. class FieldGetDbPrepValueMixin:
  200. """
  201. Some lookups require Field.get_db_prep_value() to be called on their
  202. inputs.
  203. """
  204. get_db_prep_lookup_value_is_iterable = False
  205. def get_db_prep_lookup(self, value, connection):
  206. # For relational fields, use the 'target_field' attribute of the
  207. # output_field.
  208. field = getattr(self.lhs.output_field, "target_field", None)
  209. get_db_prep_value = (
  210. getattr(field, "get_db_prep_value", None)
  211. or self.lhs.output_field.get_db_prep_value
  212. )
  213. return (
  214. "%s",
  215. [get_db_prep_value(v, connection, prepared=True) for v in value]
  216. if self.get_db_prep_lookup_value_is_iterable
  217. else [get_db_prep_value(value, connection, prepared=True)],
  218. )
  219. class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
  220. """
  221. Some lookups require Field.get_db_prep_value() to be called on each value
  222. in an iterable.
  223. """
  224. get_db_prep_lookup_value_is_iterable = True
  225. def get_prep_lookup(self):
  226. if hasattr(self.rhs, "resolve_expression"):
  227. return self.rhs
  228. prepared_values = []
  229. for rhs_value in self.rhs:
  230. if hasattr(rhs_value, "resolve_expression"):
  231. # An expression will be handled by the database but can coexist
  232. # alongside real values.
  233. pass
  234. elif self.prepare_rhs and hasattr(self.lhs.output_field, "get_prep_value"):
  235. rhs_value = self.lhs.output_field.get_prep_value(rhs_value)
  236. prepared_values.append(rhs_value)
  237. return prepared_values
  238. def process_rhs(self, compiler, connection):
  239. if self.rhs_is_direct_value():
  240. # rhs should be an iterable of values. Use batch_process_rhs()
  241. # to prepare/transform those values.
  242. return self.batch_process_rhs(compiler, connection)
  243. else:
  244. return super().process_rhs(compiler, connection)
  245. def resolve_expression_parameter(self, compiler, connection, sql, param):
  246. params = [param]
  247. if hasattr(param, "resolve_expression"):
  248. param = param.resolve_expression(compiler.query)
  249. if hasattr(param, "as_sql"):
  250. sql, params = compiler.compile(param)
  251. return sql, params
  252. def batch_process_rhs(self, compiler, connection, rhs=None):
  253. pre_processed = super().batch_process_rhs(compiler, connection, rhs)
  254. # The params list may contain expressions which compile to a
  255. # sql/param pair. Zip them to get sql and param pairs that refer to the
  256. # same argument and attempt to replace them with the result of
  257. # compiling the param step.
  258. sql, params = zip(
  259. *(
  260. self.resolve_expression_parameter(compiler, connection, sql, param)
  261. for sql, param in zip(*pre_processed)
  262. )
  263. )
  264. params = itertools.chain.from_iterable(params)
  265. return sql, tuple(params)
  266. class PostgresOperatorLookup(FieldGetDbPrepValueMixin, Lookup):
  267. """Lookup defined by operators on PostgreSQL."""
  268. postgres_operator = None
  269. def as_postgresql(self, compiler, connection):
  270. lhs, lhs_params = self.process_lhs(compiler, connection)
  271. rhs, rhs_params = self.process_rhs(compiler, connection)
  272. params = tuple(lhs_params) + tuple(rhs_params)
  273. return "%s %s %s" % (lhs, self.postgres_operator, rhs), params
  274. @Field.register_lookup
  275. class Exact(FieldGetDbPrepValueMixin, BuiltinLookup):
  276. lookup_name = "exact"
  277. def get_prep_lookup(self):
  278. from django.db.models.sql.query import Query # avoid circular import
  279. if isinstance(self.rhs, Query):
  280. if self.rhs.has_limit_one():
  281. if not self.rhs.has_select_fields:
  282. self.rhs.clear_select_clause()
  283. self.rhs.add_fields(["pk"])
  284. else:
  285. raise ValueError(
  286. "The QuerySet value for an exact lookup must be limited to "
  287. "one result using slicing."
  288. )
  289. return super().get_prep_lookup()
  290. def as_sql(self, compiler, connection):
  291. # Avoid comparison against direct rhs if lhs is a boolean value. That
  292. # turns "boolfield__exact=True" into "WHERE boolean_field" instead of
  293. # "WHERE boolean_field = True" when allowed.
  294. if (
  295. isinstance(self.rhs, bool)
  296. and getattr(self.lhs, "conditional", False)
  297. and connection.ops.conditional_expression_supported_in_where_clause(
  298. self.lhs
  299. )
  300. ):
  301. lhs_sql, params = self.process_lhs(compiler, connection)
  302. template = "%s" if self.rhs else "NOT %s"
  303. return template % lhs_sql, params
  304. return super().as_sql(compiler, connection)
  305. @Field.register_lookup
  306. class IExact(BuiltinLookup):
  307. lookup_name = "iexact"
  308. prepare_rhs = False
  309. def process_rhs(self, qn, connection):
  310. rhs, params = super().process_rhs(qn, connection)
  311. if params:
  312. params[0] = connection.ops.prep_for_iexact_query(params[0])
  313. return rhs, params
  314. @Field.register_lookup
  315. class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup):
  316. lookup_name = "gt"
  317. @Field.register_lookup
  318. class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup):
  319. lookup_name = "gte"
  320. @Field.register_lookup
  321. class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup):
  322. lookup_name = "lt"
  323. @Field.register_lookup
  324. class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup):
  325. lookup_name = "lte"
  326. class IntegerFieldFloatRounding:
  327. """
  328. Allow floats to work as query values for IntegerField. Without this, the
  329. decimal portion of the float would always be discarded.
  330. """
  331. def get_prep_lookup(self):
  332. if isinstance(self.rhs, float):
  333. self.rhs = math.ceil(self.rhs)
  334. return super().get_prep_lookup()
  335. @IntegerField.register_lookup
  336. class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual):
  337. pass
  338. @IntegerField.register_lookup
  339. class IntegerLessThan(IntegerFieldFloatRounding, LessThan):
  340. pass
  341. @Field.register_lookup
  342. class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
  343. lookup_name = "in"
  344. def get_prep_lookup(self):
  345. from django.db.models.sql.query import Query # avoid circular import
  346. if isinstance(self.rhs, Query):
  347. self.rhs.clear_ordering(clear_default=True)
  348. if not self.rhs.has_select_fields:
  349. self.rhs.clear_select_clause()
  350. self.rhs.add_fields(["pk"])
  351. return super().get_prep_lookup()
  352. def process_rhs(self, compiler, connection):
  353. db_rhs = getattr(self.rhs, "_db", None)
  354. if db_rhs is not None and db_rhs != connection.alias:
  355. raise ValueError(
  356. "Subqueries aren't allowed across different databases. Force "
  357. "the inner query to be evaluated using `list(inner_query)`."
  358. )
  359. if self.rhs_is_direct_value():
  360. # Remove None from the list as NULL is never equal to anything.
  361. try:
  362. rhs = OrderedSet(self.rhs)
  363. rhs.discard(None)
  364. except TypeError: # Unhashable items in self.rhs
  365. rhs = [r for r in self.rhs if r is not None]
  366. if not rhs:
  367. raise EmptyResultSet
  368. # rhs should be an iterable; use batch_process_rhs() to
  369. # prepare/transform those values.
  370. sqls, sqls_params = self.batch_process_rhs(compiler, connection, rhs)
  371. placeholder = "(" + ", ".join(sqls) + ")"
  372. return (placeholder, sqls_params)
  373. return super().process_rhs(compiler, connection)
  374. def get_rhs_op(self, connection, rhs):
  375. return "IN %s" % rhs
  376. def as_sql(self, compiler, connection):
  377. max_in_list_size = connection.ops.max_in_list_size()
  378. if (
  379. self.rhs_is_direct_value()
  380. and max_in_list_size
  381. and len(self.rhs) > max_in_list_size
  382. ):
  383. return self.split_parameter_list_as_sql(compiler, connection)
  384. return super().as_sql(compiler, connection)
  385. def split_parameter_list_as_sql(self, compiler, connection):
  386. # This is a special case for databases which limit the number of
  387. # elements which can appear in an 'IN' clause.
  388. max_in_list_size = connection.ops.max_in_list_size()
  389. lhs, lhs_params = self.process_lhs(compiler, connection)
  390. rhs, rhs_params = self.batch_process_rhs(compiler, connection)
  391. in_clause_elements = ["("]
  392. params = []
  393. for offset in range(0, len(rhs_params), max_in_list_size):
  394. if offset > 0:
  395. in_clause_elements.append(" OR ")
  396. in_clause_elements.append("%s IN (" % lhs)
  397. params.extend(lhs_params)
  398. sqls = rhs[offset : offset + max_in_list_size]
  399. sqls_params = rhs_params[offset : offset + max_in_list_size]
  400. param_group = ", ".join(sqls)
  401. in_clause_elements.append(param_group)
  402. in_clause_elements.append(")")
  403. params.extend(sqls_params)
  404. in_clause_elements.append(")")
  405. return "".join(in_clause_elements), params
  406. class PatternLookup(BuiltinLookup):
  407. param_pattern = "%%%s%%"
  408. prepare_rhs = False
  409. def get_rhs_op(self, connection, rhs):
  410. # Assume we are in startswith. We need to produce SQL like:
  411. # col LIKE %s, ['thevalue%']
  412. # For python values we can (and should) do that directly in Python,
  413. # but if the value is for example reference to other column, then
  414. # we need to add the % pattern match to the lookup by something like
  415. # col LIKE othercol || '%%'
  416. # So, for Python values we don't need any special pattern, but for
  417. # SQL reference values or SQL transformations we need the correct
  418. # pattern added.
  419. if hasattr(self.rhs, "as_sql") or self.bilateral_transforms:
  420. pattern = connection.pattern_ops[self.lookup_name].format(
  421. connection.pattern_esc
  422. )
  423. return pattern.format(rhs)
  424. else:
  425. return super().get_rhs_op(connection, rhs)
  426. def process_rhs(self, qn, connection):
  427. rhs, params = super().process_rhs(qn, connection)
  428. if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
  429. params[0] = self.param_pattern % connection.ops.prep_for_like_query(
  430. params[0]
  431. )
  432. return rhs, params
  433. @Field.register_lookup
  434. class Contains(PatternLookup):
  435. lookup_name = "contains"
  436. @Field.register_lookup
  437. class IContains(Contains):
  438. lookup_name = "icontains"
  439. @Field.register_lookup
  440. class StartsWith(PatternLookup):
  441. lookup_name = "startswith"
  442. param_pattern = "%s%%"
  443. @Field.register_lookup
  444. class IStartsWith(StartsWith):
  445. lookup_name = "istartswith"
  446. @Field.register_lookup
  447. class EndsWith(PatternLookup):
  448. lookup_name = "endswith"
  449. param_pattern = "%%%s"
  450. @Field.register_lookup
  451. class IEndsWith(EndsWith):
  452. lookup_name = "iendswith"
  453. @Field.register_lookup
  454. class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
  455. lookup_name = "range"
  456. def get_rhs_op(self, connection, rhs):
  457. return "BETWEEN %s AND %s" % (rhs[0], rhs[1])
  458. @Field.register_lookup
  459. class IsNull(BuiltinLookup):
  460. lookup_name = "isnull"
  461. prepare_rhs = False
  462. def as_sql(self, compiler, connection):
  463. if not isinstance(self.rhs, bool):
  464. raise ValueError(
  465. "The QuerySet value for an isnull lookup must be True or False."
  466. )
  467. sql, params = compiler.compile(self.lhs)
  468. if self.rhs:
  469. return "%s IS NULL" % sql, params
  470. else:
  471. return "%s IS NOT NULL" % sql, params
  472. @Field.register_lookup
  473. class Regex(BuiltinLookup):
  474. lookup_name = "regex"
  475. prepare_rhs = False
  476. def as_sql(self, compiler, connection):
  477. if self.lookup_name in connection.operators:
  478. return super().as_sql(compiler, connection)
  479. else:
  480. lhs, lhs_params = self.process_lhs(compiler, connection)
  481. rhs, rhs_params = self.process_rhs(compiler, connection)
  482. sql_template = connection.ops.regex_lookup(self.lookup_name)
  483. return sql_template % (lhs, rhs), lhs_params + rhs_params
  484. @Field.register_lookup
  485. class IRegex(Regex):
  486. lookup_name = "iregex"
  487. class YearLookup(Lookup):
  488. def year_lookup_bounds(self, connection, year):
  489. from django.db.models.functions import ExtractIsoYear
  490. iso_year = isinstance(self.lhs, ExtractIsoYear)
  491. output_field = self.lhs.lhs.output_field
  492. if isinstance(output_field, DateTimeField):
  493. bounds = connection.ops.year_lookup_bounds_for_datetime_field(
  494. year,
  495. iso_year=iso_year,
  496. )
  497. else:
  498. bounds = connection.ops.year_lookup_bounds_for_date_field(
  499. year,
  500. iso_year=iso_year,
  501. )
  502. return bounds
  503. def as_sql(self, compiler, connection):
  504. # Avoid the extract operation if the rhs is a direct value to allow
  505. # indexes to be used.
  506. if self.rhs_is_direct_value():
  507. # Skip the extract part by directly using the originating field,
  508. # that is self.lhs.lhs.
  509. lhs_sql, params = self.process_lhs(compiler, connection, self.lhs.lhs)
  510. rhs_sql, _ = self.process_rhs(compiler, connection)
  511. rhs_sql = self.get_direct_rhs_sql(connection, rhs_sql)
  512. start, finish = self.year_lookup_bounds(connection, self.rhs)
  513. params.extend(self.get_bound_params(start, finish))
  514. return "%s %s" % (lhs_sql, rhs_sql), params
  515. return super().as_sql(compiler, connection)
  516. def get_direct_rhs_sql(self, connection, rhs):
  517. return connection.operators[self.lookup_name] % rhs
  518. def get_bound_params(self, start, finish):
  519. raise NotImplementedError(
  520. "subclasses of YearLookup must provide a get_bound_params() method"
  521. )
  522. class YearExact(YearLookup, Exact):
  523. def get_direct_rhs_sql(self, connection, rhs):
  524. return "BETWEEN %s AND %s"
  525. def get_bound_params(self, start, finish):
  526. return (start, finish)
  527. class YearGt(YearLookup, GreaterThan):
  528. def get_bound_params(self, start, finish):
  529. return (finish,)
  530. class YearGte(YearLookup, GreaterThanOrEqual):
  531. def get_bound_params(self, start, finish):
  532. return (start,)
  533. class YearLt(YearLookup, LessThan):
  534. def get_bound_params(self, start, finish):
  535. return (start,)
  536. class YearLte(YearLookup, LessThanOrEqual):
  537. def get_bound_params(self, start, finish):
  538. return (finish,)
  539. class UUIDTextMixin:
  540. """
  541. Strip hyphens from a value when filtering a UUIDField on backends without
  542. a native datatype for UUID.
  543. """
  544. def process_rhs(self, qn, connection):
  545. if not connection.features.has_native_uuid_field:
  546. from django.db.models.functions import Replace
  547. if self.rhs_is_direct_value():
  548. self.rhs = Value(self.rhs)
  549. self.rhs = Replace(
  550. self.rhs, Value("-"), Value(""), output_field=CharField()
  551. )
  552. rhs, params = super().process_rhs(qn, connection)
  553. return rhs, params
  554. @UUIDField.register_lookup
  555. class UUIDIExact(UUIDTextMixin, IExact):
  556. pass
  557. @UUIDField.register_lookup
  558. class UUIDContains(UUIDTextMixin, Contains):
  559. pass
  560. @UUIDField.register_lookup
  561. class UUIDIContains(UUIDTextMixin, IContains):
  562. pass
  563. @UUIDField.register_lookup
  564. class UUIDStartsWith(UUIDTextMixin, StartsWith):
  565. pass
  566. @UUIDField.register_lookup
  567. class UUIDIStartsWith(UUIDTextMixin, IStartsWith):
  568. pass
  569. @UUIDField.register_lookup
  570. class UUIDEndsWith(UUIDTextMixin, EndsWith):
  571. pass
  572. @UUIDField.register_lookup
  573. class UUIDIEndsWith(UUIDTextMixin, IEndsWith):
  574. pass