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.

279 lines
8.4KB

  1. # postgresql/pygresql.py
  2. # Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """
  8. .. dialect:: postgresql+pygresql
  9. :name: pygresql
  10. :dbapi: pgdb
  11. :connectstring: postgresql+pygresql://user:password@host:port/dbname[?key=value&key=value...]
  12. :url: http://www.pygresql.org/
  13. .. note::
  14. The pygresql dialect is **not tested as part of SQLAlchemy's continuous
  15. integration** and may have unresolved issues. The recommended PostgreSQL
  16. dialect is psycopg2.
  17. .. deprecated:: 1.4 The pygresql DBAPI is deprecated and will be removed
  18. in a future version. Please use one of the supported DBAPIs to
  19. connect to PostgreSQL.
  20. """ # noqa
  21. import decimal
  22. import re
  23. from .base import _DECIMAL_TYPES
  24. from .base import _FLOAT_TYPES
  25. from .base import _INT_TYPES
  26. from .base import PGCompiler
  27. from .base import PGDialect
  28. from .base import PGIdentifierPreparer
  29. from .base import UUID
  30. from .hstore import HSTORE
  31. from .json import JSON
  32. from .json import JSONB
  33. from ... import exc
  34. from ... import processors
  35. from ... import util
  36. from ...sql.elements import Null
  37. from ...types import JSON as Json
  38. from ...types import Numeric
  39. class _PGNumeric(Numeric):
  40. def bind_processor(self, dialect):
  41. return None
  42. def result_processor(self, dialect, coltype):
  43. if not isinstance(coltype, int):
  44. coltype = coltype.oid
  45. if self.asdecimal:
  46. if coltype in _FLOAT_TYPES:
  47. return processors.to_decimal_processor_factory(
  48. decimal.Decimal, self._effective_decimal_return_scale
  49. )
  50. elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
  51. # PyGreSQL returns Decimal natively for 1700 (numeric)
  52. return None
  53. else:
  54. raise exc.InvalidRequestError(
  55. "Unknown PG numeric type: %d" % coltype
  56. )
  57. else:
  58. if coltype in _FLOAT_TYPES:
  59. # PyGreSQL returns float natively for 701 (float8)
  60. return None
  61. elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
  62. return processors.to_float
  63. else:
  64. raise exc.InvalidRequestError(
  65. "Unknown PG numeric type: %d" % coltype
  66. )
  67. class _PGHStore(HSTORE):
  68. def bind_processor(self, dialect):
  69. if not dialect.has_native_hstore:
  70. return super(_PGHStore, self).bind_processor(dialect)
  71. hstore = dialect.dbapi.Hstore
  72. def process(value):
  73. if isinstance(value, dict):
  74. return hstore(value)
  75. return value
  76. return process
  77. def result_processor(self, dialect, coltype):
  78. if not dialect.has_native_hstore:
  79. return super(_PGHStore, self).result_processor(dialect, coltype)
  80. class _PGJSON(JSON):
  81. def bind_processor(self, dialect):
  82. if not dialect.has_native_json:
  83. return super(_PGJSON, self).bind_processor(dialect)
  84. json = dialect.dbapi.Json
  85. def process(value):
  86. if value is self.NULL:
  87. value = None
  88. elif isinstance(value, Null) or (
  89. value is None and self.none_as_null
  90. ):
  91. return None
  92. if value is None or isinstance(value, (dict, list)):
  93. return json(value)
  94. return value
  95. return process
  96. def result_processor(self, dialect, coltype):
  97. if not dialect.has_native_json:
  98. return super(_PGJSON, self).result_processor(dialect, coltype)
  99. class _PGJSONB(JSONB):
  100. def bind_processor(self, dialect):
  101. if not dialect.has_native_json:
  102. return super(_PGJSONB, self).bind_processor(dialect)
  103. json = dialect.dbapi.Json
  104. def process(value):
  105. if value is self.NULL:
  106. value = None
  107. elif isinstance(value, Null) or (
  108. value is None and self.none_as_null
  109. ):
  110. return None
  111. if value is None or isinstance(value, (dict, list)):
  112. return json(value)
  113. return value
  114. return process
  115. def result_processor(self, dialect, coltype):
  116. if not dialect.has_native_json:
  117. return super(_PGJSONB, self).result_processor(dialect, coltype)
  118. class _PGUUID(UUID):
  119. def bind_processor(self, dialect):
  120. if not dialect.has_native_uuid:
  121. return super(_PGUUID, self).bind_processor(dialect)
  122. uuid = dialect.dbapi.Uuid
  123. def process(value):
  124. if value is None:
  125. return None
  126. if isinstance(value, (str, bytes)):
  127. if len(value) == 16:
  128. return uuid(bytes=value)
  129. return uuid(value)
  130. if isinstance(value, int):
  131. return uuid(int=value)
  132. return value
  133. return process
  134. def result_processor(self, dialect, coltype):
  135. if not dialect.has_native_uuid:
  136. return super(_PGUUID, self).result_processor(dialect, coltype)
  137. if not self.as_uuid:
  138. def process(value):
  139. if value is not None:
  140. return str(value)
  141. return process
  142. class _PGCompiler(PGCompiler):
  143. def visit_mod_binary(self, binary, operator, **kw):
  144. return (
  145. self.process(binary.left, **kw)
  146. + " %% "
  147. + self.process(binary.right, **kw)
  148. )
  149. def post_process_text(self, text):
  150. return text.replace("%", "%%")
  151. class _PGIdentifierPreparer(PGIdentifierPreparer):
  152. def _escape_identifier(self, value):
  153. value = value.replace(self.escape_quote, self.escape_to_quote)
  154. return value.replace("%", "%%")
  155. class PGDialect_pygresql(PGDialect):
  156. driver = "pygresql"
  157. supports_statement_cache = True
  158. statement_compiler = _PGCompiler
  159. preparer = _PGIdentifierPreparer
  160. @classmethod
  161. def dbapi(cls):
  162. import pgdb
  163. util.warn_deprecated(
  164. "The pygresql DBAPI is deprecated and will be removed "
  165. "in a future version. Please use one of the supported DBAPIs to "
  166. "connect to PostgreSQL.",
  167. version="1.4",
  168. )
  169. return pgdb
  170. colspecs = util.update_copy(
  171. PGDialect.colspecs,
  172. {
  173. Numeric: _PGNumeric,
  174. HSTORE: _PGHStore,
  175. Json: _PGJSON,
  176. JSON: _PGJSON,
  177. JSONB: _PGJSONB,
  178. UUID: _PGUUID,
  179. },
  180. )
  181. def __init__(self, **kwargs):
  182. super(PGDialect_pygresql, self).__init__(**kwargs)
  183. try:
  184. version = self.dbapi.version
  185. m = re.match(r"(\d+)\.(\d+)", version)
  186. version = (int(m.group(1)), int(m.group(2)))
  187. except (AttributeError, ValueError, TypeError):
  188. version = (0, 0)
  189. self.dbapi_version = version
  190. if version < (5, 0):
  191. has_native_hstore = has_native_json = has_native_uuid = False
  192. if version != (0, 0):
  193. util.warn(
  194. "PyGreSQL is only fully supported by SQLAlchemy"
  195. " since version 5.0."
  196. )
  197. else:
  198. self.supports_unicode_statements = True
  199. self.supports_unicode_binds = True
  200. has_native_hstore = has_native_json = has_native_uuid = True
  201. self.has_native_hstore = has_native_hstore
  202. self.has_native_json = has_native_json
  203. self.has_native_uuid = has_native_uuid
  204. def create_connect_args(self, url):
  205. opts = url.translate_connect_args(username="user")
  206. if "port" in opts:
  207. opts["host"] = "%s:%s" % (
  208. opts.get("host", "").rsplit(":", 1)[0],
  209. opts.pop("port"),
  210. )
  211. opts.update(url.query)
  212. return [], opts
  213. def is_disconnect(self, e, connection, cursor):
  214. if isinstance(e, self.dbapi.Error):
  215. if not connection:
  216. return False
  217. try:
  218. connection = connection.connection
  219. except AttributeError:
  220. pass
  221. else:
  222. if not connection:
  223. return False
  224. try:
  225. return connection.closed
  226. except AttributeError: # PyGreSQL < 5.0
  227. return connection._cnx is None
  228. return False
  229. dialect = PGDialect_pygresql