~launchpad-committers/storm/lp

« back to all changes in this revision

Viewing changes to storm/databases/mysql.py

  • Committer: Colin Watson
  • Date: 2023-07-06 10:51:34 UTC
  • mfrom: (386.34.138 storm)
  • Revision ID: cjwatson@canonical.com-20230706105134-rzpb4opv8nfpg56r
Merge Storm 0.26 release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Copyright (c) 2006, 2007 Canonical
 
3
#
 
4
# Written by Gustavo Niemeyer <gustavo@niemeyer.net>
 
5
#
 
6
# This file is part of Storm Object Relational Mapper.
 
7
#
 
8
# Storm is free software; you can redistribute it and/or modify
 
9
# it under the terms of the GNU Lesser General Public License as
 
10
# published by the Free Software Foundation; either version 2.1 of
 
11
# the License, or (at your option) any later version.
 
12
#
 
13
# Storm is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public License
 
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
#
 
21
 
 
22
from __future__ import print_function
 
23
 
 
24
from datetime import time, timedelta
 
25
from array import array
 
26
import sys
 
27
 
 
28
from storm.databases import dummy
 
29
 
 
30
try:
 
31
    import MySQLdb
 
32
    import MySQLdb.converters
 
33
except ImportError:
 
34
    MySQLdb = dummy
 
35
 
 
36
from storm.database import (
 
37
    Connection,
 
38
    ConnectionWrapper,
 
39
    Database,
 
40
    Result,
 
41
    )
 
42
from storm.exceptions import (
 
43
    DatabaseModuleError,
 
44
    OperationalError,
 
45
    wrap_exceptions,
 
46
    )
 
47
from storm.expr import (
 
48
    compile,
 
49
    compile_select,
 
50
    Insert,
 
51
    is_safe_token,
 
52
    Select,
 
53
    SQLToken,
 
54
    Undef,
 
55
    )
 
56
from storm.variables import Variable
 
57
 
 
58
 
 
59
compile = compile.create_child()
 
60
 
 
61
@compile.when(Select)
 
62
def compile_select_mysql(compile, select, state):
 
63
    if select.offset is not Undef and select.limit is Undef:
 
64
        select.limit = sys.maxsize
 
65
    return compile_select(compile, select, state)
 
66
 
 
67
@compile.when(SQLToken)
 
68
def compile_sql_token_mysql(compile, expr, state):
 
69
    """MySQL uses ` as the escape character by default."""
 
70
    if is_safe_token(expr) and not compile.is_reserved_word(expr):
 
71
        return expr
 
72
    return '`%s`' % expr.replace('`', '``')
 
73
 
 
74
 
 
75
class MySQLResult(Result):
 
76
 
 
77
    @staticmethod
 
78
    def from_database(row):
 
79
        """Convert MySQL-specific datatypes to "normal" Python types.
 
80
 
 
81
        If there are any C{array} instances in the row, convert them
 
82
        to strings.
 
83
        """
 
84
        for value in row:
 
85
            if isinstance(value, array):
 
86
                yield value.tostring()
 
87
            else:
 
88
                yield value
 
89
 
 
90
 
 
91
class MySQLConnection(Connection):
 
92
 
 
93
    result_factory = MySQLResult
 
94
    param_mark = "%s"
 
95
    compile = compile
 
96
 
 
97
    def execute(self, statement, params=None, noresult=False):
 
98
        if (isinstance(statement, Insert) and
 
99
            statement.primary_variables is not Undef):
 
100
 
 
101
            result = Connection.execute(self, statement, params)
 
102
 
 
103
            # The lastrowid value will be set if:
 
104
            #  - the table had an AUTO INCREMENT column, and
 
105
            #  - the column was not set during the insert or set to 0
 
106
            #
 
107
            # If these conditions are met, then lastrowid will be the
 
108
            # value of the first such column set.  We assume that it
 
109
            # is the first undefined primary key variable.
 
110
            if result._raw_cursor.lastrowid:
 
111
                for variable in statement.primary_variables:
 
112
                    if not variable.is_defined():
 
113
                        variable.set(result._raw_cursor.lastrowid,
 
114
                                     from_db=True)
 
115
                        break
 
116
            if noresult:
 
117
                result = None
 
118
            return result
 
119
        return Connection.execute(self, statement, params, noresult)
 
120
 
 
121
    def to_database(self, params):
 
122
        for param in params:
 
123
            if isinstance(param, Variable):
 
124
                param = param.get(to_db=True)
 
125
            if isinstance(param, timedelta):
 
126
                yield str(param)
 
127
            else:
 
128
                yield param
 
129
 
 
130
    def is_disconnection_error(self, exc, extra_disconnection_errors=()):
 
131
        # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
 
132
        return (isinstance(exc, (OperationalError,
 
133
                                 extra_disconnection_errors)) and
 
134
                exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST)
 
135
 
 
136
 
 
137
class MySQL(Database):
 
138
 
 
139
    connection_factory = MySQLConnection
 
140
    _exception_module = MySQLdb
 
141
    _converters = None
 
142
 
 
143
    def __init__(self, uri):
 
144
        super(MySQL, self).__init__(uri)
 
145
        if MySQLdb is dummy:
 
146
            raise DatabaseModuleError("'MySQLdb' module not found")
 
147
        self._connect_kwargs = {}
 
148
        if uri.database is not None:
 
149
            self._connect_kwargs["db"] = uri.database
 
150
        if uri.host is not None:
 
151
            self._connect_kwargs["host"] = uri.host
 
152
        if uri.port is not None:
 
153
            self._connect_kwargs["port"] = uri.port
 
154
        if uri.username is not None:
 
155
            self._connect_kwargs["user"] = uri.username
 
156
        if uri.password is not None:
 
157
            self._connect_kwargs["passwd"] = uri.password
 
158
        for option in ["unix_socket"]:
 
159
            if option in uri.options:
 
160
                self._connect_kwargs[option] = uri.options.get(option)
 
161
 
 
162
        if self._converters is None:
 
163
            # MySQLdb returns a timedelta by default on TIME fields.
 
164
            converters = MySQLdb.converters.conversions.copy()
 
165
            converters[MySQLdb.converters.FIELD_TYPE.TIME] = _convert_time
 
166
            self.__class__._converters = converters
 
167
 
 
168
        self._connect_kwargs["conv"] = self._converters
 
169
        self._connect_kwargs["use_unicode"] = True
 
170
        # utf8mb3 (a.k.a. utf8) is deprecated, but it's not clear that we
 
171
        # can change it without breaking applications.  Default to utf8mb3
 
172
        # for now.
 
173
        self._connect_kwargs["charset"] = uri.options.get("charset", "utf8mb3")
 
174
 
 
175
    def _raw_connect(self):
 
176
        raw_connection = ConnectionWrapper(
 
177
            MySQLdb.connect(**self._connect_kwargs), self)
 
178
 
 
179
        # Here is another sad story about bad transactional behavior.  MySQL
 
180
        # offers a feature to automatically reconnect dropped connections.
 
181
        # What sounds like a dream, is actually a nightmare for anyone who
 
182
        # is dealing with transactions.  When a reconnection happens, the
 
183
        # currently running transaction is transparently rolled back, and
 
184
        # everything that was being done is lost, without notice.  Not only
 
185
        # that, but the connection may be put back in AUTOCOMMIT mode, even
 
186
        # when that's not the default MySQLdb behavior.  The MySQL developers
 
187
        # quickly understood that this is a terrible idea, and removed the
 
188
        # behavior in MySQL 5.0.3.  Unfortunately, Debian and Ubuntu still
 
189
        # have a patch for the MySQLdb module which *reenables* that
 
190
        # behavior by default even past version 5.0.3 of MySQL.
 
191
        #
 
192
        # Some links:
 
193
        #   http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html
 
194
        #   http://dev.mysql.com/doc/refman/5.0/en/mysql-reconnect.html
 
195
        #   http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
 
196
        #
 
197
        # What we do here is to explore something that is a very weird
 
198
        # side-effect, discovered by reading the code.  When we call the
 
199
        # ping() with a False argument, the automatic reconnection is
 
200
        # disabled in a *permanent* way for this connection.  The argument
 
201
        # to ping() is new in 1.2.2, though.
 
202
        if MySQLdb.version_info >= (1, 2, 2):
 
203
            raw_connection.ping(False)
 
204
 
 
205
        return raw_connection
 
206
 
 
207
    def raw_connect(self):
 
208
        with wrap_exceptions(self):
 
209
            return self._raw_connect()
 
210
 
 
211
 
 
212
create_from_uri = MySQL
 
213
 
 
214
 
 
215
def _convert_time(time_str):
 
216
    h, m, s = time_str.split(":")
 
217
    if "." in s:
 
218
        f = float(s)
 
219
        s = int(f)
 
220
        return time(int(h), int(m), s, (f-s)*1000000)
 
221
    return time(int(h), int(m), int(s), 0)
 
222
 
 
223
 
 
224
# --------------------------------------------------------------------
 
225
# Reserved words, MySQL specific
 
226
 
 
227
# The list of reserved words here are MySQL specific.  SQL92 reserved words
 
228
# are registered in storm.expr, near the "Reserved words, from SQL1992"
 
229
# comment.  The reserved words here were taken from:
 
230
#
 
231
# http://dev.mysql.com/doc/refman/5.4/en/reserved-words.html
 
232
compile.add_reserved_words("""
 
233
    accessible analyze asensitive before bigint binary blob call change
 
234
    condition current_user database databases day_hour day_microsecond
 
235
    day_minute day_second delayed deterministic distinctrow div dual each
 
236
    elseif enclosed escaped exit explain float4 float8 force fulltext
 
237
    high_priority hour_microsecond hour_minute hour_second if ignore index
 
238
    infile inout int1 int2 int3 int4 int8 iterate keys kill leave limit linear
 
239
    lines load localtime localtimestamp lock long longblob longtext loop
 
240
    low_priority master_ssl_verify_server_cert mediumblob mediumint mediumtext
 
241
    middleint minute_microsecond minute_second mod modifies no_write_to_binlog
 
242
    optimize optionally out outfile purge range read_write reads regexp
 
243
    release rename repeat replace require return rlike schemas
 
244
    second_microsecond sensitive separator show spatial specific
 
245
    sql_big_result sql_calc_found_rows sql_small_result sqlexception
 
246
    sqlwarning ssl starting straight_join terminated tinyblob tinyint tinytext
 
247
    trigger undo unlock unsigned use utc_date utc_time utc_timestamp varbinary
 
248
    varcharacter while xor year_month zerofill
 
249
    """.split())