1# mysql/mysqlconnector.py
2# Copyright (C) 2005-2018 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"""
9.. dialect:: mysql+mysqlconnector
10    :name: MySQL Connector/Python
11    :dbapi: myconnpy
12    :connectstring: mysql+mysqlconnector://<user>:<password>@\
13<host>[:<port>]/<dbname>
14    :url: http://dev.mysql.com/downloads/connector/python/
15
16
17Unicode
18-------
19
20Please see :ref:`mysql_unicode` for current recommendations on unicode
21handling.
22
23"""
24
25from .base import (MySQLDialect, MySQLExecutionContext,
26                   MySQLCompiler, MySQLIdentifierPreparer,
27                   BIT)
28
29from ... import util
30import re
31
32
33class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
34
35    def get_lastrowid(self):
36        return self.cursor.lastrowid
37
38
39class MySQLCompiler_mysqlconnector(MySQLCompiler):
40    def visit_mod_binary(self, binary, operator, **kw):
41        if self.dialect._mysqlconnector_double_percents:
42            return self.process(binary.left, **kw) + " %% " + \
43                self.process(binary.right, **kw)
44        else:
45            return self.process(binary.left, **kw) + " % " + \
46                self.process(binary.right, **kw)
47
48    def post_process_text(self, text):
49        if self.dialect._mysqlconnector_double_percents:
50            return text.replace('%', '%%')
51        else:
52            return text
53
54    def escape_literal_column(self, text):
55        if self.dialect._mysqlconnector_double_percents:
56            return text.replace('%', '%%')
57        else:
58            return text
59
60
61class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer):
62
63    def _escape_identifier(self, value):
64        value = value.replace(self.escape_quote, self.escape_to_quote)
65        if self.dialect._mysqlconnector_double_percents:
66            return value.replace("%", "%%")
67        else:
68            return value
69
70
71class _myconnpyBIT(BIT):
72    def result_processor(self, dialect, coltype):
73        """MySQL-connector already converts mysql bits, so."""
74
75        return None
76
77
78class MySQLDialect_mysqlconnector(MySQLDialect):
79    driver = 'mysqlconnector'
80
81    supports_unicode_binds = True
82
83    supports_sane_rowcount = True
84    supports_sane_multi_rowcount = True
85
86    supports_native_decimal = True
87
88    default_paramstyle = 'format'
89    execution_ctx_cls = MySQLExecutionContext_mysqlconnector
90    statement_compiler = MySQLCompiler_mysqlconnector
91
92    preparer = MySQLIdentifierPreparer_mysqlconnector
93
94    colspecs = util.update_copy(
95        MySQLDialect.colspecs,
96        {
97            BIT: _myconnpyBIT,
98        }
99    )
100
101    @util.memoized_property
102    def supports_unicode_statements(self):
103        return util.py3k or self._mysqlconnector_version_info > (2, 0)
104
105    @classmethod
106    def dbapi(cls):
107        from mysql import connector
108        return connector
109
110    def create_connect_args(self, url):
111        opts = url.translate_connect_args(username='user')
112
113        opts.update(url.query)
114
115        util.coerce_kw_type(opts, 'allow_local_infile', bool)
116        util.coerce_kw_type(opts, 'autocommit', bool)
117        util.coerce_kw_type(opts, 'buffered', bool)
118        util.coerce_kw_type(opts, 'compress', bool)
119        util.coerce_kw_type(opts, 'connection_timeout', int)
120        util.coerce_kw_type(opts, 'connect_timeout', int)
121        util.coerce_kw_type(opts, 'consume_results', bool)
122        util.coerce_kw_type(opts, 'force_ipv6', bool)
123        util.coerce_kw_type(opts, 'get_warnings', bool)
124        util.coerce_kw_type(opts, 'pool_reset_session', bool)
125        util.coerce_kw_type(opts, 'pool_size', int)
126        util.coerce_kw_type(opts, 'raise_on_warnings', bool)
127        util.coerce_kw_type(opts, 'raw', bool)
128        util.coerce_kw_type(opts, 'ssl_verify_cert', bool)
129        util.coerce_kw_type(opts, 'use_pure', bool)
130        util.coerce_kw_type(opts, 'use_unicode', bool)
131
132        # unfortunately, MySQL/connector python refuses to release a
133        # cursor without reading fully, so non-buffered isn't an option
134        opts.setdefault('buffered', True)
135
136        # FOUND_ROWS must be set in ClientFlag to enable
137        # supports_sane_rowcount.
138        if self.dbapi is not None:
139            try:
140                from mysql.connector.constants import ClientFlag
141                client_flags = opts.get(
142                    'client_flags', ClientFlag.get_default())
143                client_flags |= ClientFlag.FOUND_ROWS
144                opts['client_flags'] = client_flags
145            except Exception:
146                pass
147        return [[], opts]
148
149    @util.memoized_property
150    def _mysqlconnector_version_info(self):
151        if self.dbapi and hasattr(self.dbapi, '__version__'):
152            m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?',
153                         self.dbapi.__version__)
154            if m:
155                return tuple(
156                    int(x)
157                    for x in m.group(1, 2, 3)
158                    if x is not None)
159
160    @util.memoized_property
161    def _mysqlconnector_double_percents(self):
162        return not util.py3k and self._mysqlconnector_version_info < (2, 0)
163
164    def _detect_charset(self, connection):
165        return connection.connection.charset
166
167    def _extract_error_code(self, exception):
168        return exception.errno
169
170    def is_disconnect(self, e, connection, cursor):
171        errnos = (2006, 2013, 2014, 2045, 2055, 2048)
172        exceptions = (self.dbapi.OperationalError, self.dbapi.InterfaceError)
173        if isinstance(e, exceptions):
174            return e.errno in errnos or \
175                "MySQL Connection not available." in str(e)
176        else:
177            return False
178
179    def _compat_fetchall(self, rp, charset=None):
180        return rp.fetchall()
181
182    def _compat_fetchone(self, rp, charset=None):
183        return rp.fetchone()
184
185    _isolation_lookup = set(['SERIALIZABLE', 'READ UNCOMMITTED',
186                             'READ COMMITTED', 'REPEATABLE READ',
187                             'AUTOCOMMIT'])
188
189    def _set_isolation_level(self, connection, level):
190        if level == 'AUTOCOMMIT':
191            connection.autocommit = True
192        else:
193            connection.autocommit = False
194            super(MySQLDialect_mysqlconnector, self)._set_isolation_level(
195                connection, level)
196
197
198dialect = MySQLDialect_mysqlconnector
199