]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Add support for PostgreSQL with PyGreSQL
authorChristoph Zwerschke <cito@online.de>
Tue, 12 Apr 2016 03:15:25 +0000 (23:15 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 15 Apr 2016 16:00:27 +0000 (12:00 -0400)
Change-Id: I040b75ff3b4110e7e8b26442a4eb226ba8c26715
Pull-request: https://github.com/zzzeek/sqlalchemy/pull/234

doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
doc/build/dialects/postgresql.rst
lib/sqlalchemy/dialects/postgresql/__init__.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/postgresql/pygresql.py [new file with mode: 0644]
test/dialect/postgresql/test_query.py
test/dialect/postgresql/test_types.py
test/engine/test_execute.py

index 53bd38a98e94a0873405db0701eec5b5ecb8c870..dc7d5105e3b9f3386c9272feed0e241bdab99021 100644 (file)
 .. changelog::
     :version: 1.1.0b1
 
+    .. change::
+        :tags: feature, postgresql
+
+        Added a new dialect for the PyGreSQL Postgresql dialect.  Thanks
+        to Christoph Zwerschke and Kaolin Imago Fire for their efforts.
+
     .. change::
         :tags: bug, orm
         :tickets: 3488
index 6f0da37809c48b46a4c391dc0933808e5f20198f..ac6cf1dc71409b0c6e99952cda4487c51b0d2a8d 100644 (file)
@@ -1935,6 +1935,15 @@ emits::
 
 :ticket:`2729`
 
+Support for PyGreSQL
+--------------------
+
+The `PyGreSQL <https://pypi.python.org/pypi/PyGreSQL>`_ DBAPI is now supported.
+
+.. seealso::
+
+    :ref:`dialect-postgresql-pygresql`
+
 The "postgres" module is removed
 ---------------------------------
 
index 616924685e6997e432e3c441572242713aaa3e5f..b4c90643d98204ee08728dc012bd1e1d7bac161d 100644 (file)
@@ -182,28 +182,34 @@ For example::
       )
 
 psycopg2
---------------
+--------
 
 .. automodule:: sqlalchemy.dialects.postgresql.psycopg2
 
 pg8000
---------------
+------
 
 .. automodule:: sqlalchemy.dialects.postgresql.pg8000
 
 psycopg2cffi
---------------
+------------
 
 .. automodule:: sqlalchemy.dialects.postgresql.psycopg2cffi
 
 py-postgresql
---------------------
+-------------
 
 .. automodule:: sqlalchemy.dialects.postgresql.pypostgresql
 
+.. _dialect-postgresql-pygresql:
+
+pygresql
+--------
+
+.. automodule:: sqlalchemy.dialects.postgresql.pygresql
 
 zxjdbc
---------------
+------
 
 .. automodule:: sqlalchemy.dialects.postgresql.zxjdbc
 
index 8aa4509bedbb4938527488b0332a6da905bdbc88..ffd100f6759543d36eec5170743a1ff97451d1ff 100644 (file)
@@ -5,7 +5,8 @@
 # This module is part of SQLAlchemy and is released under
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
-from . import base, psycopg2, pg8000, pypostgresql, zxjdbc, psycopg2cffi
+from . import base, psycopg2, pg8000, pypostgresql, pygresql, \
+    zxjdbc, psycopg2cffi
 
 base.dialect = psycopg2.dialect
 
index eb3449e40255bcd83e3290b907eb406c6e8753de..9d019b56eefcea9c1c7c155ad431731c825bcd16 100644 (file)
@@ -2395,7 +2395,9 @@ class PGDialect(default.DefaultDialect):
                   i.relname
             """
 
-        t = sql.text(IDX_SQL, typemap={'attname': sqltypes.Unicode})
+        t = sql.text(IDX_SQL, typemap={
+            'relname': sqltypes.Unicode,
+            'attname': sqltypes.Unicode})
         c = connection.execute(t, table_oid=table_oid)
 
         indexes = defaultdict(lambda: defaultdict(dict))
diff --git a/lib/sqlalchemy/dialects/postgresql/pygresql.py b/lib/sqlalchemy/dialects/postgresql/pygresql.py
new file mode 100644 (file)
index 0000000..d302066
--- /dev/null
@@ -0,0 +1,243 @@
+# postgresql/pygresql.py
+# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""
+.. dialect:: postgresql+pygresql
+    :name: pygresql
+    :dbapi: pgdb
+    :connectstring: postgresql+pygresql://user:password@host:port/dbname\
+[?key=value&key=value...]
+    :url: http://www.pygresql.org/
+"""
+
+import decimal
+import re
+
+from ... import exc, processors, util
+from ...types import Numeric, JSON as Json
+from ...sql.elements import Null
+from .base import PGDialect, PGCompiler, PGIdentifierPreparer, \
+    _DECIMAL_TYPES, _FLOAT_TYPES, _INT_TYPES, UUID
+from .hstore import HSTORE
+from .json import JSON, JSONB
+
+
+class _PGNumeric(Numeric):
+
+    def bind_processor(self, dialect):
+        return None
+
+    def result_processor(self, dialect, coltype):
+        if not isinstance(coltype, int):
+            coltype = coltype.oid
+        if self.asdecimal:
+            if coltype in _FLOAT_TYPES:
+                return processors.to_decimal_processor_factory(
+                    decimal.Decimal,
+                    self._effective_decimal_return_scale)
+            elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
+                # PyGreSQL returns Decimal natively for 1700 (numeric)
+                return None
+            else:
+                raise exc.InvalidRequestError(
+                    "Unknown PG numeric type: %d" % coltype)
+        else:
+            if coltype in _FLOAT_TYPES:
+                # PyGreSQL returns float natively for 701 (float8)
+                return None
+            elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
+                return processors.to_float
+            else:
+                raise exc.InvalidRequestError(
+                    "Unknown PG numeric type: %d" % coltype)
+
+
+class _PGHStore(HSTORE):
+
+    def bind_processor(self, dialect):
+        if not dialect.has_native_hstore:
+            return super(_PGHStore, self).bind_processor(dialect)
+        hstore = dialect.dbapi.Hstore
+        def process(value):
+            if isinstance(value, dict):
+                return hstore(value)
+            return value
+        return process
+
+    def result_processor(self, dialect, coltype):
+        if not dialect.has_native_hstore:
+            return super(_PGHStore, self).result_processor(dialect, coltype)
+
+
+class _PGJSON(JSON):
+
+    def bind_processor(self, dialect):
+        if not dialect.has_native_json:
+            return super(_PGJSON, self).bind_processor(dialect)
+        json = dialect.dbapi.Json
+
+        def process(value):
+            if value is self.NULL:
+                value = None
+            elif isinstance(value, Null) or (
+                value is None and self.none_as_null):
+                return None
+            if value is None or isinstance(value, (dict, list)):
+                return json(value)
+            return value
+
+        return process
+
+    def result_processor(self, dialect, coltype):
+        if not dialect.has_native_json:
+            return super(_PGJSON, self).result_processor(dialect, coltype)
+
+
+class _PGJSONB(JSONB):
+
+    def bind_processor(self, dialect):
+        if not dialect.has_native_json:
+            return super(_PGJSONB, self).bind_processor(dialect)
+        json = dialect.dbapi.Json
+
+        def process(value):
+            if value is self.NULL:
+                value = None
+            elif isinstance(value, Null) or (
+                value is None and self.none_as_null):
+                return None
+            if value is None or isinstance(value, (dict, list)):
+                return json(value)
+            return value
+
+        return process
+
+    def result_processor(self, dialect, coltype):
+        if not dialect.has_native_json:
+            return super(_PGJSONB, self).result_processor(dialect, coltype)
+
+
+class _PGUUID(UUID):
+
+    def bind_processor(self, dialect):
+        if not dialect.has_native_uuid:
+            return super(_PGUUID, self).bind_processor(dialect)
+        uuid = dialect.dbapi.Uuid
+
+        def process(value):
+            if value is None:
+                return None
+            if isinstance(value, (str, bytes)):
+                if len(value) == 16:
+                    return uuid(bytes=value)
+                return uuid(value)
+            if isinstance(value, int):
+                return uuid(int=value)
+            return value
+
+        return process
+
+    def result_processor(self, dialect, coltype):
+        if not dialect.has_native_uuid:
+            return super(_PGUUID, self).result_processor(dialect, coltype)
+        if not self.as_uuid:
+            def process(value):
+                if value is not None:
+                    return str(value)
+            return process
+
+
+class _PGCompiler(PGCompiler):
+
+    def visit_mod_binary(self, binary, operator, **kw):
+        return self.process(binary.left, **kw) + " %% " + \
+            self.process(binary.right, **kw)
+
+    def post_process_text(self, text):
+        return text.replace('%', '%%')
+
+
+class _PGIdentifierPreparer(PGIdentifierPreparer):
+
+    def _escape_identifier(self, value):
+        value = value.replace(self.escape_quote, self.escape_to_quote)
+        return value.replace('%', '%%')
+
+
+class PGDialect_pygresql(PGDialect):
+
+    driver = 'pygresql'
+
+    statement_compiler = _PGCompiler
+    preparer = _PGIdentifierPreparer
+
+    @classmethod
+    def dbapi(cls):
+        import pgdb
+        return pgdb
+
+    colspecs = util.update_copy(
+        PGDialect.colspecs,
+        {
+            Numeric: _PGNumeric,
+            HSTORE: _PGHStore,
+            Json: _PGJSON,
+            JSON: _PGJSON,
+            JSONB: _PGJSONB,
+            UUID: _PGUUID,
+        }
+    )
+
+    def __init__(self, **kwargs):
+        super(PGDialect_pygresql, self).__init__(**kwargs)
+        try:
+            version = self.dbapi.version
+            m = re.match(r'(\d+)\.(\d+)', version)
+            version = (int(m.group(1)), int(m.group(2)))
+        except (AttributeError, ValueError, TypeError):
+            version = (0, 0)
+        self.dbapi_version = version
+        if version < (5, 0):
+            has_native_hstore = has_native_json = has_native_uuid = False
+            if version != (0, 0):
+                util.warn("PyGreSQL is only fully supported by SQLAlchemy"
+                    " since version 5.0.")
+        else:
+            self.supports_unicode_statements = True
+            self.supports_unicode_binds = True
+            has_native_hstore = has_native_json = has_native_uuid = True
+        self.has_native_hstore = has_native_hstore
+        self.has_native_json = has_native_json
+        self.has_native_uuid = has_native_uuid
+
+    def create_connect_args(self, url):
+        opts = url.translate_connect_args(username='user')
+        if 'port' in opts:
+            opts['host'] = '%s:%s' % (
+                opts.get('host', '').rsplit(':', 1)[0], opts.pop('port'))
+        opts.update(url.query)
+        return [], opts
+
+    def is_disconnect(self, e, connection, cursor):
+        if isinstance(e, self.dbapi.Error):
+            if not connection:
+                return False
+            try:
+                connection = connection.connection
+            except AttributeError:
+                pass
+            else:
+                if not connection:
+                    return False
+            try:
+                return connection.closed
+            except AttributeError:  # PyGreSQL < 5.0
+                return connection._cnx is None
+        return False
+
+
+dialect = PGDialect_pygresql
index 9f92a7830f9f865e48142905c6b43dd41e649a12..c031e43def9e161f5b84a00628cd55f6d781051b 100644 (file)
@@ -761,6 +761,7 @@ class MatchTest(fixtures.TestBase, AssertsCompiledSQL):
 
     @testing.fails_on('postgresql+psycopg2', 'uses pyformat')
     @testing.fails_on('postgresql+pypostgresql', 'uses pyformat')
+    @testing.fails_on('postgresql+pygresql', 'uses pyformat')
     @testing.fails_on('postgresql+zxjdbc', 'uses qmark')
     @testing.fails_on('postgresql+psycopg2cffi', 'uses pyformat')
     def test_expression_positional(self):
index 8818a9941238bb6a630c4b95f9f95b72b7d9dcb7..6bcc4cf9ad40e041cd8835b5ceb82d5c5b5906e9 100644 (file)
@@ -544,11 +544,11 @@ class NumericInterpretationTest(fixtures.TestBase):
     __backend__ = True
 
     def test_numeric_codes(self):
-        from sqlalchemy.dialects.postgresql import psycopg2cffi, pg8000, \
-            psycopg2, base
+        from sqlalchemy.dialects.postgresql import pg8000, pygresql, \
+            psycopg2, psycopg2cffi, base
 
-        dialects = (pg8000.dialect(), psycopg2.dialect(),
-                    psycopg2cffi.dialect())
+        dialects = (pg8000.dialect(), pygresql.dialect(),
+                psycopg2.dialect(), psycopg2cffi.dialect())
         for dialect in dialects:
             typ = Numeric().dialect_impl(dialect)
             for code in base._INT_TYPES + base._FLOAT_TYPES + \
@@ -2757,7 +2757,10 @@ class JSONRoundTripTest(fixtures.TablesTest):
         result = engine.execute(
             select([data_table.c.data['k1'].astext])
         ).first()
-        assert isinstance(result[0], util.text_type)
+        if engine.dialect.returns_unicode_strings:
+            assert isinstance(result[0], util.text_type)
+        else:
+            assert isinstance(result[0], util.string_types)
 
     def test_query_returned_as_int(self):
         engine = testing.db
index 76d60f207c7f656c68a96ba3623a8700c6e3288c..b1c8673d11ddf6b316f74e28f87936a1bd54b0c7 100644 (file)
@@ -177,8 +177,8 @@ class ExecuteTest(fixtures.TestBase):
         lambda: testing.against('mysql+mysqldb'), 'db-api flaky')
     @testing.fails_on_everything_except(
         'postgresql+psycopg2', 'postgresql+psycopg2cffi',
-        'postgresql+pypostgresql', 'mysql+mysqlconnector',
-        'mysql+pymysql', 'mysql+cymysql')
+        'postgresql+pypostgresql', 'postgresql+pygresql',
+        'mysql+mysqlconnector', 'mysql+pymysql', 'mysql+cymysql')
     def test_raw_python(self):
         def go(conn):
             conn.execute(