diff --git a/.gitmodules b/.gitmodules index 9d4973fb..e628c2cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "cpyint"] path = cpyint - url = ../python-internal.git - branch = master + url = ../connector-python-internal + branch = master-2.0 diff --git a/CHANGES.txt b/CHANGES.txt index 18859e2a..fbdca18f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,11 +3,50 @@ MySQL Connector/Python 2.0 - Release Notes & Changes ==================================================== MySQL Connector/Python -Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. Full release notes: http://dev.mysql.com/doc/relnotes/connector-python/en/ + +v2.0.5 +====== +- BUG22529828: Fix potencial SQL injection + +v2.0.4 +====== + +- BUG20324089: Fix HASH based sharding with MySQL Fabric +- BUG20462427: Fix receiving large field data from server +- BUG20301989: Fix conversion of empty set +- BUG20407036: Fix incorrect arguments to mysld_stmt_execute error +- BUG20106629: Support Django Safetext and SafeBytes type + +v2.0.3 +====== + +- BUG19703022: Fix using passwords with integers only in option files +- BUG19777815: Add support for warnings with MySQLCursor.callproc() +- BUG19972427: Fix creating of redundant connections in Django +- BUG19331658: Fix connection pooling with MySQL Fabric +- BUG19930054: Lost connection to server error during query +- BUG19803702: Fix reporting errors with non-ascii characters + +v2.0.2 +====== + +- BUG19500097: Fix string decoding with binary character set +- BUG19677659: Move testings of errors to internal repository +- BUG19549363: Raise error when compression used with reset_session +- BUG19642249: Improve errors reporting invalid sharding keys +- BUG19711759: Fix Pylint issue in network module +- BUG19667984: Fix converting invalid datetime values in Django backend +- BUG19660283: Fix failing unit tests with MySQL server 5.7.5 +- BUG19584051: Fix comparison of type_code of columns for PEP-249 +- BUG19522948: Fix data corruption with TEXT and prepared statements +- BUG19584116: Fix extra signal causing runtime error in Django + + v2.0.1 ====== diff --git a/README.txt b/README.txt index a4c973d9..99be6286 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ MySQL Connector/Python 2.0 ========================== MySQL Connector/Python -Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. License information can be found in the LICENSE.txt file. @@ -28,7 +28,7 @@ doubt, this particular copy of the software is released under the version 2 of the GNU General Public License. MySQL Connector/Python is brought to you by Oracle. -Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. License information can be found in the LICENSE.txt file. diff --git a/cpyint b/cpyint index 15a74ca8..9b499655 160000 --- a/cpyint +++ b/cpyint @@ -1 +1 @@ -Subproject commit 15a74ca84478e597d844e4a95f9cfd584f01345c +Subproject commit 9b4996552ef7ffa50628910b4b22630bd7730591 diff --git a/lib/mysql/connector/__init__.py b/lib/mysql/connector/__init__.py index 3161cece..a0e68008 100644 --- a/lib/mysql/connector/__init__.py +++ b/lib/mysql/connector/__init__.py @@ -139,6 +139,9 @@ def connect(*args, **kwargs): raise InterfaceError("fabric and failover arguments can not be used") if 'fabric' in kwargs: + if 'pool_name' in kwargs: + raise AttributeError("'pool_name' argument is not supported with " + " MySQL Fabric. Use 'pool_size' instead.") from .fabric import connect as fabric_connect return fabric_connect(*args, **kwargs) diff --git a/lib/mysql/connector/connection.py b/lib/mysql/connector/connection.py index 79f64e71..5e680082 100644 --- a/lib/mysql/connector/connection.py +++ b/lib/mysql/connector/connection.py @@ -123,6 +123,7 @@ def __init__(self, *args, **kwargs): self._ssl_active = False self._auth_plugin = None self._pool_config_version = None + self._compress = False if len(kwargs) > 0: self.connect(**kwargs) @@ -271,6 +272,7 @@ def config(self, **kwargs): try: if config['compress']: + self._compress = True self.set_client_flags([ClientFlag.COMPRESS]) except KeyError: pass # Missing compress argument is OK @@ -884,6 +886,10 @@ def cmd_change_user(self, username='', password='', database='', if self.unread_result: raise errors.InternalError("Unread result found.") + if self._compress: + raise errors.NotSupportedError("Change user is not supported with " + "compression.") + packet = self._protocol.make_change_user( handshake=self._handshake, username=username, password=password, database=database, @@ -943,8 +949,13 @@ def reset_session(self, user_variables=None, session_variables=None): try: self.cmd_reset_connection() except errors.NotSupportedError: - self.cmd_change_user(self._user, self._password, - self._database, self._charset_id) + if self._compress: + raise errors.NotSupportedError( + "Reset session is not supported with compression for " + "MySQL server version 5.7.2 or earlier.") + else: + self.cmd_change_user(self._user, self._password, + self._database, self._charset_id) cur = self.cursor() if user_variables: @@ -1135,7 +1146,7 @@ def python_charset(self): Returns a string. """ encoding = CharacterSet.get_info(self._charset_id)[0] - if encoding == 'utf8mb4': + if encoding in ('utf8mb4', 'binary'): return 'utf8' else: return encoding diff --git a/lib/mysql/connector/conversion.py b/lib/mysql/connector/conversion.py index 4f577fd8..59d41331 100644 --- a/lib/mysql/connector/conversion.py +++ b/lib/mysql/connector/conversion.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -66,7 +66,11 @@ def set_unicode(self, value=True): def to_mysql(self, value): """Convert Python data type to MySQL""" - return value + type_name = value.__class__.__name__.lower() + try: + return getattr(self, "_{0}_to_mysql".format(type_name))(value) + except AttributeError: + return value def to_python(self, vtype, value): """Convert MySQL data type to Python""" @@ -178,10 +182,15 @@ def _str_to_mysql(self, value): def _unicode_to_mysql(self, value): """Convert unicode""" - encoded = value.encode(self.charset) - if self.charset_id in CharacterSet.slash_charsets: + charset = self.charset + charset_id = self.charset_id + if charset == 'binary': + charset = 'utf8' + charset_id = CharacterSet.get_charset_info(charset)[0] + encoded = value.encode(charset) + if charset_id in CharacterSet.slash_charsets: if b'\x5c' in encoded: - return HexLiteral(value, self.charset) + return HexLiteral(value, charset) return encoded def _bytes_to_mysql(self, value): @@ -517,6 +526,8 @@ def _SET_to_python(self, value, dsc=None): # pylint: disable=C0103 """ set_type = None val = value.decode(self.charset) + if not val: + return set() try: set_type = set(val.split(',')) except ValueError: @@ -537,6 +548,8 @@ def _STRING_to_python(self, value, dsc=None): # pylint: disable=C0103 if dsc[7] & FieldFlag.BINARY: return value + if self.charset == 'binary': + return value if isinstance(value, (bytes, bytearray)) and self.use_unicode: return value.decode(self.charset) diff --git a/lib/mysql/connector/cursor.py b/lib/mysql/connector/cursor.py index 8a0602cc..b9bfd9a1 100644 --- a/lib/mysql/connector/cursor.py +++ b/lib/mysql/connector/cursor.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -29,6 +29,7 @@ import weakref from . import errors +from .catch23 import PY2 SQL_COMMENT = r"\/\*.*?\*\/" RE_SQL_COMMENT = re.compile( @@ -42,6 +43,14 @@ re.I | re.M | re.S) RE_SQL_INSERT_VALUES = re.compile(r'.*VALUES\s*(\(.*\)).*', re.I | re.M | re.S) RE_PY_PARAM = re.compile(b'(%s)') +RE_PY_MAPPING_PARAM = re.compile( + br''' + % + \((?P[^)]+)\) + (?P[diouxXeEfFgGcrs%]) + ''', + re.X +) RE_SQL_SPLIT_STMTS = re.compile( b''';(?=(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)''') RE_SQL_FIND_PARAM = re.compile( @@ -73,6 +82,35 @@ def remaining(self): return len(self.params) - self.index +def _bytestr_format_dict(bytestr, value_dict): + """ + >>> _bytestr_format_dict(b'%(a)s', {b'a': b'foobar'}) + b'foobar + >>> _bytestr_format_dict(b'%%(a)s', {b'a': b'foobar'}) + b'%%(a)s' + >>> _bytestr_format_dict(b'%%%(a)s', {b'a': b'foobar'}) + b'%%foobar' + >>> _bytestr_format_dict(b'%(x)s %(y)s', + ... {b'x': b'x=%(y)s', b'y': b'y=%(x)s'}) + b'x=%(y)s y=%(x)s' + """ + def replace(matchobj): + value = None + groups = matchobj.groupdict() + if groups["conversion_type"] == b"%": + value = b"%" + if groups["conversion_type"] == b"s": + key = groups["mapping_key"].encode("utf-8") \ + if PY2 else groups["mapping_key"] + value = value_dict[key] + if value is None: + raise ValueError("Unsupported conversion_type: {0}" + "".format(groups["conversion_type"])) + return value.decode("utf-8") if PY2 else value + return RE_PY_MAPPING_PARAM.sub(replace, bytestr.decode("utf-8") + if PY2 else bytestr) + + class CursorBase(object): """ Base for defining MySQLCursor. This class is a skeleton and defines @@ -355,7 +393,10 @@ def _process_params_dict(self, params): conv = to_mysql(conv) conv = escape(conv) conv = quote(conv) - res["%({0})s".format(key).encode()] = conv + if PY2: + res[key] = conv + else: + res[key.encode()] = conv except Exception as err: raise errors.ProgrammingError( "Failed processing pyformat-parameters; %s" % err) @@ -488,8 +529,8 @@ def execute(self, operation, params=None, multi=False): if params is not None: if isinstance(params, dict): - for key, value in self._process_params_dict(params).items(): - stmt = stmt.replace(key, value) + stmt = _bytestr_format_dict( + stmt, self._process_params_dict(params)) elif isinstance(params, (list, tuple)): psub = _ParamSubstitutor(self._process_params(params)) stmt = RE_PY_PARAM.sub(psub, stmt) @@ -543,8 +584,8 @@ def remove_comments(match): for params in seq_params: tmp = fmt if isinstance(params, dict): - for key, value in self._process_params_dict(params).items(): - tmp = tmp.replace(key, value) + tmp = _bytestr_format_dict( + tmp, self._process_params_dict(params)) else: psub = _ParamSubstitutor(self._process_params(params)) tmp = RE_PY_PARAM.sub(psub, tmp) @@ -697,12 +738,14 @@ def callproc(self, procname, args=()): call = "CALL {0}({1})".format(procname, ','.join(argnames)) for result in self._connection.cmd_query_iter(call): + # pylint: disable=W0212 + tmp = MySQLCursorBuffered(self._connection._get_self()) + tmp._handle_result(result) + if tmp._warnings is not None: + self._warnings = tmp._warnings + # pylint: enable=W0212 if 'columns' in result: - # pylint: disable=W0212 - tmp = MySQLCursorBuffered(self._connection._get_self()) - tmp._handle_result(result) results.append(tmp) - # pylint: enable=W0212 if argnames: select = "SELECT {0}".format(','.join(argtypes)) diff --git a/lib/mysql/connector/dbapi.py b/lib/mysql/connector/dbapi.py index aaf7e81f..35cb03b9 100644 --- a/lib/mysql/connector/dbapi.py +++ b/lib/mysql/connector/dbapi.py @@ -36,18 +36,22 @@ from . import constants -class _DBAPITypeObject: +class _DBAPITypeObject(object): def __init__(self, *values): self.values = values - def __cmp__(self, other): + def __eq__(self, other): if other in self.values: - return 0 - if other < self.values: - return 1 + return True else: - return -1 + return False + + def __ne__(self, other): + if other in self.values: + return False + else: + return True Date = datetime.date Time = datetime.time @@ -64,8 +68,8 @@ def TimestampFromTicks(ticks): Binary = bytes -STRING = _DBAPITypeObject(constants.FieldType.get_string_types()) -BINARY = _DBAPITypeObject(constants.FieldType.get_binary_types()) -NUMBER = _DBAPITypeObject(constants.FieldType.get_number_types()) -DATETIME = _DBAPITypeObject(constants.FieldType.get_timestamp_types()) +STRING = _DBAPITypeObject(*constants.FieldType.get_string_types()) +BINARY = _DBAPITypeObject(*constants.FieldType.get_binary_types()) +NUMBER = _DBAPITypeObject(*constants.FieldType.get_number_types()) +DATETIME = _DBAPITypeObject(*constants.FieldType.get_timestamp_types()) ROWID = _DBAPITypeObject() diff --git a/lib/mysql/connector/django/base.py b/lib/mysql/connector/django/base.py index 573c30bd..3791f6ed 100644 --- a/lib/mysql/connector/django/base.py +++ b/lib/mysql/connector/django/base.py @@ -92,10 +92,18 @@ def _DATETIME_to_python(self, value, dsc=None): if not value: return None dt = MySQLConverter._DATETIME_to_python(self, value) + if dt is None: + return None if settings.USE_TZ and timezone.is_naive(dt): dt = dt.replace(tzinfo=timezone.utc) return dt + def _safetext_to_mysql(self, value): + return self._str_to_mysql(value) + + def _safebytes_to_mysql(self, value): + return self._bytes_to_mysql(value) + class CursorWrapper(object): """Wrapper around MySQL Connector/Python's cursor class. @@ -190,7 +198,7 @@ def __init__(self, connection): self.supports_microsecond_precision = self._microseconds_precision() def _microseconds_precision(self): - if self.connection.server_version >= (5, 6, 3): + if self.connection.mysql_version >= (5, 6, 3): return True return False @@ -210,7 +218,7 @@ def _mysql_storage_engine(self): cursor.execute(droptable) cursor.execute('CREATE TABLE {table} (X INT)'.format(table=tblname)) - if self.connection.server_version >= (5, 0, 0): + if self.connection.mysql_version >= (5, 0, 0): cursor.execute( "SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES " "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", @@ -397,7 +405,7 @@ def sequence_reset_by_name_sql(self, style, sequences): # Truncate already resets the AUTO_INCREMENT field from # MySQL version 5.0.13 onwards. Refs #16961. res = [] - if self.connection.server_version < (5, 0, 13): + if self.connection.mysql_version < (5, 0, 13): fmt = "{alter} {table} {{tablename}} {auto_inc} {field};".format( alter=style.SQL_KEYWORD('ALTER'), table=style.SQL_KEYWORD('TABLE'), @@ -429,13 +437,7 @@ def value_to_db_datetime(self, value): "MySQL backend does not support timezone-aware times." ) - try: - # Django 1.6 - self.connection.ensure_connection() - except AttributeError: - if not self.connection.connection: - self.connection._connect() - return self.connection.connection.converter._datetime_to_mysql(value) + return self.connection.converter.to_mysql(value) def value_to_db_time(self, value): if value is None: @@ -446,13 +448,7 @@ def value_to_db_time(self, value): raise ValueError("MySQL backend does not support timezone-aware " "times.") - try: - # Django 1.6 - self.connection.ensure_connection() - except AttributeError: - if not self.connection.connection: - self.connection._connect() - return self.connection.connection.converter._time_to_mysql(value) + return self.connection.converter.to_mysql(value) def year_lookup_bounds(self, value): # Again, no microseconds @@ -465,7 +461,7 @@ def year_lookup_bounds_for_datetime_field(self, value): # Again, no microseconds first, second = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value) - if self.connection.server_version >= (5, 6, 4): + if self.connection.mysql_version >= (5, 6, 4): return [first.replace(microsecond=0), second] else: return [first.replace(microsecond=0), @@ -520,15 +516,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) - self.server_version = None - - # Since some features depend on the MySQL version, we need to connect - try: - # Django 1.6 - self.ensure_connection() - except AttributeError: - self._connect() + self.converter = DjangoMySQLConverter() self.ops = DatabaseOperations(self) self.features = DatabaseFeatures(self) self.client = DatabaseClient(self) @@ -582,15 +571,13 @@ def get_connection_params(self): def get_new_connection(self, conn_params): # Django 1.6 cnx = mysql.connector.connect(**conn_params) - self.server_version = cnx.get_server_version() cnx.set_converter_class(DjangoMySQLConverter) - connection_created.send(sender=self.__class__, connection=self) return cnx def init_connection_state(self): # Django 1.6 - if self.server_version < (5, 5, 3): + if self.mysql_version < (5, 5, 3): # See sysvar_sql_auto_is_null in MySQL Reference manual self.connection.cmd_query("SET SQL_AUTO_IS_NULL = 0") @@ -609,6 +596,7 @@ def create_cursor(self): def _connect(self): """Setup the connection with MySQL""" self.connection = self.get_new_connection(self.get_connection_params()) + connection_created.send(sender=self.__class__, connection=self) self.init_connection_state() def _cursor(self): @@ -735,6 +723,11 @@ def is_usable(self): # Django 1.6 return self.connection.is_connected() - @property + @cached_property def mysql_version(self): - return self.server_version + config = self.get_connection_params() + temp_conn = mysql.connector.connect(**config) + server_version = temp_conn.get_server_version() + temp_conn.close() + + return server_version diff --git a/lib/mysql/connector/django/creation.py b/lib/mysql/connector/django/creation.py index 4d4df1bf..f34e54bd 100644 --- a/lib/mysql/connector/django/creation.py +++ b/lib/mysql/connector/django/creation.py @@ -30,6 +30,7 @@ def __init__(self, connection): 'IntegerField': 'integer', 'BigIntegerField': 'bigint', 'IPAddressField': 'char(15)', + 'GenericIPAddressField': 'char(39)', 'NullBooleanField': 'bool', 'OneToOneField': 'integer', @@ -42,7 +43,7 @@ def __init__(self, connection): } # Support for microseconds - if self.connection.get_server_version() >= (5, 6, 4): + if self.connection.mysql_version >= (5, 6, 4): self.data_types.update({ 'DateTimeField': 'datetime(6)', 'TimeField': 'time(6)', diff --git a/lib/mysql/connector/errorcode.py b/lib/mysql/connector/errorcode.py index 756f8e7a..03814824 100644 --- a/lib/mysql/connector/errorcode.py +++ b/lib/mysql/connector/errorcode.py @@ -24,8 +24,8 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # This file was auto-generated. -_GENERATED_ON = '2014-05-23' -_MYSQL_VERSION = (5, 7, 4) +_GENERATED_ON = '2014-10-10' +_MYSQL_VERSION = (5, 7, 5) """This module contains the MySQL Server and Client error codes""" @@ -649,7 +649,7 @@ ER_DELAYED_NOT_SUPPORTED = 1616 WARN_NO_MASTER_INFO = 1617 WARN_OPTION_IGNORED = 1618 -WARN_PLUGIN_DELETE_BUILTIN = 1619 +ER_PLUGIN_DELETE_BUILTIN = 1619 WARN_PLUGIN_BUSY = 1620 ER_VARIABLE_IS_READONLY = 1621 ER_WARN_ENGINE_TRANSACTION_ROLLBACK = 1622 @@ -913,34 +913,82 @@ ER_OLD_TEMPORALS_UPGRADED = 1880 ER_INNODB_FORCED_RECOVERY = 1881 ER_AES_INVALID_IV = 1882 -ER_FILE_CORRUPT = 1883 -ER_ERROR_ON_MASTER = 1884 -ER_INCONSISTENT_ERROR = 1885 -ER_STORAGE_ENGINE_NOT_LOADED = 1886 -ER_GET_STACKED_DA_WITHOUT_ACTIVE_HANDLER = 1887 -ER_WARN_LEGACY_SYNTAX_CONVERTED = 1888 -ER_BINLOG_UNSAFE_FULLTEXT_PLUGIN = 1889 -ER_CANNOT_DISCARD_TEMPORARY_TABLE = 1890 -ER_FK_DEPTH_EXCEEDED = 1891 -ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE_V2 = 1892 -ER_WARN_TRIGGER_DOESNT_HAVE_CREATED = 1893 -ER_REFERENCED_TRG_DOES_NOT_EXIST = 1894 -ER_EXPLAIN_NOT_SUPPORTED = 1895 -ER_INVALID_FIELD_SIZE = 1896 -ER_MISSING_HA_CREATE_OPTION = 1897 -ER_ENGINE_OUT_OF_MEMORY = 1898 -ER_PASSWORD_EXPIRE_ANONYMOUS_USER = 1899 -ER_SLAVE_SQL_THREAD_MUST_STOP = 1900 -ER_NO_FT_MATERIALIZED_SUBQUERY = 1901 -ER_INNODB_UNDO_LOG_FULL = 1902 -ER_INVALID_ARGUMENT_FOR_LOGARITHM = 1903 -ER_SLAVE_IO_THREAD_MUST_STOP = 1904 -ER_WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO = 1905 -ER_WARN_ONLY_MASTER_LOG_FILE_NO_POS = 1906 -ER_QUERY_TIMEOUT = 1907 -ER_NON_RO_SELECT_DISABLE_TIMER = 1908 -ER_DUP_LIST_ENTRY = 1909 -ER_SQL_MODE_NO_EFFECT = 1910 +ER_PLUGIN_CANNOT_BE_UNINSTALLED = 1883 +ER_GTID_UNSAFE_BINLOG_SPLITTABLE_STATEMENT_AND_GTID_GROUP = 1884 +ER_FILE_CORRUPT = 1885 +ER_ERROR_ON_MASTER = 1886 +ER_INCONSISTENT_ERROR = 1887 +ER_STORAGE_ENGINE_NOT_LOADED = 1888 +ER_GET_STACKED_DA_WITHOUT_ACTIVE_HANDLER = 1889 +ER_WARN_LEGACY_SYNTAX_CONVERTED = 1890 +ER_BINLOG_UNSAFE_FULLTEXT_PLUGIN = 1891 +ER_CANNOT_DISCARD_TEMPORARY_TABLE = 1892 +ER_FK_DEPTH_EXCEEDED = 1893 +ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE_V2 = 1894 +ER_WARN_TRIGGER_DOESNT_HAVE_CREATED = 1895 +ER_REFERENCED_TRG_DOES_NOT_EXIST = 1896 +ER_EXPLAIN_NOT_SUPPORTED = 1897 +ER_INVALID_FIELD_SIZE = 1898 +ER_MISSING_HA_CREATE_OPTION = 1899 +ER_ENGINE_OUT_OF_MEMORY = 1900 +ER_PASSWORD_EXPIRE_ANONYMOUS_USER = 1901 +ER_SLAVE_SQL_THREAD_MUST_STOP = 1902 +ER_NO_FT_MATERIALIZED_SUBQUERY = 1903 +ER_INNODB_UNDO_LOG_FULL = 1904 +ER_INVALID_ARGUMENT_FOR_LOGARITHM = 1905 +ER_SLAVE_IO_THREAD_MUST_STOP = 1906 +ER_WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO = 1907 +ER_WARN_ONLY_MASTER_LOG_FILE_NO_POS = 1908 +ER_QUERY_TIMEOUT = 1909 +ER_NON_RO_SELECT_DISABLE_TIMER = 1910 +ER_DUP_LIST_ENTRY = 1911 +ER_SQL_MODE_NO_EFFECT = 1912 +ER_AGGREGATE_ORDER_FOR_UNION = 1913 +ER_AGGREGATE_ORDER_NON_AGG_QUERY = 1914 +ER_SLAVE_WORKER_STOPPED_PREVIOUS_THD_ERROR = 1915 +ER_DONT_SUPPORT_SLAVE_PRESERVE_COMMIT_ORDER = 1916 +ER_SERVER_OFFLINE_MODE = 1917 +ER_GIS_DIFFERENT_SRIDS = 1918 +ER_GIS_UNSUPPORTED_ARGUMENT = 1919 +ER_GIS_UNKNOWN_ERROR = 1920 +ER_GIS_UNKNOWN_EXCEPTION = 1921 +ER_GIS_INVALID_DATA = 1922 +ER_BOOST_GEOMETRY_EMPTY_INPUT_EXCEPTION = 1923 +ER_BOOST_GEOMETRY_CENTROID_EXCEPTION = 1924 +ER_BOOST_GEOMETRY_OVERLAY_INVALID_INPUT_EXCEPTION = 1925 +ER_BOOST_GEOMETRY_TURN_INFO_EXCEPTION = 1926 +ER_BOOST_GEOMETRY_SELF_INTERSECTION_POINT_EXCEPTION = 1927 +ER_BOOST_GEOMETRY_UNKNOWN_EXCEPTION = 1928 +ER_STD_BAD_ALLOC_ERROR = 1929 +ER_STD_DOMAIN_ERROR = 1930 +ER_STD_LENGTH_ERROR = 1931 +ER_STD_INVALID_ARGUMENT = 1932 +ER_STD_OUT_OF_RANGE_ERROR = 1933 +ER_STD_OVERFLOW_ERROR = 1934 +ER_STD_RANGE_ERROR = 1935 +ER_STD_UNDERFLOW_ERROR = 1936 +ER_STD_LOGIC_ERROR = 1937 +ER_STD_RUNTIME_ERROR = 1938 +ER_STD_UNKNOWN_EXCEPTION = 1939 +ER_GIS_DATA_WRONG_ENDIANESS = 1940 +ER_CHANGE_MASTER_PASSWORD_LENGTH = 1941 +ER_USER_LOCK_WRONG_NAME = 1942 +ER_USER_LOCK_DEADLOCK = 1943 +ER_REPLACE_INACCESSIBLE_ROWS = 1944 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_GIS = 1945 +ER_ILLEGAL_USER_VAR = 1946 +ER_GTID_MODE_OFF = 1947 +ER_UNSUPPORTED_BY_REPLICATION_THREAD = 1948 +ER_INCORRECT_TYPE = 1949 +ER_FIELD_IN_ORDER_NOT_SELECT = 1950 +ER_AGGREGATE_IN_ORDER_NOT_SELECT = 1951 +ER_INVALID_RPL_WILD_TABLE_FILTER_PATTERN = 1952 +ER_NET_OK_PACKET_TOO_LARGE = 1953 +ER_INVALID_JSON_DATA = 1954 +ER_INVALID_GEOJSON_MISSING_MEMBER = 1955 +ER_INVALID_GEOJSON_WRONG_TYPE = 1956 +ER_INVALID_GEOJSON_UNSPECIFIED = 1957 +ER_DIMENSION_UNSUPPORTED = 1958 CR_UNKNOWN_ERROR = 2000 CR_SOCKET_CREATE_ERROR = 2001 CR_CONNECTION_ERROR = 2002 @@ -990,7 +1038,7 @@ CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046 CR_CONN_UNKNOW_PROTOCOL = 2047 CR_INVALID_CONN_HANDLE = 2048 -CR_SECURE_AUTH = 2049 +CR_UNUSED_1 = 2049 CR_FETCH_CANCELED = 2050 CR_NO_DATA = 2051 CR_NO_STMT_METADATA = 2052 diff --git a/lib/mysql/connector/errors.py b/lib/mysql/connector/errors.py index 0dff38c2..7368ffaf 100644 --- a/lib/mysql/connector/errors.py +++ b/lib/mysql/connector/errors.py @@ -26,6 +26,7 @@ from . import utils from .locales import get_client_error +from .catch23 import PY2 # _CUSTOM_ERROR_EXCEPTIONS holds custom exceptions and is ued by the # function custom_error_exception. _ERROR_EXCEPTIONS (at bottom of module) @@ -187,7 +188,7 @@ def __init__(self, msg=None, errno=None, values=None, sqlstate=None): if self.msg and self.errno != -1: fields = { 'errno': self.errno, - 'msg': self.msg + 'msg': self.msg.encode('utf8') if PY2 else self.msg } if self.sqlstate: fmt = '{errno} ({state}): {msg}' diff --git a/lib/mysql/connector/fabric/caching.py b/lib/mysql/connector/fabric/caching.py index ffcc11bd..78f8999c 100644 --- a/lib/mysql/connector/fabric/caching.py +++ b/lib/mysql/connector/fabric/caching.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -24,6 +24,7 @@ """Implementing caching mechanisms for MySQL Fabric""" +import bisect from datetime import datetime, timedelta from hashlib import sha1 import logging @@ -35,6 +36,26 @@ _CACHE_TTL = 1 * 60 # 1 minute +def insort_right_rev(alist, new_element, low=0, high=None): + """Similar to bisect.insort_right but for reverse sorted lists + + This code is similar to the Python code found in Lib/bisect.py. + We simply change the comparison from 'less than' to 'greater than'. + """ + + if low < 0: + raise ValueError('low must be non-negative') + if high is None: + high = len(alist) + while low < high: + middle = (low + high) // 2 + if new_element > alist[middle]: + high = middle + else: + low = middle + 1 + alist.insert(low, new_element) + + class CacheEntry(object): """Base class for MySQL Fabric cache entries""" @@ -83,6 +104,8 @@ def __init__(self, shard, version=None, fabric_uuid=None): fabric_uuid=fabric_uuid) self.partitioning = {} self._shard = shard + self.keys = [] + self.keys_reversed = [] if shard.key and shard.group: self.add_partition(shard.key, shard.group) @@ -107,6 +130,8 @@ def add_partition(self, key, group): )) elif self.shard_type == 'RANGE_STRING': pass + elif self.shard_type == "HASH": + pass else: raise ValueError("Unsupported sharding type {0}".format( self.shard_type @@ -115,6 +140,8 @@ def add_partition(self, key, group): 'group': group, } self.reset_ttl() + bisect.insort_right(self.keys, key) + insort_right_rev(self.keys_reversed, key) @classmethod def hash_index(cls, part1, part2=None): diff --git a/lib/mysql/connector/fabric/connection.py b/lib/mysql/connector/fabric/connection.py index ebdd4370..ffd20469 100644 --- a/lib/mysql/connector/fabric/connection.py +++ b/lib/mysql/connector/fabric/connection.py @@ -61,7 +61,7 @@ HAVE_SSL = True # pylint: enable=F0401,E0611 -from ..connection import MySQLConnection +import mysql.connector from ..pooling import MySQLConnectionPool from ..errors import ( Error, InterfaceError, NotSupportedError, MySQLFabricError, InternalError, @@ -753,36 +753,43 @@ def get_shard_server(self, tables, key, scope=SCOPE_LOCAL, mode=None): return self.get_group_server(entry.global_group, mode=mode) if entry.shard_type == 'RANGE': - partitions = sorted(entry.partitioning.keys()) - index = partitions[bisect(partitions, int(key)) - 1] + try: + range_key = int(key) + except ValueError: + raise ValueError("Key must be an integer for RANGE") + partitions = entry.keys + index = partitions[bisect(partitions, range_key) - 1] partition = entry.partitioning[index] elif entry.shard_type == 'RANGE_DATETIME': if not isinstance(key, (datetime.date, datetime.datetime)): raise ValueError( "Key must be datetime.date or datetime.datetime for " "RANGE_DATETIME") - partition_keys = sorted(entry.partitioning.keys(), reverse=True) - for partkey in partition_keys: + index = None + for partkey in entry.keys_reversed: if key >= partkey: index = partkey break - partition = entry.partitioning[index] + try: + partition = entry.partitioning[index] + except KeyError: + raise ValueError("Key invalid; was '{0}'".format(key)) elif entry.shard_type == 'RANGE_STRING': if not isunicode(key): raise ValueError("Key must be a unicode value") - partition_keys = sorted(entry.partitioning.keys(), reverse=True) - for partkey in partition_keys: - size = len(partkey) - if key[0:size] >= partkey[0:size]: + index = None + for partkey in entry.keys_reversed: + if key >= partkey: index = partkey break - partition = entry.partitioning[index] + try: + partition = entry.partitioning[index] + except KeyError: + raise ValueError("Key invalid; was '{0}'".format(key)) elif entry.shard_type == 'HASH': md5key = md5(str(key)) - partition_keys = sorted( - entry.partitioning.keys(), reverse=True) - index = partition_keys[-1] - for partkey in partition_keys: + index = entry.keys_reversed[-1] + for partkey in entry.keys_reversed: if md5key.digest() >= b16decode(partkey): index = partkey break @@ -1116,6 +1123,8 @@ def store_config(self, **kwargs): del test_config['pool_name'] if 'pool_size' in test_config: del test_config['pool_size'] + if 'pool_reset_session' in test_config: + del test_config['pool_reset_session'] try: pool = MySQLConnectionPool(pool_name=str(uuid.uuid4())) pool.set_config(**test_config) @@ -1179,7 +1188,7 @@ def _connect(self): dbconfig['host'] = mysqlserver.host dbconfig['port'] = mysqlserver.port try: - self._mysql_cnx = MySQLConnection(**dbconfig) + self._mysql_cnx = mysql.connector.connect(**dbconfig) except Error as exc: if counter == attempts: self.reset_cache(mysqlserver.group) diff --git a/lib/mysql/connector/locales/eng/client_error.py b/lib/mysql/connector/locales/eng/client_error.py index 6aaa42d7..c448d034 100644 --- a/lib/mysql/connector/locales/eng/client_error.py +++ b/lib/mysql/connector/locales/eng/client_error.py @@ -24,8 +24,8 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # This file was auto-generated. -_GENERATED_ON = '2014-05-23' -_MYSQL_VERSION = (5, 7, 4) +_GENERATED_ON = '2014-10-10' +_MYSQL_VERSION = (5, 7, 5) # Start MySQL Error messages CR_UNKNOWN_ERROR = u"Unknown MySQL error" @@ -77,7 +77,7 @@ CR_SHARED_MEMORY_CONNECT_SET_ERROR = u"Can't open shared memory; cannot send request event to server (%s)" CR_CONN_UNKNOW_PROTOCOL = u"Wrong or unknown protocol" CR_INVALID_CONN_HANDLE = u"Invalid connection handle" -CR_SECURE_AUTH = u"Connection using old (pre-4.1.1) authentication protocol refused (client option 'secure_auth' enabled)" +CR_UNUSED_1 = u"Connection using old (pre-4.1.1) authentication protocol refused (client option 'secure_auth' enabled)" CR_FETCH_CANCELED = u"Row retrieval was canceled by mysql_stmt_close() call" CR_NO_DATA = u"Attempt to read column without prior row fetch" CR_NO_STMT_METADATA = u"Prepared statement contains no metadata" diff --git a/lib/mysql/connector/network.py b/lib/mysql/connector/network.py index b5b44f00..31f3f8cc 100644 --- a/lib/mysql/connector/network.py +++ b/lib/mysql/connector/network.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -220,10 +220,14 @@ def recv_plain(self): """Receive packets from the MySQL server""" try: # Read the header of the MySQL packet, 4 bytes - packet = bytearray(4) - read = self.sock.recv_into(packet, 4) - if read != 4: - raise errors.InterfaceError(errno=2013) + packet = bytearray(b'') + packet_len = 0 + while packet_len < 4: + chunk = self.sock.recv(4 - packet_len) + if not chunk: + raise errors.InterfaceError(errno=2013) + packet += chunk + packet_len = len(packet) # Save the packet number and payload length self._packet_number = packet[3] @@ -255,12 +259,13 @@ def recv_py26_plain(self): try: # Read the header of the MySQL packet, 4 bytes header = bytearray(b'') - - while len(header) < 4: - chunk = self.sock.recv(4) + header_len = 0 + while header_len < 4: + chunk = self.sock.recv(4 - header_len) if not chunk: raise errors.InterfaceError(errno=2013) header += chunk + header_len = len(header) # Save the packet number and payload length self._packet_number = header[3] @@ -435,7 +440,7 @@ def open_connection(self): """Open the TCP/IP connection to the MySQL server """ # Get address information - addrinfo = None + addrinfo = [None] * 5 try: addrinfos = socket.getaddrinfo(self.server_host, self.server_port, @@ -449,10 +454,10 @@ def open_connection(self): elif info[0] == socket.AF_INET: addrinfo = info break - if self.force_ipv6 and not addrinfo: + if self.force_ipv6 and addrinfo[0] is None: raise errors.InterfaceError( "No IPv6 address found for {0}".format(self.server_host)) - if not addrinfo: + if addrinfo[0] is None: addrinfo = addrinfos[0] except IOError as err: raise errors.InterfaceError( diff --git a/lib/mysql/connector/optionfiles.py b/lib/mysql/connector/optionfiles.py index 6db43fd6..e4a0366c 100644 --- a/lib/mysql/connector/optionfiles.py +++ b/lib/mysql/connector/optionfiles.py @@ -103,10 +103,14 @@ def read_option_files(**config): except KeyError: continue + not_evaluate = ('password', 'passwd') for option, value in config_options.items(): if option not in config: try: - config[option] = eval(value[0]) # pylint: disable=W0123 + if option in not_evaluate: + config[option] = value[0] + else: + config[option] = eval(value[0]) # pylint: disable=W0123 except (NameError, SyntaxError): config[option] = value[0] diff --git a/lib/mysql/connector/pooling.py b/lib/mysql/connector/pooling.py index 579a3af4..8937662b 100644 --- a/lib/mysql/connector/pooling.py +++ b/lib/mysql/connector/pooling.py @@ -269,6 +269,17 @@ def add_connection(self, cnx=None): if not cnx: cnx = MySQLConnection(**self._cnx_config) + try: + if (self._reset_session and self._cnx_config['compress'] + and cnx.get_server_version() < (5, 7, 3)): + raise errors.NotSupportedError("Pool reset session is " + "not supported with " + "compression for MySQL " + "server version 5.7.2 " + "or earlier.") + except KeyError: + pass + # pylint: disable=W0201,W0212 cnx._pool_config_version = self._config_version # pylint: enable=W0201,W0212 diff --git a/lib/mysql/connector/protocol.py b/lib/mysql/connector/protocol.py index 4ced446f..79d41a40 100644 --- a/lib/mysql/connector/protocol.py +++ b/lib/mysql/connector/protocol.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -313,11 +313,8 @@ def read_text_result(self, sock, count=1): while packet.startswith(b'\xff\xff\xff'): datas.append(packet[4:]) packet = sock.recv() - if packet[4] == 254: - eof = self.parse_eof(packet) - else: - datas.append(packet[4:]) - rowdata = utils.read_lc_string_list(b''.join(datas)) + datas.append(packet[4:]) + rowdata = utils.read_lc_string_list(bytearray(b'').join(datas)) elif packet[4] == 254: eof = self.parse_eof(packet) rowdata = None @@ -636,24 +633,25 @@ def make_stmt_execute(self, statement_id, data=(), parameters=(), values.append(packed) elif isinstance(value, str): if PY2: - values.append(utils.intstore(len(value)) + value) + values.append(utils.lc_int(len(value)) + + value) else: value = value.encode(charset) values.append( - utils.intstore(len(value)) + value) + utils.lc_int(len(value)) + value) field_type = FieldType.VARCHAR elif isinstance(value, bytes): - values.append(utils.intstore(len(value)) + value) + values.append(utils.lc_int(len(value)) + value) field_type = FieldType.BLOB elif PY2 and \ isinstance(value, unicode): # pylint: disable=E0602 value = value.encode(charset) - values.append(utils.intstore(len(value)) + value) + values.append(utils.lc_int(len(value)) + value) field_type = FieldType.VARCHAR elif isinstance(value, Decimal): values.append( - utils.intstore(len(str(value).encode(charset))) + - str(value).encode(charset)) + utils.lc_int(len(str(value).encode( + charset))) + str(value).encode(charset)) field_type = FieldType.DECIMAL elif isinstance(value, float): values.append(struct.pack('d', value)) diff --git a/lib/mysql/connector/utils.py b/lib/mysql/connector/utils.py index 586b422e..acbde809 100644 --- a/lib/mysql/connector/utils.py +++ b/lib/mysql/connector/utils.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -100,12 +100,12 @@ def int4store(i): def int8store(i): """ - Takes an unsigned integer (4 bytes) and packs it as string. + Takes an unsigned integer (8 bytes) and packs it as string. Returns string. """ if i < 0 or i > 18446744073709551616: - raise ValueError('int4store requires 0 <= i <= 2^64') + raise ValueError('int8store requires 0 <= i <= 2^64') else: return bytearray(struct.pack(' 18446744073709551616: + raise ValueError('Requires 0 <= i <= 2^64') + + if i < 251: + return bytearray(struct.pack(', like most @@ -26,7 +26,7 @@ as mysql.connector.version. """ -VERSION = (2, 0, 1, '', 0) +VERSION = (2, 0, 5, '', 0) if VERSION[3] and VERSION[4]: VERSION_TEXT = '{0}.{1}.{2}{3}{4}'.format(*VERSION) diff --git a/tests/data/option_files/my.cnf b/tests/data/option_files/my.cnf index afafa4b7..8c27afcd 100644 --- a/tests/data/option_files/my.cnf +++ b/tests/data/option_files/my.cnf @@ -1,5 +1,6 @@ [client] +password=12345 port=1000 socket=/var/run/mysqld/mysqld.sock ssl-ca=dummyCA diff --git a/tests/mysqld.py b/tests/mysqld.py index 4e61d3df..6a00aa80 100644 --- a/tests/mysqld.py +++ b/tests/mysqld.py @@ -419,6 +419,16 @@ def bootstrap(self): "CREATE DATABASE myconnpy;" ] + if self._version[0:3] >= (5, 7, 5): + # MySQL 5.7.5 creates no user while bootstrapping + extra_sql.append( + "INSERT INTO mysql.user VALUES ('localhost','root',''," + "'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'," + "'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'," + "'Y','Y','Y','Y','Y','','','','',0,0,0,0," + "@@default_authentication_plugin,'','N'," + "CURRENT_TIMESTAMP,NULL);" + ) if self._version[0:3] >= (5, 7, 4): # MySQL 5.7.4 only creates root@localhost extra_sql.append( diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 3efa41b0..081b6414 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -2305,6 +2305,9 @@ def test_parameters(self): err) +@unittest.skipIf(tests.MYSQL_VERSION >= (5, 7, 5), + "MySQL {0} does not support old password auth".format( + tests.MYSQL_VERSION_TXT)) class BugOra18415927(tests.MySQLConnectorTests): """BUG#18415927: AUTH_RESPONSE VARIABLE INCREMENTED WITHOUT BEING DEFINED """ @@ -2867,3 +2870,382 @@ def test_option_files_with_include(self): os.remove(temp_cnf_file) os.remove(temp_include_file) + + +class BugOra19584051(tests.MySQLConnectorTests): + """BUG#19584051: TYPE_CODE DOES NOT COMPARE EQUAL + """ + def setUp(self): + config = tests.get_mysql_config() + self.cnx = connection.MySQLConnection(**config) + self.cursor = self.cnx.cursor() + + self.tbl = 'Bug19584051' + self.cursor.execute("DROP TABLE IF EXISTS %s" % self.tbl) + + create = ('CREATE TABLE {0}(col1 INT NOT NULL, col2 BLOB, ' + 'col3 VARCHAR(10), col4 DECIMAL(4,2), ' + 'col5 DATETIME , col6 YEAR, ' + 'PRIMARY KEY(col1))'.format(self.tbl)) + + self.cursor.execute(create) + + def tearDown(self): + self.cursor.execute("DROP TABLE IF EXISTS %s" % self.tbl) + self.cursor.close() + self.cnx.close() + + def test_dbapi(self): + cur = self.cnx.cursor() + sql = ("INSERT INTO {0}(col1, col2, col3, col4, col5, col6) " + "VALUES (%s, %s, %s, %s, %s, %s)".format(self.tbl)) + params = (100, 'blob-data', 'foo', 1.2, datetime(2014, 8, 4, 9, 11, 14), + 2014) + + exp = [ + mysql.connector.NUMBER, + mysql.connector.BINARY, + mysql.connector.STRING, + mysql.connector.NUMBER, + mysql.connector.DATETIME, + mysql.connector.NUMBER, + ] + cur.execute(sql, params) + + sql = "SELECT * FROM {0}".format(self.tbl) + cur.execute(sql) + temp = cur.fetchone() + type_codes = [row[1] for row in cur.description] + self.assertEqual(exp, type_codes) + cur.close() + + +class BugOra19522948(tests.MySQLConnectorTests): + """BUG#19522948: DATA CORRUPTION WITH TEXT FIELDS + """ + def setUp(self): + config = tests.get_mysql_config() + self.cnx = connection.MySQLConnection(**config) + self.cur = self.cnx.cursor() + + self.tbl = 'Bug19522948' + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = "CREATE TABLE {0} (c1 LONGTEXT NOT NULL)".format( + self.tbl + ) + self.cur.execute(create) + + def tearDown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + self.cur.close() + self.cnx.close() + + def test_row_to_python(self): + cur = self.cnx.cursor(prepared=True) + + data = "test_data"*10 + cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,)) + self.cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual((data,), self.cur.fetchone()) + self.cur.execute("TRUNCATE TABLE {0}".format(self.tbl)) + + data = "test_data"*1000 + cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,)) + self.cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual((data,), self.cur.fetchone()) + self.cur.execute("TRUNCATE TABLE {0}".format(self.tbl)) + + data = "test_data"*10000 + cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,)) + self.cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual((data,), self.cur.fetchone()) + + +class BugOra19500097(tests.MySQLConnectorTests): + """BUG#19500097: BETTER SUPPORT FOR RAW/BINARY DATA + """ + def setUp(self): + config = tests.get_mysql_config() + self.cnx = connection.MySQLConnection(**config) + self.cur = self.cnx.cursor() + + self.tbl = 'Bug19500097' + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = ("CREATE TABLE {0} (col1 VARCHAR(10), col2 INT) " + "DEFAULT CHARSET latin1".format(self.tbl)) + self.cur.execute(create) + + def tearDown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + self.cur.close() + self.cnx.close() + + def test_binary_charset(self): + + sql = "INSERT INTO {0} VALUES(%s, %s)".format(self.tbl) + self.cur.execute(sql, ('foo', 1)) + self.cur.execute(sql, ('ëëë', 2)) + self.cur.execute(sql, (u'ááá', 5)) + + self.cnx.set_charset_collation('binary') + self.cur.execute(sql, ('bar', 3)) + self.cur.execute(sql, ('ëëë', 4)) + self.cur.execute(sql, (u'ááá', 6)) + + exp = [ + (bytearray(b'foo'), 1), + (bytearray(b'\xeb\xeb\xeb'), 2), + (bytearray(b'\xe1\xe1\xe1'), 5), + (bytearray(b'bar'), 3), + (bytearray(b'\xc3\xab\xc3\xab\xc3\xab'), 4), + (bytearray(b'\xc3\xa1\xc3\xa1\xc3\xa1'), 6) + ] + + self.cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual(exp, self.cur.fetchall()) + + +@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 3), + "MySQL {0} does not support COM_RESET_CONNECTION".format( + tests.MYSQL_VERSION_TXT)) +class BugOra19549363(tests.MySQLConnectorTests): + """BUG#19549363: Compression does not work with Change User + """ + + def tearDown(self): + mysql.connector._CONNECTION_POOLS = {} + + def test_compress(self): + config = tests.get_mysql_config() + config['compress'] = True + + mysql.connector._CONNECTION_POOLS = {} + config['pool_name'] = 'mypool' + config['pool_size'] = 3 + config['pool_reset_session'] = True + cnx1 = mysql.connector.connect(**config) + + try: + cnx1.close() + except: + self.fail("Reset session with compression test failed.") + + +class BugOra19803702(tests.MySQLConnectorTests): + """BUG#19803702: CAN'T REPORT ERRORS THAT HAVE NON-ASCII CHARACTERS + """ + def test_errors(self): + config = tests.get_mysql_config() + self.cnx = connection.MySQLConnection(**config) + self.cur = self.cnx.cursor() + + self.tbl = 'áááëëëááá' + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = ("CREATE TABLE {0} (col1 VARCHAR(10), col2 INT) " + "DEFAULT CHARSET latin1".format(self.tbl)) + + self.cur.execute(create) + self.assertRaises(errors.DatabaseError, self.cur.execute, create) + + def tearDown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + self.cur.close() + self.cnx.close() + + +class BugOra19777815(tests.MySQLConnectorTests): + """BUG#19777815: CALLPROC() DOES NOT SUPPORT WARNINGS + """ + def setUp(self): + config = tests.get_mysql_config() + config['get_warnings'] = True + self.cnx = connection.MySQLConnection(**config) + cur = self.cnx.cursor() + self.sp1 = 'BUG19777815' + self.sp2 = 'BUG19777815_with_result' + create1 = ( + "CREATE PROCEDURE {0}() BEGIN SIGNAL SQLSTATE '01000' " + "SET MESSAGE_TEXT = 'TEST WARNING'; END;".format(self.sp1) + ) + create2 = ( + "CREATE PROCEDURE {0}() BEGIN SELECT 1; SIGNAL SQLSTATE '01000' " + "SET MESSAGE_TEXT = 'TEST WARNING'; END;".format(self.sp2) + ) + + cur.execute("DROP PROCEDURE IF EXISTS {0}".format(self.sp1)) + cur.execute("DROP PROCEDURE IF EXISTS {0}".format(self.sp2)) + cur.execute(create1) + cur.execute(create2) + cur.close() + + def tearDown(self): + cur = self.cnx.cursor() + cur.execute("DROP PROCEDURE IF EXISTS {0}".format(self.sp1)) + cur.execute("DROP PROCEDURE IF EXISTS {0}".format(self.sp2)) + cur.close() + self.cnx.close() + + def test_warning(self): + cur = self.cnx.cursor() + cur.callproc(self.sp1) + exp = [(u'Warning', 1642, u'TEST WARNING')] + self.assertEqual(exp, cur.fetchwarnings()) + cur.close() + + def test_warning_with_rows(self): + cur = self.cnx.cursor() + cur.callproc(self.sp2) + + exp = [(1,)] + try: + select_result = cur.stored_results().next() + except AttributeError: + # Python 3 + select_result = next(cur.stored_results()) + self.assertEqual(exp, select_result.fetchall()) + exp = [(u'Warning', 1642, u'TEST WARNING')] + self.assertEqual(exp, cur.fetchwarnings()) + cur.close() + + +class BugOra20407036(tests.MySQLConnectorTests): + """BUG#20407036: INCORRECT ARGUMENTS TO MYSQLD_STMT_EXECUTE ERROR + """ + def setUp(self): + config = tests.get_mysql_config() + self.cnx = connection.MySQLConnection(**config) + self.cur = self.cnx.cursor() + + self.tbl = 'Bug20407036' + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = ("CREATE TABLE {0} ( id int(10) unsigned NOT NULL, " + "text VARCHAR(70000) CHARACTER SET utf8 NOT NULL, " + "rooms tinyint(3) unsigned NOT NULL) " + "ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 " + "COLLATE=utf8_unicode_ci".format(self.tbl)) + self.cur.execute(create) + + def tearDown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + self.cur.close() + self.cnx.close() + + def test_binary_charset(self): + cur = self.cnx.cursor(prepared=True) + sql = "INSERT INTO {0}(text, rooms) VALUES(%s, %s)".format(self.tbl) + cur.execute(sql, ('a'*252, 1)) + cur.execute(sql, ('a'*253, 2)) + cur.execute(sql, ('a'*255, 3)) + cur.execute(sql, ('a'*251, 4)) + cur.execute(sql, ('a'*65535, 5)) + + exp = [ + (0, 'a'*252, 1), + (0, 'a'*253, 2), + (0, 'a'*255, 3), + (0, 'a'*251, 4), + (0, 'a'*65535, 5), + ] + + self.cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual(exp, self.cur.fetchall()) + + +class BugOra20301989(tests.MySQLConnectorTests): + """BUG#20301989: SET DATA TYPE NOT TRANSLATED CORRECTLY WHEN EMPTY + """ + def setUp(self): + config = tests.get_mysql_config() + cnx = connection.MySQLConnection(**config) + cur = cnx.cursor() + + self.tbl = 'Bug20301989' + cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = ("CREATE TABLE {0} (col1 SET('val1', 'val2')) " + "DEFAULT CHARSET latin1".format(self.tbl)) + cur.execute(create) + cur.close() + cnx.close() + + def tearDown(self): + config = tests.get_mysql_config() + cnx = connection.MySQLConnection(**config) + cur = cnx.cursor() + cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + cur.close() + cnx.close() + + def test_set(self): + config = tests.get_mysql_config() + cnx = connection.MySQLConnection(**config) + cur = cnx.cursor() + sql = "INSERT INTO {0} VALUES(%s)".format(self.tbl) + cur.execute(sql, ('val1,val2',)) + cur.execute(sql, ('val1',)) + cur.execute(sql, ('',)) + cur.execute(sql, (None,)) + + exp = [ + (set([u'val1', u'val2']),), + (set([u'val1']),), + (set([]),), + (None,) + ] + + cur.execute("SELECT * FROM {0}".format(self.tbl)) + self.assertEqual(exp, cur.fetchall()) + + +class BugOra20462427(tests.MySQLConnectorTests): + """BUG#20462427: BYTEARRAY INDEX OUT OF RANGE + """ + def setUp(self): + config = tests.get_mysql_config() + config['autocommit'] = True + config['connection_timeout'] = 100 + self.cnx = connection.MySQLConnection(**config) + self.cur = self.cnx.cursor() + + self.tbl = 'BugOra20462427' + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + + create = ("CREATE TABLE {0} (" + "id INT PRIMARY KEY, " + "a LONGTEXT " + ") ENGINE=Innodb DEFAULT CHARSET utf8".format(self.tbl)) + + self.cur.execute(create) + + def tearDown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl)) + self.cur.close() + self.cnx.close() + + def test_bigdata(self): + temp = 'a'*16777210 + insert = "INSERT INTO {0} (a) VALUES ('{1}')".format(self.tbl, temp) + + self.cur.execute(insert) + self.cur.execute("SELECT a FROM {0}".format(self.tbl)) + res = self.cur.fetchall() + self.assertEqual(16777210, len(res[0][0])) + + self.cur.execute("UPDATE {0} SET a = concat(a, 'a')".format(self.tbl)) + self.cur.execute("SELECT a FROM {0}".format(self.tbl)) + res = self.cur.fetchall() + self.assertEqual(16777211, len(res[0][0])) + + self.cur.execute("UPDATE {0} SET a = concat(a, 'a')".format(self.tbl)) + self.cur.execute("SELECT a FROM {0}".format(self.tbl)) + res = self.cur.fetchall() + self.assertEqual(16777212, len(res[0][0])) + + self.cur.execute("UPDATE {0} SET a = concat(a, 'a')".format(self.tbl)) + self.cur.execute("SELECT a FROM {0}".format(self.tbl)) + res = self.cur.fetchall() + self.assertEqual(16777213, len(res[0][0])) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index e07342c3..e2aef9f8 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -360,6 +360,8 @@ def test__process_params(self): datetime.time(20, 3, 23), st_now, datetime.timedelta(hours=40, minutes=30, seconds=12), + 'foo %(t)s', + 'foo %(s)s', ) exp = ( b'NULL', @@ -381,6 +383,8 @@ def test__process_params(self): b"'" + time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii') + b"'", b"'40:30:12'", + b"'foo %(t)s'", + b"'foo %(s)s'", ) self.cnx = connection.MySQLConnection(**tests.get_mysql_config()) @@ -420,28 +424,32 @@ def test__process_params_dict(self): 'p': datetime.time(20, 3, 23), 'q': st_now, 'r': datetime.timedelta(hours=40, minutes=30, seconds=12), + 's': 'foo %(t)s', + 't': 'foo %(s)s', } exp = { - b'%(a)s': b'NULL', - b'%(b)s': b'128', - b'%(c)s': b'1281288', - b'%(d)s': repr(float(3.14)) if PY2 else b'3.14', - b'%(e)s': b"'3.14'", - b'%(f)s': b"'back\\\\slash'", - b'%(g)s': b"'newline\\n'", - b'%(h)s': b"'return\\r'", - b'%(i)s': b"'\\'single\\''", - b'%(j)s': b'\'\\"double\\"\'', - b'%(k)s': b"'windows\\\x1a'", - b'%(l)s': b"'Strings are sexy'", - b'%(m)s': b"'\xe8\x8a\xb1'", - b'%(n)s': b"'2008-05-07 20:01:23'", - b'%(o)s': b"'2008-05-07'", - b'%(p)s': b"'20:03:23'", - b'%(q)s': b"'" + + b'a': b'NULL', + b'b': b'128', + b'c': b'1281288', + b'd': repr(float(3.14)) if PY2 else b'3.14', + b'e': b"'3.14'", + b'f': b"'back\\\\slash'", + b'g': b"'newline\\n'", + b'h': b"'return\\r'", + b'i': b"'\\'single\\''", + b'j': b'\'\\"double\\"\'', + b'k': b"'windows\\\x1a'", + b'l': b"'Strings are sexy'", + b'm': b"'\xe8\x8a\xb1'", + b'n': b"'2008-05-07 20:01:23'", + b'o': b"'2008-05-07'", + b'p': b"'20:03:23'", + b'q': b"'" + time.strftime('%Y-%m-%d %H:%M:%S', st_now).encode('ascii') + b"'", - b'%(r)s': b"'40:30:12'", + b'r': b"'40:30:12'", + b's': b"'foo %(t)s'", + b't': b"'foo %(s)s'", } self.cnx = connection.MySQLConnection(**tests.get_mysql_config()) diff --git a/tests/test_django.py b/tests/test_django.py index 77b59e51..364e2c01 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -26,6 +26,7 @@ import datetime import sys +import unittest import tests @@ -82,6 +83,8 @@ import django.db # pylint: disable=W0611 if tests.DJANGO_VERSION >= (1, 6): from django.db.backends import FieldInfo +from django.db.backends.signals import connection_created +from django.utils.safestring import SafeBytes, SafeText import mysql.connector from mysql.connector.django.base import (DatabaseWrapper, DatabaseOperations, @@ -196,7 +199,7 @@ def setUp(self): def test__init__(self): exp = self.conn.get_server_version() - self.assertEqual(exp, self.cnx.server_version) + self.assertEqual(exp, self.cnx.mysql_version) value = datetime.time(2, 5, 7) exp = self.conn.converter._time_to_mysql(value) @@ -207,6 +210,41 @@ def test__init__(self): exp = self.conn.converter._time_to_mysql(value) self.assertEqual(exp, self.cnx.ops.value_to_db_time(value)) + def test_signal(self): + from django.db import connection + + def conn_setup(*args, **kwargs): + conn = kwargs['connection'] + settings.DEBUG = True + cur = conn.cursor() + settings.DEBUG = False + cur.execute("SET @xyz=10") + cur.close() + + connection_created.connect(conn_setup) + cursor = connection.cursor() + cursor.execute("SELECT @xyz") + + self.assertEqual((10,), cursor.fetchone()) + cursor.close() + self.cnx.close() + + def count_conn(self, *args, **kwargs): + try: + self.connections += 1 + except AttributeError: + self.connection = 1 + + def test_connections(self): + connection_created.connect(self.count_conn) + self.connections = 0 + + # Checking if DatabaseWrapper object creates a connection by default + conn = DatabaseWrapper(settings.DATABASES['default']) + dbo = DatabaseOperations(conn) + dbo.value_to_db_time(datetime.time(3, 3, 3)) + self.assertEqual(self.connections, 0) + class DjangoDatabaseOperations(tests.MySQLConnectorTests): @@ -248,3 +286,38 @@ def test__TIME_to_python(self): django_converter = DjangoMySQLConverter() self.assertEqual(datetime.time(10, 11, 12), django_converter._TIME_to_python(value, dsc=None)) + + def test__DATETIME_to_python(self): + value = b'1990-11-12 00:00:00' + django_converter = DjangoMySQLConverter() + self.assertEqual(datetime.datetime(1990, 11, 12, 0, 0, 0), + django_converter._DATETIME_to_python(value, dsc=None)) + + settings.USE_TZ = True + value = b'0000-00-00 00:00:00' + django_converter = DjangoMySQLConverter() + self.assertEqual(None, + django_converter._DATETIME_to_python(value, dsc=None)) + settings.USE_TZ = False + + +class BugOra20106629(tests.MySQLConnectorTests): + """CONNECTOR/PYTHON DJANGO BACKEND DOESN'T SUPPORT SAFETEXT""" + def setUp(self): + dbconfig = tests.get_mysql_config() + self.conn = mysql.connector.connect(**dbconfig) + self.cnx = DatabaseWrapper(settings.DATABASES['default']) + self.cur = self.cnx.cursor() + self.tbl = "BugOra20106629" + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl), ()) + self.cur.execute("CREATE TABLE {0}(col1 TEXT, col2 BLOB)".format(self.tbl), ()) + + def teardown(self): + self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl), ()) + + def test_safe_string(self): + safe_text = SafeText("dummy & safe data ") + safe_bytes = SafeBytes(b"\x00\x00\x4c\x6e\x67\x39") + self.cur.execute("INSERT INTO {0} VALUES(%s, %s)".format(self.tbl), (safe_text, safe_bytes)) + self.cur.execute("SELECT * FROM {0}".format(self.tbl), ()) + self.assertEqual(self.cur.fetchall(), [(safe_text, safe_bytes)]) diff --git a/tests/test_errorcode.py b/tests/test_errorcode.py index d63531a1..829eacc8 100644 --- a/tests/test_errorcode.py +++ b/tests/test_errorcode.py @@ -31,19 +31,6 @@ class ErrorCodeTests(tests.MySQLConnectorTests): - def test__GENERATED_ON(self): - self.assertTrue(isinstance(errorcode._GENERATED_ON, str)) - try: - generatedon = datetime.strptime(errorcode._GENERATED_ON, - '%Y-%m-%d').date() - except ValueError as err: - self.fail(err) - - delta = (datetime.now().date() - generatedon).days - self.assertTrue( - delta < 120, - "errorcode.py is more than 120 days old ({0})".format(delta)) - def test__MYSQL_VERSION(self): minimum = (5, 6, 6) self.assertTrue(isinstance(errorcode._MYSQL_VERSION, tuple)) diff --git a/tests/test_fabric.py b/tests/test_fabric.py index fe175233..f0fd2433 100644 --- a/tests/test_fabric.py +++ b/tests/test_fabric.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -41,6 +41,7 @@ from mysql.connector import fabric, errorcode from mysql.connector.fabric import connection, balancing from mysql.connector.catch23 import UNICODE_TYPES, PY2 +from mysql.connector.pooling import PooledMySQLConnection ERR_NO_FABRIC_CONFIG = "Fabric configuration not available" @@ -424,6 +425,43 @@ def _truncate(self, cur, table): cur.execute("SELECT @@global.gtid_executed") return cur.fetchone()[0] + def test_range(self): + self.assertTrue(self._check_table("employees.employees_range", 'RANGE')) + tbl_name = "employees_range" + + tables = ["employees.{0}".format(tbl_name)] + + self.cnx.set_property(tables=tables, + scope=fabric.SCOPE_GLOBAL, + mode=fabric.MODE_READWRITE) + cur = self.cnx.cursor() + gtid_executed = self._truncate(cur, tbl_name) + self.cnx.commit() + + insert = ("INSERT INTO {0} " + "VALUES (%s, %s, %s, %s, %s, %s)").format(tbl_name) + + self._populate(self.cnx, gtid_executed, tbl_name, insert, + self.emp_data[1985] + self.emp_data[2000], 0) + + time.sleep(2) + + # Year is key of self.emp_data, second value is emp_no for RANGE key + exp_keys = [(1985, 10002), (2000, 47291)] + for year, emp_no in exp_keys: + self.cnx.set_property(tables=tables, + scope=fabric.SCOPE_LOCAL, + key=emp_no, mode=fabric.MODE_READONLY) + cur = self.cnx.cursor() + cur.execute("SELECT * FROM {0}".format(tbl_name)) + rows = cur.fetchall() + self.assertEqual(rows, self.emp_data[year]) + + self.cnx.set_property(tables=tables, + key='spam', mode=fabric.MODE_READONLY) + self.assertRaises(ValueError, self.cnx.cursor) + + def test_range_datetime(self): self.assertTrue(self._check_table( "employees.employees_range_datetime", 'RANGE_DATETIME')) @@ -497,9 +535,15 @@ def test_range_string(self): rows = cur.fetchall() self.assertEqual(rows, emp_exp_range_string[str_key]) - self.cnx.set_property(tables=tables, - key=b'not unicode str', mode=fabric.MODE_READONLY) - self.assertRaises(ValueError, self.cnx.cursor) + if not PY2: + self.assertRaises(TypeError, self.cnx.set_property, + tables=tables, key=b'not unicode str', + mode=fabric.MODE_READONLY) + else: + self.cnx.set_property(tables=tables, + key=b'not unicode str', + mode=fabric.MODE_READONLY) + self.assertRaises(ValueError, self.cnx.cursor) self.cnx.set_property(tables=tables, key=12345, mode=fabric.MODE_READONLY) @@ -510,3 +554,111 @@ def test_range_string(self): key='not unicode str', mode=fabric.MODE_READONLY) self.assertRaises(ValueError, self.cnx.cursor) + + def test_bug19642249(self): + self.assertTrue(self._check_table( + "employees.employees_range_string", 'RANGE_STRING')) + + # Invalid key for RANGE_STRING + tbl_name = "employees_range_string" + tables = ["employees.{0}".format(tbl_name)] + + self.cnx.set_property(tables=tables, + key=u'1', mode=fabric.MODE_READONLY) + try: + cur = self.cnx.cursor() + except ValueError as exc: + self.assertEqual("Key invalid; was '1'", str(exc)) + else: + self.fail("ValueError not raised") + + # Invalid key for RANGE_DATETIME + tbl_name = "employees_range_datetime" + tables = ["employees.{0}".format(tbl_name)] + + self.cnx.set_property(tables=tables, + key=datetime.date(1977, 1, 1), + mode=fabric.MODE_READONLY) + try: + cur = self.cnx.cursor() + except ValueError as exc: + self.assertEqual("Key invalid; was '1977-01-01'", str(exc)) + else: + self.fail("ValueError not raised") + + def test_bug19331658(self): + """Pooling not working with fabric + """ + self.assertRaises( + AttributeError, mysql.connector.connect, + fabric=tests.FABRIC_CONFIG, user='root', database='employees', + pool_name='mypool') + + pool_size = 2 + cnx = mysql.connector.connect( + fabric=tests.FABRIC_CONFIG, user='root', database='employees', + pool_size=pool_size, pool_reset_session=False + ) + tbl_name = "employees_range" + + tables = ["employees.{0}".format(tbl_name)] + + cnx.set_property(tables=tables, + scope=fabric.SCOPE_GLOBAL, + mode=fabric.MODE_READWRITE) + cnx.cursor() + self.assertTrue(isinstance(cnx._mysql_cnx, PooledMySQLConnection)) + + data = self.emp_data[1985] + + for emp in data: + cnx.set_property(tables=tables, + key=emp[0], + scope=fabric.SCOPE_LOCAL, + mode=fabric.MODE_READWRITE) + cnx.cursor() + mysqlserver = cnx._fabric_mysql_server + config = cnx._mysql_config + self.assertEqual( + cnx._mysql_cnx.pool_name, "{0}_{1}_{2}_{3}".format( + mysqlserver.host, mysqlserver.port, config['user'], + config['database']) + ) + + mysql.connector._CONNECTION_POOLS = {} + + def test_range_hash(self): + self.assertTrue(self._check_table( + "employees.employees_hash", 'HASH')) + tbl_name = "employees_hash" + + tables = ["employees.{0}".format(tbl_name)] + + self.cnx.set_property(tables=tables, + scope=fabric.SCOPE_GLOBAL, + mode=fabric.MODE_READWRITE) + + cur = self.cnx.cursor() + gtid_executed = self._truncate(cur, tbl_name) + self.cnx.commit() + + insert = ("INSERT INTO {0} " + "VALUES (%s, %s, %s, %s, %s, %s)").format(tbl_name) + + self._populate(self.cnx, gtid_executed, tbl_name, insert, + self.emp_data[1985] + self.emp_data[2000], 3) + + time.sleep(2) + + emp_exp_hash = self.emp_data[1985] + self.emp_data[2000] + + rows = [] + self.cnx.reset_properties() + str_keys = ['group1', 'group2'] + for str_key in str_keys: + self.cnx.set_property(group=str_key, mode=fabric.MODE_READONLY) + cur = self.cnx.cursor() + cur.execute("SELECT * FROM {0}".format(tbl_name)) + rows += cur.fetchall() + + self.assertEqual(rows, emp_exp_hash) diff --git a/tests/test_locales.py b/tests/test_locales.py index 35d371a6..e6e1b672 100644 --- a/tests/test_locales.py +++ b/tests/test_locales.py @@ -84,25 +84,6 @@ class LocalesEngClientErrorTests(tests.MySQLConnectorTests): """Testing locales.eng.client_error""" - def test__GENERATED_ON(self): - try: - from mysql.connector.locales.eng import client_error - except ImportError: - self.fail("locales.eng.client_error could not be imported") - - self.assertTrue(isinstance(client_error._GENERATED_ON, str)) - try: - generatedon = datetime.strptime(client_error._GENERATED_ON, - '%Y-%m-%d').date() - except ValueError as err: - self.fail(err) - - delta = datetime.now().date() - generatedon - self.assertTrue( - delta.days < 120, # pylint disable=E1103 - "eng/client_error.py is more than 120 days old ({0})".format( - delta.days)) # pylint disable=E1103 - def test__MYSQL_VERSION(self): try: from mysql.connector.locales.eng import client_error diff --git a/tests/test_mysql_datatypes.py b/tests/test_mysql_datatypes.py index ba538092..cd74324d 100644 --- a/tests/test_mysql_datatypes.py +++ b/tests/test_mysql_datatypes.py @@ -28,7 +28,7 @@ import time import datetime -from mysql.connector import connection +from mysql.connector import connection, errors import tests @@ -339,18 +339,23 @@ def test_temporal_datetime(self): # Testing YEAR(2), which is now obsolete since MySQL 5.6.6 tblname = self.tables['temporal_year'] - cur.execute( + stmt = ( "CREATE TABLE {table} (" "`id` int NOT NULL AUTO_INCREMENT KEY, " "`t_year_2` YEAR(2))".format(table=tblname) - ) - cur.execute(_get_insert_stmt(tblname, ['t_year_2']), (10,)) - cur.execute(_get_select_stmt(tblname, ['t_year_2'])) - row = cur.fetchone() - - if tests.MYSQL_VERSION >= (5, 6, 6): - self.assertEqual(2010, row[0]) + ) + if tests.MYSQL_VERSION >= (5, 7, 5): + # Support for YEAR(2) removed in MySQL 5.7.5 + self.assertRaises(errors.DatabaseError, cur.execute, stmt) else: - self.assertEqual(10, row[0]) + cur.execute(stmt) + cur.execute(_get_insert_stmt(tblname, ['t_year_2']), (10,)) + cur.execute(_get_select_stmt(tblname, ['t_year_2'])) + row = cur.fetchone() + + if tests.MYSQL_VERSION >= (5, 6, 6): + self.assertEqual(2010, row[0]) + else: + self.assertEqual(10, row[0]) cur.close() diff --git a/tests/test_optionfiles.py b/tests/test_optionfiles.py index 36444343..6b930cf5 100644 --- a/tests/test_optionfiles.py +++ b/tests/test_optionfiles.py @@ -99,6 +99,7 @@ def test_read(self,): def test_get_groups(self): exp = { + 'password': '12345', 'port': '1001', 'socket': '/var/run/mysqld/mysqld2.sock', 'ssl-ca': 'dummyCA', @@ -122,6 +123,7 @@ def test_get_groups(self): def test_get_groups_as_dict(self): exp = dict([ ('client', {'port': '1000', + 'password': '12345', 'socket': '/var/run/mysqld/mysqld.sock', 'ssl-ca': 'dummyCA', 'ssl-cert': 'dummyCert', @@ -167,6 +169,7 @@ def test_read_option_files(self): option_file_dir = os.path.join('tests', 'data', 'option_files') exp = { + 'password': '12345', 'port': 1000, 'unix_socket': '/var/run/mysqld/mysqld.sock', 'ssl_ca': 'dummyCA', @@ -177,6 +180,7 @@ def test_read_option_files(self): option_file_dir, 'my.cnf')) self.assertEqual(exp, result) exp = { + 'password': '12345', 'port': 1001, 'unix_socket': '/var/run/mysqld/mysqld2.sock', 'ssl_ca': 'dummyCA', diff --git a/tests/test_utils.py b/tests/test_utils.py index c694c6ab..150bb799 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,5 @@ # MySQL Connector/Python - MySQL driver written in Python. -# Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # MySQL Connector/Python is licensed under the terms of the GPLv2 # , like most @@ -123,6 +123,18 @@ def test_intstore(self): except ValueError as err: self.fail("intstore failed with 'int{0}store: {1}".format(i, err)) + def test_lc_int(self): + prefix = (b'', b'\xfc', b'\xfd', b'\xfe') + intstore = (1, 2, 3, 8) + try: + for i, j in enumerate((128, 251, 2**24-1, 2**64-1)): + lenenc = utils.lc_int(j) + exp = prefix[i] + \ + getattr(utils, 'int{0}store'.format(intstore[i]))(j) + self.assertEqual(exp, lenenc) + except ValueError as err: + self.fail("length_encoded_int failed for size {0}".format(j, err)) + def test_read_bytes(self): """Read a number of bytes from a buffer""" buf = bytearray(b"ABCDEFghijklm") diff --git a/unittests.py b/unittests.py index ea0dcc39..93b59d5c 100644 --- a/unittests.py +++ b/unittests.py @@ -120,6 +120,7 @@ language = {lc_messages_dir}/english [mysqld] +max_allowed_packet=26777216 basedir = {basedir} datadir = {datadir} tmpdir = {tmpdir} @@ -135,6 +136,8 @@ log-bin = mysqld_{name}_bin local_infile = 1 innodb_flush_log_at_trx_commit = 2 +innodb_log_file_size = 1Gb +general_log_file = general_{name}.log ssl """