_range.py 17 KB

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