_range.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. """Implementation of the Range type and adaptation
  2. """
  3. # psycopg/_range.py - Implementation of the Range type and adaptation
  4. #
  5. # Copyright (C) 2012-2019 Daniele Varrazzo <daniele.varrazzo@gmail.com>
  6. # Copyright (C) 2020-2021 The Psycopg Team
  7. #
  8. # psycopg2 is free software: you can redistribute it and/or modify it
  9. # under the terms of the GNU Lesser General Public License as published
  10. # by the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # In addition, as a special exception, the copyright holders give
  14. # permission to link this program with the OpenSSL library (or with
  15. # modified versions of OpenSSL that use the same license as OpenSSL),
  16. # and distribute linked combinations including the two.
  17. #
  18. # You must obey the GNU Lesser General Public License in all respects for
  19. # all of the code used other than OpenSSL.
  20. #
  21. # psycopg2 is distributed in the hope that it will be useful, but WITHOUT
  22. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  23. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  24. # License for more details.
  25. import re
  26. from psycopg2._psycopg import ProgrammingError, InterfaceError
  27. from psycopg2.extensions import ISQLQuote, adapt, register_adapter
  28. from psycopg2.extensions import new_type, new_array_type, register_type
  29. class Range:
  30. """Python representation for a PostgreSQL |range|_ type.
  31. :param lower: lower bound for the range. `!None` means unbound
  32. :param upper: upper bound for the range. `!None` means unbound
  33. :param bounds: one of the literal strings ``()``, ``[)``, ``(]``, ``[]``,
  34. representing whether the lower or upper bounds are included
  35. :param empty: if `!True`, the range is empty
  36. """
  37. __slots__ = ('_lower', '_upper', '_bounds')
  38. def __init__(self, lower=None, upper=None, bounds='[)', empty=False):
  39. if not empty:
  40. if bounds not in ('[)', '(]', '()', '[]'):
  41. raise ValueError(f"bound flags not valid: {bounds!r}")
  42. self._lower = lower
  43. self._upper = upper
  44. self._bounds = bounds
  45. else:
  46. self._lower = self._upper = self._bounds = None
  47. def __repr__(self):
  48. if self._bounds is None:
  49. return f"{self.__class__.__name__}(empty=True)"
  50. else:
  51. return "{}({!r}, {!r}, {!r})".format(self.__class__.__name__,
  52. self._lower, self._upper, self._bounds)
  53. def __str__(self):
  54. if self._bounds is None:
  55. return 'empty'
  56. items = [
  57. self._bounds[0],
  58. str(self._lower),
  59. ', ',
  60. str(self._upper),
  61. self._bounds[1]
  62. ]
  63. return ''.join(items)
  64. @property
  65. def lower(self):
  66. """The lower bound of the range. `!None` if empty or unbound."""
  67. return self._lower
  68. @property
  69. def upper(self):
  70. """The upper bound of the range. `!None` if empty or unbound."""
  71. return self._upper
  72. @property
  73. def isempty(self):
  74. """`!True` if the range is empty."""
  75. return self._bounds is None
  76. @property
  77. def lower_inf(self):
  78. """`!True` if the range doesn't have a lower bound."""
  79. if self._bounds is None:
  80. return False
  81. return self._lower is None
  82. @property
  83. def upper_inf(self):
  84. """`!True` if the range doesn't have an upper bound."""
  85. if self._bounds is None:
  86. return False
  87. return self._upper is None
  88. @property
  89. def lower_inc(self):
  90. """`!True` if the lower bound is included in the range."""
  91. if self._bounds is None or self._lower is None:
  92. return False
  93. return self._bounds[0] == '['
  94. @property
  95. def upper_inc(self):
  96. """`!True` if the upper bound is included in the range."""
  97. if self._bounds is None or self._upper is None:
  98. return False
  99. return self._bounds[1] == ']'
  100. def __contains__(self, x):
  101. if self._bounds is None:
  102. return False
  103. if self._lower is not None:
  104. if self._bounds[0] == '[':
  105. if x < self._lower:
  106. return False
  107. else:
  108. if x <= self._lower:
  109. return False
  110. if self._upper is not None:
  111. if self._bounds[1] == ']':
  112. if x > self._upper:
  113. return False
  114. else:
  115. if x >= self._upper:
  116. return False
  117. return True
  118. def __bool__(self):
  119. return self._bounds is not None
  120. def __nonzero__(self):
  121. # Python 2 compatibility
  122. return type(self).__bool__(self)
  123. def __eq__(self, other):
  124. if not isinstance(other, Range):
  125. return False
  126. return (self._lower == other._lower
  127. and self._upper == other._upper
  128. and self._bounds == other._bounds)
  129. def __ne__(self, other):
  130. return not self.__eq__(other)
  131. def __hash__(self):
  132. return hash((self._lower, self._upper, self._bounds))
  133. # as the postgres docs describe for the server-side stuff,
  134. # ordering is rather arbitrary, but will remain stable
  135. # and consistent.
  136. def __lt__(self, other):
  137. if not isinstance(other, Range):
  138. return NotImplemented
  139. for attr in ('_lower', '_upper', '_bounds'):
  140. self_value = getattr(self, attr)
  141. other_value = getattr(other, attr)
  142. if self_value == other_value:
  143. pass
  144. elif self_value is None:
  145. return True
  146. elif other_value is None:
  147. return False
  148. else:
  149. return self_value < other_value
  150. return False
  151. def __le__(self, other):
  152. if self == other:
  153. return True
  154. else:
  155. return self.__lt__(other)
  156. def __gt__(self, other):
  157. if isinstance(other, Range):
  158. return other.__lt__(self)
  159. else:
  160. return NotImplemented
  161. def __ge__(self, other):
  162. if self == other:
  163. return True
  164. else:
  165. return self.__gt__(other)
  166. def __getstate__(self):
  167. return {slot: getattr(self, slot)
  168. for slot in self.__slots__ if hasattr(self, slot)}
  169. def __setstate__(self, state):
  170. for slot, value in state.items():
  171. setattr(self, slot, value)
  172. def register_range(pgrange, pyrange, conn_or_curs, globally=False):
  173. """Create and register an adapter and the typecasters to convert between
  174. a PostgreSQL |range|_ type and a PostgreSQL `Range` subclass.
  175. :param pgrange: the name of the PostgreSQL |range| type. Can be
  176. schema-qualified
  177. :param pyrange: a `Range` strict subclass, or just a name to give to a new
  178. class
  179. :param conn_or_curs: a connection or cursor used to find the oid of the
  180. range and its subtype; the typecaster is registered in a scope limited
  181. to this object, unless *globally* is set to `!True`
  182. :param globally: if `!False` (default) register the typecaster only on
  183. *conn_or_curs*, otherwise register it globally
  184. :return: `RangeCaster` instance responsible for the conversion
  185. If a string is passed to *pyrange*, a new `Range` subclass is created
  186. with such name and will be available as the `~RangeCaster.range` attribute
  187. of the returned `RangeCaster` object.
  188. The function queries the database on *conn_or_curs* to inspect the
  189. *pgrange* type and raises `~psycopg2.ProgrammingError` if the type is not
  190. found. If querying the database is not advisable, use directly the
  191. `RangeCaster` class and register the adapter and typecasters using the
  192. provided functions.
  193. """
  194. caster = RangeCaster._from_db(pgrange, pyrange, conn_or_curs)
  195. caster._register(not globally and conn_or_curs or None)
  196. return caster
  197. class RangeAdapter:
  198. """`ISQLQuote` adapter for `Range` subclasses.
  199. This is an abstract class: concrete classes must set a `name` class
  200. attribute or override `getquoted()`.
  201. """
  202. name = None
  203. def __init__(self, adapted):
  204. self.adapted = adapted
  205. def __conform__(self, proto):
  206. if self._proto is ISQLQuote:
  207. return self
  208. def prepare(self, conn):
  209. self._conn = conn
  210. def getquoted(self):
  211. if self.name is None:
  212. raise NotImplementedError(
  213. 'RangeAdapter must be subclassed overriding its name '
  214. 'or the getquoted() method')
  215. r = self.adapted
  216. if r.isempty:
  217. return b"'empty'::" + self.name.encode('utf8')
  218. if r.lower is not None:
  219. a = adapt(r.lower)
  220. if hasattr(a, 'prepare'):
  221. a.prepare(self._conn)
  222. lower = a.getquoted()
  223. else:
  224. lower = b'NULL'
  225. if r.upper is not None:
  226. a = adapt(r.upper)
  227. if hasattr(a, 'prepare'):
  228. a.prepare(self._conn)
  229. upper = a.getquoted()
  230. else:
  231. upper = b'NULL'
  232. return self.name.encode('utf8') + b'(' + lower + b', ' + upper \
  233. + b", '" + r._bounds.encode('utf8') + b"')"
  234. class RangeCaster:
  235. """Helper class to convert between `Range` and PostgreSQL range types.
  236. Objects of this class are usually created by `register_range()`. Manual
  237. creation could be useful if querying the database is not advisable: in
  238. this case the oids must be provided.
  239. """
  240. def __init__(self, pgrange, pyrange, oid, subtype_oid, array_oid=None):
  241. self.subtype_oid = subtype_oid
  242. self._create_ranges(pgrange, pyrange)
  243. name = self.adapter.name or self.adapter.__class__.__name__
  244. self.typecaster = new_type((oid,), name, self.parse)
  245. if array_oid is not None:
  246. self.array_typecaster = new_array_type(
  247. (array_oid,), name + "ARRAY", self.typecaster)
  248. else:
  249. self.array_typecaster = None
  250. def _create_ranges(self, pgrange, pyrange):
  251. """Create Range and RangeAdapter classes if needed."""
  252. # if got a string create a new RangeAdapter concrete type (with a name)
  253. # else take it as an adapter. Passing an adapter should be considered
  254. # an implementation detail and is not documented. It is currently used
  255. # for the numeric ranges.
  256. self.adapter = None
  257. if isinstance(pgrange, str):
  258. self.adapter = type(pgrange, (RangeAdapter,), {})
  259. self.adapter.name = pgrange
  260. else:
  261. try:
  262. if issubclass(pgrange, RangeAdapter) \
  263. and pgrange is not RangeAdapter:
  264. self.adapter = pgrange
  265. except TypeError:
  266. pass
  267. if self.adapter is None:
  268. raise TypeError(
  269. 'pgrange must be a string or a RangeAdapter strict subclass')
  270. self.range = None
  271. try:
  272. if isinstance(pyrange, str):
  273. self.range = type(pyrange, (Range,), {})
  274. if issubclass(pyrange, Range) and pyrange is not Range:
  275. self.range = pyrange
  276. except TypeError:
  277. pass
  278. if self.range is None:
  279. raise TypeError(
  280. 'pyrange must be a type or a Range strict subclass')
  281. @classmethod
  282. def _from_db(self, name, pyrange, conn_or_curs):
  283. """Return a `RangeCaster` instance for the type *pgrange*.
  284. Raise `ProgrammingError` if the type is not found.
  285. """
  286. from psycopg2.extensions import STATUS_IN_TRANSACTION
  287. from psycopg2.extras import _solve_conn_curs
  288. conn, curs = _solve_conn_curs(conn_or_curs)
  289. if conn.info.server_version < 90200:
  290. raise ProgrammingError("range types not available in version %s"
  291. % conn.info.server_version)
  292. # Store the transaction status of the connection to revert it after use
  293. conn_status = conn.status
  294. # Use the correct schema
  295. if '.' in name:
  296. schema, tname = name.split('.', 1)
  297. else:
  298. tname = name
  299. schema = 'public'
  300. # get the type oid and attributes
  301. try:
  302. curs.execute("""\
  303. select rngtypid, rngsubtype,
  304. (select typarray from pg_type where oid = rngtypid)
  305. from pg_range r
  306. join pg_type t on t.oid = rngtypid
  307. join pg_namespace ns on ns.oid = typnamespace
  308. where typname = %s and ns.nspname = %s;
  309. """, (tname, schema))
  310. except ProgrammingError:
  311. if not conn.autocommit:
  312. conn.rollback()
  313. raise
  314. else:
  315. rec = curs.fetchone()
  316. # revert the status of the connection as before the command
  317. if (conn_status != STATUS_IN_TRANSACTION
  318. and not conn.autocommit):
  319. conn.rollback()
  320. if not rec:
  321. raise ProgrammingError(
  322. f"PostgreSQL type '{name}' not found")
  323. type, subtype, array = rec
  324. return RangeCaster(name, pyrange,
  325. oid=type, subtype_oid=subtype, array_oid=array)
  326. _re_range = re.compile(r"""
  327. ( \(|\[ ) # lower bound flag
  328. (?: # lower bound:
  329. " ( (?: [^"] | "")* ) " # - a quoted string
  330. | ( [^",]+ ) # - or an unquoted string
  331. )? # - or empty (not catched)
  332. ,
  333. (?: # upper bound:
  334. " ( (?: [^"] | "")* ) " # - a quoted string
  335. | ( [^"\)\]]+ ) # - or an unquoted string
  336. )? # - or empty (not catched)
  337. ( \)|\] ) # upper bound flag
  338. """, re.VERBOSE)
  339. _re_undouble = re.compile(r'(["\\])\1')
  340. def parse(self, s, cur=None):
  341. if s is None:
  342. return None
  343. if s == 'empty':
  344. return self.range(empty=True)
  345. m = self._re_range.match(s)
  346. if m is None:
  347. raise InterfaceError(f"failed to parse range: '{s}'")
  348. lower = m.group(3)
  349. if lower is None:
  350. lower = m.group(2)
  351. if lower is not None:
  352. lower = self._re_undouble.sub(r"\1", lower)
  353. upper = m.group(5)
  354. if upper is None:
  355. upper = m.group(4)
  356. if upper is not None:
  357. upper = self._re_undouble.sub(r"\1", upper)
  358. if cur is not None:
  359. lower = cur.cast(self.subtype_oid, lower)
  360. upper = cur.cast(self.subtype_oid, upper)
  361. bounds = m.group(1) + m.group(6)
  362. return self.range(lower, upper, bounds)
  363. def _register(self, scope=None):
  364. register_type(self.typecaster, scope)
  365. if self.array_typecaster is not None:
  366. register_type(self.array_typecaster, scope)
  367. register_adapter(self.range, self.adapter)
  368. class NumericRange(Range):
  369. """A `Range` suitable to pass Python numeric types to a PostgreSQL range.
  370. PostgreSQL types :sql:`int4range`, :sql:`int8range`, :sql:`numrange` are
  371. casted into `!NumericRange` instances.
  372. """
  373. pass
  374. class DateRange(Range):
  375. """Represents :sql:`daterange` values."""
  376. pass
  377. class DateTimeRange(Range):
  378. """Represents :sql:`tsrange` values."""
  379. pass
  380. class DateTimeTZRange(Range):
  381. """Represents :sql:`tstzrange` values."""
  382. pass
  383. # Special adaptation for NumericRange. Allows to pass number range regardless
  384. # of whether they are ints, floats and what size of ints are, which are
  385. # pointless in Python world. On the way back, no numeric range is casted to
  386. # NumericRange, but only to their subclasses
  387. class NumberRangeAdapter(RangeAdapter):
  388. """Adapt a range if the subtype doesn't need quotes."""
  389. def getquoted(self):
  390. r = self.adapted
  391. if r.isempty:
  392. return b"'empty'"
  393. if not r.lower_inf:
  394. # not exactly: we are relying that none of these object is really
  395. # quoted (they are numbers). Also, I'm lazy and not preparing the
  396. # adapter because I assume encoding doesn't matter for these
  397. # objects.
  398. lower = adapt(r.lower).getquoted().decode('ascii')
  399. else:
  400. lower = ''
  401. if not r.upper_inf:
  402. upper = adapt(r.upper).getquoted().decode('ascii')
  403. else:
  404. upper = ''
  405. return (f"'{r._bounds[0]}{lower},{upper}{r._bounds[1]}'").encode('ascii')
  406. # TODO: probably won't work with infs, nans and other tricky cases.
  407. register_adapter(NumericRange, NumberRangeAdapter)
  408. # Register globally typecasters and adapters for builtin range types.
  409. # note: the adapter is registered more than once, but this is harmless.
  410. int4range_caster = RangeCaster(NumberRangeAdapter, NumericRange,
  411. oid=3904, subtype_oid=23, array_oid=3905)
  412. int4range_caster._register()
  413. int8range_caster = RangeCaster(NumberRangeAdapter, NumericRange,
  414. oid=3926, subtype_oid=20, array_oid=3927)
  415. int8range_caster._register()
  416. numrange_caster = RangeCaster(NumberRangeAdapter, NumericRange,
  417. oid=3906, subtype_oid=1700, array_oid=3907)
  418. numrange_caster._register()
  419. daterange_caster = RangeCaster('daterange', DateRange,
  420. oid=3912, subtype_oid=1082, array_oid=3913)
  421. daterange_caster._register()
  422. tsrange_caster = RangeCaster('tsrange', DateTimeRange,
  423. oid=3908, subtype_oid=1114, array_oid=3909)
  424. tsrange_caster._register()
  425. tstzrange_caster = RangeCaster('tstzrange', DateTimeTZRange,
  426. oid=3910, subtype_oid=1184, array_oid=3911)
  427. tstzrange_caster._register()