gooderp18绿色标准版
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

275 lignes
11KB

  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import itertools
  3. from collections.abc import Iterable, Iterator
  4. from .sql import SQL, make_identifier
  5. def _sql_from_table(alias: str, table: SQL) -> SQL:
  6. """ Return a FROM clause element from ``alias`` and ``table``. """
  7. if (alias_identifier := SQL.identifier(alias)) == table:
  8. return table
  9. return SQL("%s AS %s", table, alias_identifier)
  10. def _sql_from_join(kind: SQL, alias: str, table: SQL, condition: SQL) -> SQL:
  11. """ Return a FROM clause element for a JOIN. """
  12. return SQL("%s %s ON (%s)", kind, _sql_from_table(alias, table), condition)
  13. _SQL_JOINS = {
  14. "JOIN": SQL("JOIN"),
  15. "LEFT JOIN": SQL("LEFT JOIN"),
  16. }
  17. def _generate_table_alias(src_table_alias: str, link: str) -> str:
  18. """ Generate a standard table alias name. An alias is generated as following:
  19. - the base is the source table name (that can already be an alias)
  20. - then, the joined table is added in the alias using a 'link field name'
  21. that is used to render unique aliases for a given path
  22. - the name is shortcut if it goes beyond PostgreSQL's identifier limits
  23. .. code-block:: pycon
  24. >>> _generate_table_alias('res_users', link='parent_id')
  25. 'res_users__parent_id'
  26. :param str src_table_alias: alias of the source table
  27. :param str link: field name
  28. :return str: alias
  29. """
  30. return make_identifier(f"{src_table_alias}__{link}")
  31. class Query:
  32. """ Simple implementation of a query object, managing tables with aliases,
  33. join clauses (with aliases, condition and parameters), where clauses (with
  34. parameters), order, limit and offset.
  35. :param env: model environment (for lazy evaluation)
  36. :param alias: name or alias of the table
  37. :param table: a table expression (``str`` or ``SQL`` object), optional
  38. """
  39. def __init__(self, env, alias: str, table: (SQL | None) = None):
  40. # database cursor
  41. self._env = env
  42. self._tables: dict[str, SQL] = {
  43. alias: table if table is not None else SQL.identifier(alias),
  44. }
  45. # joins {alias: (kind(SQL), table(SQL), condition(SQL))}
  46. self._joins: dict[str, tuple[SQL, SQL, SQL]] = {}
  47. # holds the list of WHERE conditions (to be joined with 'AND')
  48. self._where_clauses: list[SQL] = []
  49. # groupby, having, order, limit, offset
  50. self.groupby: SQL | None = None
  51. self.having: SQL | None = None
  52. self._order: SQL | None = None
  53. self.limit: int | None = None
  54. self.offset: int | None = None
  55. # memoized result
  56. self._ids: tuple[int, ...] | None = None
  57. def make_alias(self, alias: str, link: str) -> str:
  58. """ Return an alias based on ``alias`` and ``link``. """
  59. return _generate_table_alias(alias, link)
  60. def add_table(self, alias: str, table: (SQL | None) = None):
  61. """ Add a table with a given alias to the from clause. """
  62. assert alias not in self._tables and alias not in self._joins, f"Alias {alias!r} already in {self}"
  63. self._tables[alias] = table if table is not None else SQL.identifier(alias)
  64. self._ids = None
  65. def add_join(self, kind: str, alias: str, table: str | SQL | None, condition: SQL):
  66. """ Add a join clause with the given alias, table and condition. """
  67. sql_kind = _SQL_JOINS.get(kind.upper())
  68. assert sql_kind is not None, f"Invalid JOIN type {kind!r}"
  69. assert alias not in self._tables, f"Alias {alias!r} already used"
  70. table = table or alias
  71. if isinstance(table, str):
  72. table = SQL.identifier(table)
  73. if alias in self._joins:
  74. assert self._joins[alias] == (sql_kind, table, condition)
  75. else:
  76. self._joins[alias] = (sql_kind, table, condition)
  77. self._ids = None
  78. def add_where(self, where_clause: str | SQL, where_params=()):
  79. """ Add a condition to the where clause. """
  80. self._where_clauses.append(SQL(where_clause, *where_params)) # pylint: disable = sql-injection
  81. self._ids = None
  82. def join(self, lhs_alias: str, lhs_column: str, rhs_table: str | SQL, rhs_column: str, link: str) -> str:
  83. """
  84. Perform a join between a table already present in the current Query object and
  85. another table. This method is essentially a shortcut for methods :meth:`~.make_alias`
  86. and :meth:`~.add_join`.
  87. :param str lhs_alias: alias of a table already defined in the current Query object.
  88. :param str lhs_column: column of `lhs_alias` to be used for the join's ON condition.
  89. :param str rhs_table: name of the table to join to `lhs_alias`.
  90. :param str rhs_column: column of `rhs_alias` to be used for the join's ON condition.
  91. :param str link: used to generate the alias for the joined table, this string should
  92. represent the relationship (the link) between both tables.
  93. """
  94. assert lhs_alias in self._tables or lhs_alias in self._joins, "Alias %r not in %s" % (lhs_alias, str(self))
  95. rhs_alias = self.make_alias(lhs_alias, link)
  96. condition = SQL("%s = %s", SQL.identifier(lhs_alias, lhs_column), SQL.identifier(rhs_alias, rhs_column))
  97. self.add_join('JOIN', rhs_alias, rhs_table, condition)
  98. return rhs_alias
  99. def left_join(self, lhs_alias: str, lhs_column: str, rhs_table: str, rhs_column: str, link: str) -> str:
  100. """ Add a LEFT JOIN to the current table (if necessary), and return the
  101. alias corresponding to ``rhs_table``.
  102. See the documentation of :meth:`join` for a better overview of the
  103. arguments and what they do.
  104. """
  105. assert lhs_alias in self._tables or lhs_alias in self._joins, "Alias %r not in %s" % (lhs_alias, str(self))
  106. rhs_alias = self.make_alias(lhs_alias, link)
  107. condition = SQL("%s = %s", SQL.identifier(lhs_alias, lhs_column), SQL.identifier(rhs_alias, rhs_column))
  108. self.add_join('LEFT JOIN', rhs_alias, rhs_table, condition)
  109. return rhs_alias
  110. @property
  111. def order(self) -> SQL | None:
  112. return self._order
  113. @order.setter
  114. def order(self, value: SQL | str | None):
  115. self._order = SQL(value) if value is not None else None # pylint: disable = sql-injection
  116. @property
  117. def table(self) -> str:
  118. """ Return the query's main table, i.e., the first one in the FROM clause. """
  119. return next(iter(self._tables))
  120. @property
  121. def from_clause(self) -> SQL:
  122. """ Return the FROM clause of ``self``, without the FROM keyword. """
  123. tables = SQL(", ").join(itertools.starmap(_sql_from_table, self._tables.items()))
  124. if not self._joins:
  125. return tables
  126. items = (
  127. tables,
  128. *(
  129. _sql_from_join(kind, alias, table, condition)
  130. for alias, (kind, table, condition) in self._joins.items()
  131. ),
  132. )
  133. return SQL(" ").join(items)
  134. @property
  135. def where_clause(self) -> SQL:
  136. """ Return the WHERE condition of ``self``, without the WHERE keyword. """
  137. return SQL(" AND ").join(self._where_clauses)
  138. def is_empty(self) -> bool:
  139. """ Return whether the query is known to return nothing. """
  140. return self._ids == ()
  141. def select(self, *args: str | SQL) -> SQL:
  142. """ Return the SELECT query as an ``SQL`` object. """
  143. sql_args = map(SQL, args) if args else [SQL.identifier(self.table, 'id')]
  144. return SQL(
  145. "%s%s%s%s%s%s%s%s",
  146. SQL("SELECT %s", SQL(", ").join(sql_args)),
  147. SQL(" FROM %s", self.from_clause),
  148. SQL(" WHERE %s", self.where_clause) if self._where_clauses else SQL(),
  149. SQL(" GROUP BY %s", self.groupby) if self.groupby else SQL(),
  150. SQL(" HAVING %s", self.having) if self.having else SQL(),
  151. SQL(" ORDER BY %s", self._order) if self._order else SQL(),
  152. SQL(" LIMIT %s", self.limit) if self.limit else SQL(),
  153. SQL(" OFFSET %s", self.offset) if self.offset else SQL(),
  154. )
  155. def subselect(self, *args: str | SQL) -> SQL:
  156. """ Similar to :meth:`.select`, but for sub-queries.
  157. This one avoids the ORDER BY clause when possible,
  158. and includes parentheses around the subquery.
  159. """
  160. if self._ids is not None and not args:
  161. # inject the known result instead of the subquery
  162. if not self._ids:
  163. # in case we have nothing, we want to use a sub_query with no records
  164. # because an empty tuple leads to a syntax error
  165. # and a tuple containing just None creates issues for `NOT IN`
  166. return SQL("(SELECT 1 WHERE FALSE)")
  167. return SQL("%s", self._ids)
  168. if self.limit or self.offset:
  169. # in this case, the ORDER BY clause is necessary
  170. return SQL("(%s)", self.select(*args))
  171. sql_args = map(SQL, args) if args else [SQL.identifier(self.table, 'id')]
  172. return SQL(
  173. "(%s%s%s)",
  174. SQL("SELECT %s", SQL(", ").join(sql_args)),
  175. SQL(" FROM %s", self.from_clause),
  176. SQL(" WHERE %s", self.where_clause) if self._where_clauses else SQL(),
  177. )
  178. def get_result_ids(self) -> tuple[int, ...]:
  179. """ Return the result of ``self.select()`` as a tuple of ids. The result
  180. is memoized for future use, which avoids making the same query twice.
  181. """
  182. if self._ids is None:
  183. self._ids = tuple(id_ for id_, in self._env.execute_query(self.select()))
  184. return self._ids
  185. def set_result_ids(self, ids: Iterable[int], ordered: bool = True) -> None:
  186. """ Set up the query to return the lines given by ``ids``. The parameter
  187. ``ordered`` tells whether the query must be ordered to match exactly the
  188. sequence ``ids``.
  189. """
  190. assert not (self._joins or self._where_clauses or self.limit or self.offset), \
  191. "Method set_result_ids() can only be called on a virgin Query"
  192. ids = tuple(ids)
  193. if not ids:
  194. self.add_where("FALSE")
  195. elif ordered:
  196. # This guarantees that self.select() returns the results in the
  197. # expected order of ids:
  198. # SELECT "stuff".id
  199. # FROM "stuff"
  200. # JOIN (SELECT * FROM unnest(%s) WITH ORDINALITY) AS "stuff__ids"
  201. # ON ("stuff"."id" = "stuff__ids"."unnest")
  202. # ORDER BY "stuff__ids"."ordinality"
  203. alias = self.join(
  204. self.table, 'id',
  205. SQL('(SELECT * FROM unnest(%s) WITH ORDINALITY)', list(ids)), 'unnest',
  206. 'ids',
  207. )
  208. self.order = SQL.identifier(alias, 'ordinality')
  209. else:
  210. self.add_where(SQL("%s IN %s", SQL.identifier(self.table, 'id'), ids))
  211. self._ids = ids
  212. def __str__(self) -> str:
  213. sql = self.select()
  214. return f"<Query: {sql.code!r} with params: {sql.params!r}>"
  215. def __bool__(self):
  216. return bool(self.get_result_ids())
  217. def __len__(self) -> int:
  218. if self._ids is None:
  219. if self.limit or self.offset:
  220. # optimization: generate a SELECT FROM, and then count the rows
  221. sql = SQL("SELECT COUNT(*) FROM (%s) t", self.select(""))
  222. else:
  223. sql = self.select('COUNT(*)')
  224. return self._env.execute_query(sql)[0][0]
  225. return len(self.get_result_ids())
  226. def __iter__(self) -> Iterator[int]:
  227. return iter(self.get_result_ids())
上海开阖软件有限公司 沪ICP备12045867号-1