diff --git a/.github/DISCUSSION_TEMPLATE/build-issue.yml b/.github/DISCUSSION_TEMPLATE/build-issue.yml new file mode 100644 index 00000000..c2e93df4 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/build-issue.yml @@ -0,0 +1,49 @@ +title: "[Build] " +body: + - type: input + id: os + attributes: + label: What OS and which version do you use? + description: | + e.g. + - Windows 11 + - macOS 13.4 + - Ubuntu 22.04 + + - type: textarea + id: libmysqlclient + attributes: + label: How did you installed mysql client library? + description: | + e.g. + - `apt-get install libmysqlclient-dev` + - `brew install mysql-client` + - `brew install mysql` + render: bash + + - type: textarea + id: pkgconfig-output + attributes: + label: Output from `pkg-config --cflags --libs mysqlclient` + description: If you are using mariadbclient, run `pkg-config --cflags --libs mariadb` instead. + render: bash + + - type: input + id: mysqlclient-install + attributes: + label: How did you tried to install mysqlclient? + description: | + e.g. + - `pip install mysqlclient` + - `poetry add mysqlclient` + + - type: textarea + id: mysqlclient-error + attributes: + label: Output of building mysqlclient + description: not only error message. full log from start installing mysqlclient. + render: bash + + + + diff --git a/.github/DISCUSSION_TEMPLATE/issue-report.yml b/.github/DISCUSSION_TEMPLATE/issue-report.yml new file mode 100644 index 00000000..6724fcf8 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/issue-report.yml @@ -0,0 +1,93 @@ +body: + - type: markdown + attributes: + value: | + Failed to buid? [Use this form](https://github.com/PyMySQL/mysqlclient/discussions/new?category=build-issue). + + We don't use this issue tracker to help users. + Please use this tracker only when you are sure about it is an issue of this software. + + If you had trouble, please ask it on some user community. + + - [Python Discord](https://www.pythondiscord.com/) + For general Python questions, including developing application using MySQL. + + - [MySQL Community Slack](https://lefred.be/mysql-community-on-slack/) + For general MySQL questions. + + - [mysqlclient Discuss](https://github.com/PyMySQL/mysqlclient/discussions) + For mysqlclient specific topics. + + - type: textarea + id: describe + attributes: + label: Describe the bug + description: "A **clear and concise** description of what the bug is." + + - type: textarea + id: environments + attributes: + label: Environment + description: | + - Server and version (e.g. MySQL 8.0.33, MariaDB 10.11.4) + - OS (e.g. Windows 11, Ubuntu 22.04, macOS 13.4.1) + - Python version + + - type: input + id: libmysqlclient + attributes: + label: How did you install libmysqlclient libraries? + description: | + e.g. brew install mysql-cleint, brew install mariadb, apt-get install libmysqlclient-dev + + - type: input + id: mysqlclient-version + attributes: + label: What version of mysqlclient do you use? + + - type: markdown + attributes: + value: | + ## Complete step to reproduce. + # + Do not expect maintainer complement any piece of code, schema, and data need to reproduce. + You need to provide **COMPLETE** step to reproduce. + + It is very recommended to use Docker to start MySQL server. + Maintainer can not use your Database to reproduce your issue. + + **If you write only little code snippet, maintainer may close your issue + without any comment.** + + - type: textarea + id: reproduce-docker + attributes: + label: Docker command to start MySQL server + render: bash + description: e.g. `docker run -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 --rm --name mysql mysql:8.0` + + - type: textarea + id: reproduce-code + attributes: + label: Minimum but complete code to reproduce + render: python + value: | + # Write Python code here. + import MySQLdb + + conn = MySQLdb.connect(host='127.0.0.1', port=3306, user='root') + ... + + - type: textarea + id: reproduce-schema + attributes: + label: Schema and initial data required to reproduce. + render: sql + value: | + -- Write SQL here. + -- e.g. CREATE TABLE ... + + - type: textarea + id: reproduce-other + attributes: + label: Commands, and any other step required to reproduce your issue. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 4b7cf394..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -**IF YOU HAVE SOME TROUBLE, IT'S MAY NOT ISSUE OF THIS PROJECT. GO STACKOVERFLOW!!!** - -If you failed to build, go Stackoverflow. -If you failed to install, go Stackoverflow. -If you failed to connect to database, go Stackoverflow. - -FYI, MySQL Connector/C 6.1.10 has bug. see https://github.com/PyMySQL/mysqlclient-python/issues/169#issuecomment-299778504 - -Only when If you're sure it's PyMySQL's issue, report the complete steps to reproduce, from creating database. - -I don't have time to investigate your issue from an incomplete code snippet. - -See also: https://medium.com/@methane/why-you-must-not-ask-questions-on-github-issues-51d741d83fde diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..9f1273ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +contact_links: + - name: Failed to build + about: Ask help for build error. + url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=build-issue" + + - name: Report issue + about: Found bug? + url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=issue-report" + + - name: Ask question + about: Ask other questions. + url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=q-a" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..95a95ac3 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,15 @@ +name: Lint + +on: + push: + branches: ["main"] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx install ruff + - run: ruff check src/ + - run: ruff format src/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..cf784c78 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,108 @@ +name: Test + +on: + push: + branches: ["main"] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + env: + PIP_NO_PYTHON_VERSION_WARNING: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + include: + - python-version: "3.12" + mariadb: 1 + steps: + - if: ${{ matrix.mariadb }} + name: Start MariaDB + # https://github.com/actions/runner-images/blob/9d9b3a110dfc98100cdd09cb2c957b9a768e2979/images/linux/scripts/installers/mysql.sh#L10-L13 + run: | + docker pull mariadb:10.11 + docker run -d -e MARIADB_ROOT_PASSWORD=root -p 3306:3306 --rm --name mariadb mariadb:10.11 + sudo apt-get -y install libmariadb-dev + mysql --version + mysql -uroot -proot -h127.0.0.1 -e "CREATE DATABASE mysqldb_test" + + - if: ${{ !matrix.mariadb }} + name: Start MySQL + run: | + sudo systemctl start mysql.service + mysql --version + mysql -uroot -proot -e "CREATE DATABASE mysqldb_test" + + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: "requirements.txt" + allow-prereleases: true + + - name: Install mysqlclient + run: | + pip install -v . + + - name: Install test dependencies + run: | + pip install -r requirements.txt + + - name: Run tests + env: + TESTDB: actions.cnf + run: | + pytest --cov=MySQLdb tests + + - uses: codecov/codecov-action@v5 + + django-test: + name: "Run Django LTS test suite" + needs: test + runs-on: ubuntu-latest + env: + PIP_NO_PYTHON_VERSION_WARNING: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + DJANGO_VERSION: "4.2.16" + steps: + - name: Start MySQL + run: | + sudo systemctl start mysql.service + mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql + mysql -uroot -proot -e "set global innodb_flush_log_at_trx_commit=0;" + mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" + mysql -uroot -proot -e "CREATE DATABASE django_default; CREATE DATABASE django_other;" + + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: "ci/django-requirements.txt" + + - name: Install mysqlclient + run: | + #pip install -r requirements.txt + #pip install mysqlclient # Use stable version + pip install . + + - name: Setup Django + run: | + sudo apt-get install libmemcached-dev + wget https://github.com/django/django/archive/${DJANGO_VERSION}.tar.gz + tar xf ${DJANGO_VERSION}.tar.gz + cp ci/test_mysql.py django-${DJANGO_VERSION}/tests/ + cd django-${DJANGO_VERSION} + pip install . -r tests/requirements/py3.txt + + - name: Run Django test + run: | + cd django-${DJANGO_VERSION}/tests/ + PYTHONPATH=.. python3 ./runtests.py --settings=test_mysql diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml new file mode 100644 index 00000000..f8dbf87a --- /dev/null +++ b/.github/workflows/windows.yaml @@ -0,0 +1,97 @@ +name: Build windows wheels + +on: + push: + branches: ["main", "ci"] + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + env: + CONNECTOR_VERSION: "3.4.1" + steps: + - name: Cache Connector + id: cache-connector + uses: actions/cache@v4 + with: + path: c:/mariadb-connector + key: mariadb-connector-c-${{ env.CONNECTOR_VERSION }}-win-2 + + - name: Download and Unzip Connector + if: steps.cache-connector.outputs.cache-hit != 'true' + shell: bash + run: | + curl -LO "https://downloads.mariadb.com/Connectors/c/connector-c-${CONNECTOR_VERSION}/mariadb-connector-c-${CONNECTOR_VERSION}-src.zip" + unzip "mariadb-connector-c-${CONNECTOR_VERSION}-src.zip" -d c:/ + mv "c:/mariadb-connector-c-${CONNECTOR_VERSION}-src" c:/mariadb-connector-src + + - name: make build directory + if: steps.cache-connector.outputs.cache-hit != 'true' + shell: cmd + working-directory: c:/mariadb-connector-src + run: | + mkdir build + + - name: cmake + if: steps.cache-connector.outputs.cache-hit != 'true' + shell: cmd + working-directory: c:/mariadb-connector-src/build + run: | + cmake -A x64 .. -DCMAKE_BUILD_TYPE=Release -DCLIENT_PLUGIN_DIALOG=static -DCLIENT_PLUGIN_SHA256_PASSWORD=static -DCLIENT_PLUGIN_CACHING_SHA2_PASSWORD=static -DDEFAULT_SSL_VERIFY_SERVER_CERT=0 + + - name: cmake build + if: steps.cache-connector.outputs.cache-hit != 'true' + shell: cmd + working-directory: c:/mariadb-connector-src/build + run: | + cmake --build . -j 8 --config Release + + - name: cmake install + if: steps.cache-connector.outputs.cache-hit != 'true' + shell: cmd + working-directory: c:/mariadb-connector-src/build + run: | + cmake -DCMAKE_INSTALL_PREFIX=c:/mariadb-connector -DCMAKE_INSTALL_COMPONENT=Development -DCMAKE_BUILD_TYPE=Release -P cmake_install.cmake + + - name: Checkout mysqlclient + uses: actions/checkout@v4 + with: + path: mysqlclient + + - name: Site Config + shell: bash + working-directory: mysqlclient + run: | + pwd + find . + cat <site.cfg + [options] + static = True + connector = C:/mariadb-connector + EOF + cat site.cfg + + - uses: actions/setup-python@v5 + - name: Install cibuildwheel + run: python -m pip install cibuildwheel + - name: Build wheels + working-directory: mysqlclient + env: + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" + CIBW_ARCHS: "AMD64" + CIBW_TEST_COMMAND: 'python -c "import MySQLdb; print(MySQLdb.version_info)" ' + run: "python -m cibuildwheel --prerelease-pythons --output-dir dist" + + - name: Build sdist + working-directory: mysqlclient + run: | + python -m pip install build + python -m build -s -o dist + + - name: Upload Wheel + uses: actions/upload-artifact@v4 + with: + name: win-wheels + path: mysqlclient/dist/*.* diff --git a/.gitignore b/.gitignore index 42bbfb5d..1f081cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ .tox/ build/ dist/ -MySQLdb/release.py .coverage diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..75d7a389 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +version: 2 + +sphinx: + configuration: doc/conf.py + +build: + os: ubuntu-22.04 + tools: + python: "3.13" + + apt_packages: + - default-libmysqlclient-dev + - build-essential + +python: + install: + - requirements: doc/requirements.txt + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a1df409..00000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -dist: xenial -language: python - -# See aws s3 ls s3://travis-python-archives/binaries/ubuntu/16.04/x86_64/ -python: - - "nightly" - - "pypy3.5" - - "pypy2.7-5.10.0" - - "3.7" - - "3.6" - - "3.5" - - "2.7" - -cache: pip - -services: - - mysql - -install: - - pip install -U pip - - pip install -U mock coverage pytest pytest-cov codecov - -env: - global: - - TESTDB=travis.cnf - -before_script: - - "mysql --help" - - "mysql --print-defaults" - - "mysql -e 'create database mysqldb_test charset utf8mb4;'" - -script: - - pip install -e . - - pytest --cov ./MySQLdb - -after_succes: - - codecov - -# vim: sw=2 ts=2 sts=2 diff --git a/HISTORY.rst b/HISTORY.rst index 2fd4500b..66470541 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,8 +1,242 @@ +====================== + What's new in 2.2.7 +====================== + +Release: 2025-01-10 + +* Add ``user``, ``host``, ``database``, and ``db`` attributes to ``Connection``. + opentelemetry-instrumentation-(dbapi|mysqlclient) use them. (#753) + +====================== + What's new in 2.2.6 +====================== + +Release: 2024-11-12 + +* MariaDB Connector/C 3.4 and MairaDB 11.4 enabled SSL and CA verification by default. + It affected 2.2.5 windows wheel. This release disables SSL and CA verification by default. (#731) + +* Add ``server_public_key_path`` option. It is needed to connect MySQL server with + ``sha256_password`` or ``caching_sha2_password`` authentication plugin without + secure connection. (#744) + +====================== + What's new in 2.2.5 +====================== + +Release: 2024-10-20 + +* (Windows wheel) Update MariaDB Connector/C to 3.4.1. #726 +* (Windows wheel) Build wheels for Python 3.13. #726 + +====================== + What's new in 2.2.4 +====================== + +Release: 2024-02-09 + +* Support ``ssl=True`` in ``connect()``. (#700) + This makes better compatibility with PyMySQL and mysqlclient==2.2.1 + with libmariadb. See #698 for detail. + + +====================== + What's new in 2.2.3 +====================== + +Release: 2024-02-04 + +* Fix ``Connection.kill()`` method that broken in 2.2.2. (#689) + + +====================== + What's new in 2.2.2 +====================== + +Release: 2024-02-04 + +* Support building with MySQL 8.3 (#688). +* Deprecate ``db.shutdown()`` and ``db.kill()`` methods in docstring. + This is because ``mysql_shutdown()`` and ``mysql_kill()`` were removed in MySQL 8.3. + They will emit DeprecationWarning in the future but not for now. + + +====================== + What's new in 2.2.1 +====================== + +Release: 2023-12-13 + +* ``Connection.ping()`` avoid using ``MYSQL_OPT_RECONNECT`` option until + ``reconnect=True`` is specified. MySQL 8.0.33 start showing warning + when the option is used. (#664) +* Windows: Update MariaDB Connector/C to 3.3.8. (#665) +* Windows: Build wheels for Python 3.12 (#644) + + +====================== + What's new in 2.2.0 +====================== + +Release: 2023-06-22 + +* Use ``pkg-config`` instead of ``mysql_config`` (#586) +* Raise ProgrammingError on -inf (#557) +* Raise IntegrityError for ER_BAD_NULL. (#579) +* Windows: Use MariaDB Connector/C 3.3.4 (#585) +* Use pkg-config instead of mysql_config (#586) +* Add collation option (#564) +* Drop Python 3.7 support (#593) +* Use pyproject.toml for build (#598) +* Add Cursor.mogrify (#477) +* Partial support of ssl_mode option with mariadbclient (#475) +* Discard remaining results without creating Python objects (#601) +* Fix executemany with binary prefix (#605) + +====================== + What's new in 2.1.1 +====================== + +Release: 2022-06-22 + +* Fix qualname of exception classes. (#522) +* Fix range check in ``MySQLdb._mysql.result.fetch_row()``. Invalid ``how`` argument caused SEGV. (#538) +* Fix docstring of ``_mysql.connect``. (#540) +* Windows: Binary wheels are updated. (#541) + * Use MariaDB Connector/C 3.3.1. + * Use cibuildwheel to build wheels. + * Python 3.8-3.11 + +====================== + What's new in 2.1.0 +====================== + +Release: 2021-11-17 + +* Add ``multistatement=True`` option. You can disable multi statement. (#500). +* Remove unnecessary bytes encoder which is remained for Django 1.11 + compatibility (#490). +* Deprecate ``passwd`` and ``db`` keyword. Use ``password`` and ``database`` + instead. (#488). +* Windows: Binary wheels are built with MariaDB Connector/C 3.2.4. (#508) +* ``set_character_set()`` sends ``SET NAMES`` query always. This means + all new connections send it too. This solves compatibility issues + when server and client library are different version. (#509) +* Remove ``escape()`` and ``escape_string()`` from ``MySQLdb`` package. + (#511) +* Add Python 3.10 support and drop Python 3.5 support. + +====================== + What's new in 2.0.3 +====================== + +Release: 2021-01-01 + +* Add ``-std=c99`` option to cflags by default for ancient compilers that doesn't + accept C99 by default. +* You can customize cflags and ldflags by setting ``MYSQLCLIENT_CFLAGS`` and + ``MYSQLCLIENT_LDFLAGS``. It overrides ``mysql_config``. + +====================== + What's new in 2.0.2 +====================== + +Release: 2020-12-10 + +* Windows: Update MariaDB Connector/C to 3.1.11. +* Optimize fetching many rows with DictCursor. + +====================== + What's new in 2.0.1 +====================== + +Release: 2020-07-03 + +* Fixed multithread safety issue in fetching row. +* Removed obsolete members from Cursor. (e.g. `messages`, `_warnings`, `_last_executed`) + +====================== + What's new in 2.0.0 +====================== + +Release: 2020-07-02 + +* Dropped Python 2 support +* Dropped Django 1.11 support +* Add context manager interface to Connection which closes the connection on ``__exit__``. +* Add ``ssl_mode`` option. + + +====================== + What's new in 1.4.6 +====================== + +Release: 2019-11-21 + +* The ``cp1252`` encoding is used when charset is "latin1". (#390) + +====================== + What's new in 1.4.5 +====================== + +Release: 2019-11-06 + +* The ``auth_plugin`` option is added. (#389) + + +====================== + What's new in 1.4.4 +====================== + +Release: 2019-08-12 + +* ``charset`` option is passed to ``mysql_options(mysql, MYSQL_SET_CHARSET_NAME, charset)`` + before ``mysql_real_connect`` is called. + This avoid extra ``SET NAMES `` query when creating connection. + + +====================== + What's new in 1.4.3 +====================== + +Release: 2019-08-09 + +* ``--static`` build supports ``libmariadbclient.a`` +* Try ``mariadb_config`` when ``mysql_config`` is not found +* Fixed warning happened in Python 3.8 (#359) +* Fixed ``from MySQLdb import *``, while I don't recommend it. (#369) +* Fixed SEGV ``MySQLdb.escape_string("1")`` when libmariadb is used and + no connection is created. (#367) +* Fixed many circular references are created in ``Cursor.executemany()``. (#375) + + +====================== + What's new in 1.4.2 +====================== + +Release: 2019-02-08 + +* Fix Django 1.11 compatibility. (#327) + mysqlclient 1.5 will not support Django 1.11. It is not because + mysqlclient will break backward compatibility, but Django used + unsupported APIs and Django 1.11 don't fix bugs including + compatibility issues. + +====================== + What's new in 1.4.1 +====================== + +Release: 2019-01-19 + +* Fix dict parameter support (#323, regression of 1.4.0) + ====================== What's new in 1.4.0 ====================== -Release: TBD +Release: 2019-01-18 + +* Dropped Python 3.4 support. * Removed ``threadsafe`` and ``embedded`` build options. @@ -18,11 +252,26 @@ Release: TBD * Remove ``waiter`` option from Connection. -* Remove ``fileno``, ``escape_sequence``, and ``escape_dict`` methods - from Connection class. +* Remove ``escape_sequence``, and ``escape_dict`` methods from Connection class. * Remove automatic MySQL warning checking. +* Drop support for MySQL Connector/C with MySQL<5.1.12. + +* Remove ``_mysql.NULL`` constant. + +* Remove ``_mysql.thread_safe()`` function. + +* Support non-ASCII field name with non-UTF-8 connection encoding. (#210) + +* Optimize decoding speed of string and integer types. + +* Remove ``MySQLdb.constants.REFRESH`` module. + +* Remove support for old datetime format for MySQL < 4.1. + +* Fix wrong errno is raised when ``mysql_real_connect`` is failed. (#316) + ====================== What's new in 1.3.14 @@ -36,13 +285,20 @@ Release: 2018-12-04 * Add ``Connection._get_native_connection`` for XTA project (#269) -* Fix SEGV on MariaDB Connector/C when ``Connection.close()`` is called - for closed connection. (#270, #272, #276) +* Fix SEGV on MariaDB Connector/C when some methods of ``Connection`` + objects are called after ``Connection.close()`` is called. (#270, #272, #276) + See https://jira.mariadb.org/browse/CONC-289 * Fix ``Connection.client_flag`` (#266) * Fix SSCursor may raise same exception twice (#282) + * This removed ``Cursor._last_executed`` which was duplicate of ``Cursor._executed``. + Both members are private. So this type of changes are not documented in changelog + generally. But Django used the private member for ``last_executed_query`` implementation. + If you use the method the method directly or indirectly, this version will break + your application. See https://code.djangoproject.com/ticket/30013 + * ``waiter`` option is now deprecated. (#285) * Fixed SSL support is not detected when built with MySQL < 5.1 (#291) @@ -147,7 +403,7 @@ More tests for date and time columns. (#41) Fix calling .execute() method for closed cursor cause TypeError. (#37) -Improve peformance to parse date. (#43) +Improve performance to parse date. (#43) Support geometry types (#49) @@ -408,4 +664,3 @@ ursor.fetchXXXDict() methods raise DeprecationWarning cursor.begin() is making a brief reappearence. cursor.callproc() now works, with some limitations. - diff --git a/INSTALL.rst b/INSTALL.rst deleted file mode 100644 index 2588b99e..00000000 --- a/INSTALL.rst +++ /dev/null @@ -1,161 +0,0 @@ -==================== -MySQLdb Installation -==================== - -.. contents:: -.. - -Prerequisites -------------- - -+ Python 2.7, 3.4 or higher - -+ setuptools - - * http://pypi.python.org/pypi/setuptools - -+ MySQL 5.5 or higher - - * http://www.mysql.com/downloads/ - - * MySQL-5.0 may work, but not supported. - -+ C compiler - - * Most free software-based systems already have this, usually gcc. - - * Most commercial UNIX platforms also come with a C compiler, or - you can also use gcc. - - * If you have some Windows flavor, you should use Windows SDK or - Visual C++. - - -Building and installing ------------------------ - -The setup.py script uses mysql_config to find all compiler and linker -options, and should work as is on any POSIX-like platform, so long as -mysql_config is in your path. - -Depending on which version of MySQL you have, you may have the option -of using three different client libraries. To select the client library, -edit the [options] section of site.cfg: - - static - if True, try to link against a static library; otherwise link - against dynamic libraries (default). You may need static linking - to use the embedded server. - This option doesn't work for MySQL>5.6 since libmysqlclient - requires libstdc++. If you want to use, add `-lstdc++` to - mysql_config manually. - -If `/lib` is not added to `/etc/ld.so.conf`, `import _mysql` -doesn't work. To fix this, (1) set `LD_LIBRARY_PATH`, or (2) add -`-Wl,-rpath,/lib` to ldflags in your mysql_config. - -Finally, putting it together:: - - $ tar xz mysqlclient-1.3.6.tar.gz - $ cd mysqlclient-1.3.6 - $ # edit site.cfg if necessary - $ python setup.py build - $ sudo python setup.py install # or su first - - -Windows -....... - -I don't do Windows. However if someone provides me with a package for -Windows, I'll make it available. Don't ask me for help with Windows -because I can't help you. - -Generally, though, running setup.py is similar to above:: - - C:\...> python setup.py install - C:\...> python setup.py bdist_wininst - -The latter example should build a Windows installer package, if you -have the correct tools. In any event, you *must* have a C compiler. -Additionally, you have to set an environment variable (mysqlroot) -which is the path to your MySQL installation. In theory, it would be -possible to get this information out of the registry, but like I said, -I don't do Windows, but I'll accept a patch that does this. - -On Windows, you will definitely have to edit site.cfg since there is -no mysql_config in the MySQL package. - - -Binary Packages ---------------- - -I don't plan to make binary packages any more. However, if someone -contributes one, I will make it available. Several OS vendors have -their own packages available. - - -RPMs -.... - -If you prefer to install RPMs, you can use the bdist_rpm command with -setup.py. This only builds the RPM; it does not install it. You may -want to use the --python=XXX option, where XXX is the name of the -Python executable, i.e. python, python2, python2.4; the default is -python. Using this will incorporate the Python executable name into -the package name for the RPM so you have install the package multiple -times if you need to support more than one version of Python. You can -also set this in setup.cfg. - - -Red Hat Linux -............. - -MySQL-python is pre-packaged in Red Hat Linux 7.x and newer. This -includes Fedora Core and Red Hat Enterprise Linux. You can also -build your own RPM packages as described above. - - -Debian GNU/Linux -................ - -Packaged as `python-mysqldb`_:: - - # apt-get install python-mysqldb - -Or use Synaptic. - -.. _`python-mysqldb`: http://packages.debian.org/python-mysqldb - - -Ubuntu -...... - -Same as with Debian. - - -Gentoo Linux -............ - -Packaged as `mysql-python`_. :: - - # emerge sync - # emerge mysql-python - # emerge zmysqlda # if you use Zope - -.. _`mysql-python`: https://packages.gentoo.org/packages/search?q=mysql-python - - -BSD -... - -MySQL-python is a ported package in FreeBSD, NetBSD, and OpenBSD, -although the name may vary to match OS conventions. - - -License -------- - -GPL or the original license based on Python 1.5.2's license. - - -:Author: Andy Dustman diff --git a/MANIFEST.in b/MANIFEST.in index 415a995a..58a996de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,7 @@ recursive-include doc *.rst recursive-include tests *.py include doc/conf.py -include MANIFEST.in include HISTORY.rst -include INSTALL.rst include README.md include LICENSE -include metadata.cfg include site.cfg -include setup_common.py -include setup_posix.py -include setup_windows.py diff --git a/Makefile b/Makefile index 491cc75e..3f9ff8bb 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,20 @@ .PHONY: build build: - python3 setup.py build_ext -if + python setup.py build_ext -if +.PHONY: doc +doc: + pip install . + pip install sphinx + cd doc && make html .PHONY: clean clean: find . -name '*.pyc' -delete find . -name '__pycache__' -delete - rm *.so - python3 setup.py clean + rm -rf build + +.PHONY: check +check: + ruff *.py src ci + black *.py src ci diff --git a/MySQLdb/__init__.py b/MySQLdb/__init__.py deleted file mode 100644 index fa52a83d..00000000 --- a/MySQLdb/__init__.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -MySQLdb - A DB API v2.0 compatible interface to MySQL. - -This package is a wrapper around _mysql, which mostly implements the -MySQL C API. - -connect() -- connects to server - -See the C API specification and the MySQL documentation for more info -on other items. - -For information on how MySQLdb handles type conversion, see the -MySQLdb.converters module. -""" - -from MySQLdb.release import __version__, version_info, __author__ - -from . import _mysql - -if version_info != _mysql.version_info: - raise ImportError("this is MySQLdb version %s, but _mysql is version %r" % - (version_info, _mysql.version_info)) - -threadsafety = 1 -apilevel = "2.0" -paramstyle = "format" - -from ._mysql import * -from MySQLdb.compat import PY2 -from MySQLdb.constants import FIELD_TYPE -from MySQLdb.times import Date, Time, Timestamp, \ - DateFromTicks, TimeFromTicks, TimestampFromTicks - -try: - frozenset -except NameError: - from sets import ImmutableSet as frozenset - -class DBAPISet(frozenset): - - """A special type of set for which A == x is true if A is a - DBAPISet and x is a member of that set.""" - - def __eq__(self, other): - if isinstance(other, DBAPISet): - return not self.difference(other) - return other in self - - -STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, - FIELD_TYPE.VAR_STRING]) -BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB, - FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB]) -NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT, - FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG, - FIELD_TYPE.TINY, FIELD_TYPE.YEAR, FIELD_TYPE.NEWDECIMAL]) -DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE]) -TIME = DBAPISet([FIELD_TYPE.TIME]) -TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME]) -DATETIME = TIMESTAMP -ROWID = DBAPISet() - -def test_DBAPISet_set_equality(): - assert STRING == STRING - -def test_DBAPISet_set_inequality(): - assert STRING != NUMBER - -def test_DBAPISet_set_equality_membership(): - assert FIELD_TYPE.VAR_STRING == STRING - -def test_DBAPISet_set_inequality_membership(): - assert FIELD_TYPE.DATE != STRING - -if PY2: - def Binary(x): - return bytearray(x) -else: - def Binary(x): - return bytes(x) - -def Connect(*args, **kwargs): - """Factory function for connections.Connection.""" - from MySQLdb.connections import Connection - return Connection(*args, **kwargs) - -connect = Connection = Connect - -__all__ = [ 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', - 'Date', 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', - 'TimestampFromTicks', 'DataError', 'DatabaseError', 'Error', - 'FIELD_TYPE', 'IntegrityError', 'InterfaceError', 'InternalError', - 'MySQLError', 'NULL', 'NUMBER', 'NotSupportedError', 'DBAPISet', - 'OperationalError', 'ProgrammingError', 'ROWID', 'STRING', 'TIME', - 'TIMESTAMP', 'Warning', 'apilevel', 'connect', 'connections', - 'constants', 'converters', 'cursors', 'debug', 'escape', - 'escape_string', 'get_client_info', - 'paramstyle', 'string_literal', 'threadsafety', 'version_info'] - diff --git a/MySQLdb/compat.py b/MySQLdb/compat.py deleted file mode 100644 index 70580b61..00000000 --- a/MySQLdb/compat.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - -if sys.version_info[0] == 3: - PY2 = False - unicode = str - unichr = chr - long = int -else: - PY2 = True - unicode = unicode - unichr = unichr - long = long diff --git a/MySQLdb/constants/REFRESH.py b/MySQLdb/constants/REFRESH.py deleted file mode 100644 index 4a08b94e..00000000 --- a/MySQLdb/constants/REFRESH.py +++ /dev/null @@ -1,17 +0,0 @@ -"""MySQL REFRESH Constants - -These constants seem to mostly deal with things internal to the -MySQL server. Forget you saw this. - -""" - -GRANT = 1 -LOG = 2 -TABLES = 4 -HOSTS = 8 -STATUS = 16 -THREADS = 32 -SLAVE = 64 -MASTER = 128 -READ_LOCK = 16384 -FAST = 32768 diff --git a/MySQLdb/constants/__init__.py b/MySQLdb/constants/__init__.py deleted file mode 100644 index 3da4a0e7..00000000 --- a/MySQLdb/constants/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['CR', 'FIELD_TYPE','CLIENT','REFRESH','ER','FLAG'] diff --git a/README.md b/README.md index cd91383d..e679b533 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,100 @@ # mysqlclient -[![Build Status](https://secure.travis-ci.org/PyMySQL/mysqlclient-python.png)](http://travis-ci.org/PyMySQL/mysqlclient-python) +This project is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1). +This project adds Python 3 support and fixed many bugs. -This is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1). +* PyPI: https://pypi.org/project/mysqlclient/ +* GitHub: https://github.com/PyMySQL/mysqlclient -This project adds Python 3 support and bug fixes. -I hope this fork is merged back to MySQLdb1 like distribute was merged back to setuptools. -## Install - -### Prerequisites +## Support -You may need to install the Python and MySQL development headers and libraries like so: +**Do Not use Github Issue Tracker to ask help. OSS Maintainer is not free tech support** -* `sudo apt-get install python-dev default-libmysqlclient-dev` # Debian / Ubuntu -* `sudo yum install python-devel mysql-devel` # Red Hat / CentOS -* `brew install mysql-connector-c` # macOS (Homebrew) (Currently, it has bug. See below) +When your question looks relating to Python rather than MySQL/MariaDB: -On Windows, there are binary wheels you can install without MySQLConnector/C or MSVC. +* Python mailing list [python-list](https://mail.python.org/mailman/listinfo/python-list) +* Slack [pythondev.slack.com](https://pyslackers.com/web/slack) -#### Note on Python 3 : if you are using python3 then you need to install python3-dev using the following command : +Or when you have question about MySQL/MariaDB: -`sudo apt-get install python3-dev` # debian / Ubuntu +* [MySQL Support](https://dev.mysql.com/support/) +* [Getting Help With MariaDB](https://mariadb.com/kb/en/getting-help-with-mariadb/) -`sudo yum install python3-devel ` # Red Hat / CentOS -#### **Note about bug of MySQL Connector/C on macOS** - -See also: https://bugs.mysql.com/bug.php?id=86971 +## Install -Versions of MySQL Connector/C may have incorrect default configuration options that cause compilation errors when `mysqlclient-python` is installed. (As of November 2017, this is known to be true for homebrew's `mysql-connector-c` and [official package](https://dev.mysql.com/downloads/connector/c/)) +### Windows -Modification of `mysql_config` resolves these issues as follows. +Building mysqlclient on Windows is very hard. +But there are some binary wheels you can install easily. -Change +If binary wheels do not exist for your version of Python, it may be possible to +build from source, but if this does not work, **do not come asking for support.** +To build from source, download the +[MariaDB C Connector](https://mariadb.com/downloads/#connectors) and install +it. It must be installed in the default location +(usually "C:\Program Files\MariaDB\MariaDB Connector C" or +"C:\Program Files (x86)\MariaDB\MariaDB Connector C" for 32-bit). If you +build the connector yourself or install it in a different location, set the +environment variable `MYSQLCLIENT_CONNECTOR` before installing. Once you have +the connector installed and an appropriate version of Visual Studio for your +version of Python: ``` -# on macOS, on or about line 112: -# Create options -libs="-L$pkglibdir" -libs="$libs -l " +$ pip install mysqlclient ``` -to +### macOS (Homebrew) +Install MySQL and mysqlclient: + +```bash +$ # Assume you are activating Python 3 venv +$ brew install mysql pkg-config +$ pip install mysqlclient ``` -# Create options -libs="-L$pkglibdir" -libs="$libs -lmysqlclient -lssl -lcrypto" + +If you don't want to install MySQL server, you can use mysql-client instead: + +```bash +$ # Assume you are activating Python 3 venv +$ brew install mysql-client pkg-config +$ export PKG_CONFIG_PATH="$(brew --prefix)/opt/mysql-client/lib/pkgconfig" +$ pip install mysqlclient ``` -An improper ssl configuration may also create issues; see, e.g, `brew info openssl` for details on macOS. +### Linux -### Install from PyPI +**Note that this is a basic step. I can not support complete step for build for all +environment. If you can see some error, you should fix it by yourself, or ask for +support in some user forum. Don't file a issue on the issue tracker.** -`pip install mysqlclient` +You may need to install the Python 3 and MySQL development headers and libraries like so: -NOTE: Wheels for Windows may be not released with source package. You should pin version -in your `requirements.txt` to avoid trying to install newest source package. +* `$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pkg-config` # Debian / Ubuntu +* `% sudo yum install python3-devel mysql-devel pkgconfig` # Red Hat / CentOS +Then you can install mysqlclient via pip now: -### Install from source +``` +$ pip install mysqlclient +``` + +### Customize build (POSIX) + +mysqlclient uses `pkg-config --cflags --ldflags mysqlclient` by default for finding +compiler/linker flags. -1. Download source by `git clone` or [zipfile](https://github.com/PyMySQL/mysqlclient-python/archive/master.zip). -2. Customize `site.cfg` -3. `python setup.py install` +You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment +variables to customize compiler/linker options. + +```bash +$ export MYSQLCLIENT_CFLAGS=`pkg-config mysqlclient --cflags` +$ export MYSQLCLIENT_LDFLAGS=`pkg-config mysqlclient --libs` +$ pip install mysqlclient +``` ### Documentation Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/) - diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..75f0c541 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. \ No newline at end of file diff --git a/ci/django-requirements.txt b/ci/django-requirements.txt new file mode 100644 index 00000000..83c8a8f2 --- /dev/null +++ b/ci/django-requirements.txt @@ -0,0 +1,24 @@ +# django-3.2.19/tests/requirements/py3.txt +aiosmtpd +asgiref >= 3.3.2 +argon2-cffi >= 16.1.0 +backports.zoneinfo; python_version < '3.9' +bcrypt +docutils +geoip2 +jinja2 >= 2.9.2 +numpy +Pillow >= 6.2.0 +# pylibmc/libmemcached can't be built on Windows. +pylibmc; sys.platform != 'win32' +pymemcache >= 3.4.0 +# RemovedInDjango41Warning. +python-memcached >= 1.59 +pytz +pywatchman; sys.platform != 'win32' +PyYAML +selenium +sqlparse >= 0.2.2 +tblib >= 1.5.0 +tzdata +colorama; sys.platform == 'win32' diff --git a/ci/test_mysql.py b/ci/test_mysql.py new file mode 100644 index 00000000..498be7cf --- /dev/null +++ b/ci/test_mysql.py @@ -0,0 +1,43 @@ +# This is an example test settings file for use with the Django test suite. +# +# The 'sqlite3' backend requires only the ENGINE setting (an in- +# memory database will be used). All other backends will require a +# NAME and potentially authentication information. See the +# following section in the docs for more information: +# +# https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/ +# +# The different databases that Django supports behave differently in certain +# situations, so it is recommended to run the test suite against as many +# database backends as possible. You may want to create a separate settings +# file for each of the backends you test against. + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_default", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, + "other": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_other", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, +} + +SECRET_KEY = "django_tests_secret_key" + +# Use a fast hasher to speed up tests. +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.MD5PasswordHasher", +] + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +USE_TZ = False diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..014486d2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "src/MySQLdb/constants/*" diff --git a/doc/FAQ.rst b/doc/FAQ.rst index 21e00b9b..14c8f72c 100644 --- a/doc/FAQ.rst +++ b/doc/FAQ.rst @@ -26,7 +26,7 @@ probably the issue, but it shouldn't happen any more. ImportError ----------- - ImportError: No module named _mysql + ImportError: No module named _mysql If you see this, it's likely you did some wrong when installing MySQLdb; re-read (or read) README. _mysql is the low-level C module @@ -42,7 +42,7 @@ still have to edit a configuration file so that the setup knows where to find MySQL and what libraries to include. - ImportError: libmysqlclient_r.so.14: cannot open shared object file: No such file or directory + ImportError: libmysqlclient_r.so.14: cannot open shared object file: No such file or directory The number after .so may vary, but this means you have a version of MySQLdb compiled against one version of MySQL, and are now trying to @@ -67,7 +67,7 @@ Solutions: `_. - ImportError: ld.so.1: python: fatal: libmtmalloc.so.1: DF_1_NOOPEN tagged object may not be dlopen()'ed + ImportError: ld.so.1: python: fatal: libmtmalloc.so.1: DF_1_NOOPEN tagged object may not be dlopen()'ed This is a weird one from Solaris. What does it mean? I have no idea. However, things like this can happen if there is some sort of a compiler @@ -79,9 +79,9 @@ different vendors. Solution: Rebuild Python or MySQL (or maybe both) from source. - ImportError: dlopen(./_mysql.so, 2): Symbol not found: _sprintf$LDBLStub - Referenced from: ./_mysql.so - Expected in: dynamic lookup + ImportError: dlopen(./_mysql.so, 2): Symbol not found: _sprintf$LDBLStub + Referenced from: ./_mysql.so + Expected in: dynamic lookup This is one from Mac OS X. It seems to have been a compiler mismatch, but this time between two different versions of GCC. It seems nearly @@ -110,7 +110,7 @@ rolled back, and they cause pending transactions to commit. Other Errors ------------ - OperationalError: (1251, 'Client does not support authentication protocol requested by server; consider upgrading MySQL client') + OperationalError: (1251, 'Client does not support authentication protocol requested by server; consider upgrading MySQL client') This means your server and client libraries are not the same version. More specifically, it probably means you have a 4.1 or newer server diff --git a/doc/MySQLdb.constants.rst b/doc/MySQLdb.constants.rst index e28dee2b..a803e3e5 100644 --- a/doc/MySQLdb.constants.rst +++ b/doc/MySQLdb.constants.rst @@ -1,59 +1,50 @@ -constants Package -================= - -:mod:`constants` Package ------------------------- - -.. automodule:: MySQLdb.constants - :members: - :undoc-members: - :show-inheritance: - -:mod:`CLIENT` Module --------------------- - -.. automodule:: MySQLdb.constants.CLIENT - :members: - :undoc-members: - :show-inheritance: - -:mod:`CR` Module ----------------- - -.. automodule:: MySQLdb.constants.CR - :members: - :undoc-members: - :show-inheritance: - -:mod:`ER` Module ----------------- - -.. automodule:: MySQLdb.constants.ER - :members: - :undoc-members: - :show-inheritance: - -:mod:`FIELD_TYPE` Module ------------------------- - -.. automodule:: MySQLdb.constants.FIELD_TYPE - :members: - :undoc-members: - :show-inheritance: - -:mod:`FLAG` Module ------------------- - -.. automodule:: MySQLdb.constants.FLAG - :members: - :undoc-members: - :show-inheritance: - -:mod:`REFRESH` Module ---------------------- - -.. automodule:: MySQLdb.constants.REFRESH - :members: - :undoc-members: - :show-inheritance: - +constants Package +================= + +:mod:`constants` Package +------------------------ + +.. automodule:: MySQLdb.constants + :members: + :undoc-members: + :show-inheritance: + +:mod:`CLIENT` Module +-------------------- + +.. automodule:: MySQLdb.constants.CLIENT + :members: + :undoc-members: + :show-inheritance: + +:mod:`CR` Module +---------------- + +.. automodule:: MySQLdb.constants.CR + :members: + :undoc-members: + :show-inheritance: + +:mod:`ER` Module +---------------- + +.. automodule:: MySQLdb.constants.ER + :members: + :undoc-members: + :show-inheritance: + +:mod:`FIELD_TYPE` Module +------------------------ + +.. automodule:: MySQLdb.constants.FIELD_TYPE + :members: + :undoc-members: + :show-inheritance: + +:mod:`FLAG` Module +------------------ + +.. automodule:: MySQLdb.constants.FLAG + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/MySQLdb.rst b/doc/MySQLdb.rst index ac690900..5e6791d5 100644 --- a/doc/MySQLdb.rst +++ b/doc/MySQLdb.rst @@ -9,37 +9,49 @@ MySQLdb Package :undoc-members: :show-inheritance: -:mod:`connections` Module -------------------------- +:mod:`MySQLdb.connections` Module +--------------------------------- .. automodule:: MySQLdb.connections :members: Connection :undoc-members: - :show-inheritance: -:mod:`converters` Module ------------------------- +:mod:`MySQLdb.converters` Module +-------------------------------- .. automodule:: MySQLdb.converters :members: :undoc-members: - :show-inheritance: -:mod:`cursors` Module ---------------------- +:mod:`MySQLdb.cursors` Module +----------------------------- .. automodule:: MySQLdb.cursors - :members: Cursor + :members: :undoc-members: :show-inheritance: -:mod:`times` Module -------------------- +:mod:`MySQLdb.times` Module +--------------------------- .. automodule:: MySQLdb.times :members: :undoc-members: - :show-inheritance: + +:mod:`MySQLdb._mysql` Module +---------------------------- + +.. automodule:: MySQLdb._mysql + :members: + :undoc-members: + +:mod:`MySQLdb._exceptions` Module +--------------------------------- + +.. automodule:: MySQLdb._exceptions + :members: + :undoc-members: + Subpackages ----------- diff --git a/doc/_mysql.rst b/doc/_mysql.rst deleted file mode 100644 index 4a60591b..00000000 --- a/doc/_mysql.rst +++ /dev/null @@ -1,7 +0,0 @@ -_mysql Module -============= - -.. automodule:: _mysql - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/doc/_mysql_exceptions.rst b/doc/_mysql_exceptions.rst deleted file mode 100644 index 9b65de35..00000000 --- a/doc/_mysql_exceptions.rst +++ /dev/null @@ -1,7 +0,0 @@ -_mysql_exceptions Module -======================== - -.. automodule:: _mysql_exceptions - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/conf.py b/doc/conf.py index b9f58bd7..e06003ff 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # MySQLdb documentation build configuration file, created by # sphinx-quickstart on Sun Oct 07 19:36:17 2012. @@ -11,214 +10,218 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +# skip flake8 and black for this file +# flake8: noqa +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +# sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- +nitpick_ignore = [ + ("py:class", "datetime.date"), + ("py:class", "datetime.time"), + ("py:class", "datetime.datetime"), +] + + # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = "1.0" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = "utf-8-sig" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'MySQLdb' -copyright = u'2012, Andy Dustman' +project = "mysqlclient" +copyright = "2023, Inada Naoki" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.2' +version = "1.2" # The full version, including alpha/beta/rc tags. -release = '1.2.4b4' +release = "1.2.4b4" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = "" # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = "%B %d, %Y" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = "%b %d, %Y" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = "" # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'MySQLdbdoc' +htmlhelp_basename = "MySQLdbdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'MySQLdb.tex', u'MySQLdb Documentation', - u'Andy Dustman', 'manual'), + ("index", "MySQLdb.tex", "MySQLdb Documentation", "Andy Dustman", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'mysqldb', u'MySQLdb Documentation', - [u'Andy Dustman'], 1) -] +man_pages = [("index", "mysqldb", "MySQLdb Documentation", ["Andy Dustman"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -227,16 +230,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'MySQLdb', u'MySQLdb Documentation', - u'Andy Dustman', 'MySQLdb', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "MySQLdb", + "MySQLdb Documentation", + "Andy Dustman", + "MySQLdb", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/doc/modules.rst b/doc/modules.rst deleted file mode 100644 index 7cf3faaa..00000000 --- a/doc/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -MySQLdb -======= - -.. toctree:: - :maxdepth: 4 - - MySQLdb diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..48319f03 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,2 @@ +sphinx~=8.0 +sphinx-rtd-theme~=3.0.0 diff --git a/doc/user_guide.rst b/doc/user_guide.rst index 55c2c83f..391b162f 100644 --- a/doc/user_guide.rst +++ b/doc/user_guide.rst @@ -8,13 +8,13 @@ MySQLdb User's Guide Introduction ------------ -MySQLdb is a thread-compatible interface to the popular MySQL -database server that provides the Python database API. +MySQLdb is an interface to the popular MySQL or MariaDB +database servers that provides the Python database API. Installation ------------ -The ``README`` file has complete installation instructions. +The `README `_ file has complete installation instructions. MySQLdb._mysql @@ -28,7 +28,7 @@ for this module is intentionally weak because you probably should use the higher-level MySQLdb module. If you really need it, use the standard MySQL docs and transliterate as necessary. -.. _`MySQL documentation`: http://dev.mysql.com/doc/ +.. _`MySQL documentation`: https://dev.mysql.com/doc/ MySQL C API translation @@ -55,48 +55,47 @@ MySQL C API function mapping C API ``_mysql`` =================================== ================================== ``mysql_affected_rows()`` ``conn.affected_rows()`` - ``mysql_autocommit()`` ``conn.autocommit()`` - ``mysql_character_set_name()`` ``conn.character_set_name()`` - ``mysql_close()`` ``conn.close()`` - ``mysql_commit()`` ``conn.commit()`` - ``mysql_connect()`` ``_mysql.connect()`` - ``mysql_data_seek()`` ``result.data_seek()`` - ``mysql_debug()`` ``_mysql.debug()`` - ``mysql_dump_debug_info`` ``conn.dump_debug_info()`` - ``mysql_escape_string()`` ``_mysql.escape_string()`` - ``mysql_fetch_row()`` ``result.fetch_row()`` + ``mysql_autocommit()`` ``conn.autocommit()`` + ``mysql_character_set_name()`` ``conn.character_set_name()`` + ``mysql_close()`` ``conn.close()`` + ``mysql_commit()`` ``conn.commit()`` + ``mysql_connect()`` ``_mysql.connect()`` + ``mysql_data_seek()`` ``result.data_seek()`` + ``mysql_debug()`` ``_mysql.debug()`` + ``mysql_dump_debug_info`` ``conn.dump_debug_info()`` + ``mysql_escape_string()`` ``_mysql.escape_string()`` + ``mysql_fetch_row()`` ``result.fetch_row()`` ``mysql_get_character_set_info()`` ``conn.get_character_set_info()`` - ``mysql_get_client_info()`` ``_mysql.get_client_info()`` - ``mysql_get_host_info()`` ``conn.get_host_info()`` - ``mysql_get_proto_info()`` ``conn.get_proto_info()`` - ``mysql_get_server_info()`` ``conn.get_server_info()`` - ``mysql_info()`` ``conn.info()`` - ``mysql_insert_id()`` ``conn.insert_id()`` - ``mysql_num_fields()`` ``result.num_fields()`` - ``mysql_num_rows()`` ``result.num_rows()`` - ``mysql_options()`` various options to ``_mysql.connect()`` - ``mysql_ping()`` ``conn.ping()`` - ``mysql_query()`` ``conn.query()`` - ``mysql_real_connect()`` ``_mysql.connect()`` - ``mysql_real_query()`` ``conn.query()`` - ``mysql_real_escape_string()`` ``conn.escape_string()`` - ``mysql_rollback()`` ``conn.rollback()`` - ``mysql_row_seek()`` ``result.row_seek()`` - ``mysql_row_tell()`` ``result.row_tell()`` - ``mysql_select_db()`` ``conn.select_db()`` - ``mysql_set_character_set()`` ``conn.set_character_set()`` + ``mysql_get_client_info()`` ``_mysql.get_client_info()`` + ``mysql_get_host_info()`` ``conn.get_host_info()`` + ``mysql_get_proto_info()`` ``conn.get_proto_info()`` + ``mysql_get_server_info()`` ``conn.get_server_info()`` + ``mysql_info()`` ``conn.info()`` + ``mysql_insert_id()`` ``conn.insert_id()`` + ``mysql_num_fields()`` ``result.num_fields()`` + ``mysql_num_rows()`` ``result.num_rows()`` + ``mysql_options()`` various options to ``_mysql.connect()`` + ``mysql_ping()`` ``conn.ping()`` + ``mysql_query()`` ``conn.query()`` + ``mysql_real_connect()`` ``_mysql.connect()`` + ``mysql_real_query()`` ``conn.query()`` + ``mysql_real_escape_string()`` ``conn.escape_string()`` + ``mysql_rollback()`` ``conn.rollback()`` + ``mysql_row_seek()`` ``result.row_seek()`` + ``mysql_row_tell()`` ``result.row_tell()`` + ``mysql_select_db()`` ``conn.select_db()`` + ``mysql_set_character_set()`` ``conn.set_character_set()`` ``mysql_ssl_set()`` ``ssl`` option to ``_mysql.connect()`` - ``mysql_stat()`` ``conn.stat()`` - ``mysql_store_result()`` ``conn.store_result()`` - ``mysql_thread_id()`` ``conn.thread_id()`` - ``mysql_thread_safe_client()`` ``conn.thread_safe_client()`` - ``mysql_use_result()`` ``conn.use_result()`` - ``mysql_warning_count()`` ``conn.warning_count()`` - ``CLIENT_*`` ``MySQLdb.constants.CLIENT.*`` - ``CR_*`` ``MySQLdb.constants.CR.*`` - ``ER_*`` ``MySQLdb.constants.ER.*`` - ``FIELD_TYPE_*`` ``MySQLdb.constants.FIELD_TYPE.*`` - ``FLAG_*`` ``MySQLdb.constants.FLAG.*`` + ``mysql_stat()`` ``conn.stat()`` + ``mysql_store_result()`` ``conn.store_result()`` + ``mysql_thread_id()`` ``conn.thread_id()`` + ``mysql_use_result()`` ``conn.use_result()`` + ``mysql_warning_count()`` ``conn.warning_count()`` + ``CLIENT_*`` ``MySQLdb.constants.CLIENT.*`` + ``CR_*`` ``MySQLdb.constants.CR.*`` + ``ER_*`` ``MySQLdb.constants.ER.*`` + ``FIELD_TYPE_*`` ``MySQLdb.constants.FIELD_TYPE.*`` + ``FLAG_*`` ``MySQLdb.constants.FLAG.*`` =================================== ================================== @@ -107,7 +106,7 @@ Okay, so you want to use ``_mysql`` anyway. Here are some examples. The simplest possible database connection is:: - import _mysql + from MySQLdb import _mysql db=_mysql.connect() This creates a connection to the MySQL server running on the local @@ -126,19 +125,19 @@ We haven't even begun to touch upon all the parameters ``connect()`` can take. For this reason, I prefer to use keyword parameters:: db=_mysql.connect(host="localhost",user="joebob", - passwd="moonpie",db="thangs") + password="moonpie",database="thangs") This does exactly what the last example did, but is arguably easier to read. But since the default host is "localhost", and if your login name really was "joebob", you could shorten it to this:: - db=_mysql.connect(passwd="moonpie",db="thangs") + db=_mysql.connect(password="moonpie",database="thangs") UNIX sockets and named pipes don't work over a network, so if you specify a host other than localhost, TCP will be used, and you can specify an odd port if you need to (the default port is 3306):: - db=_mysql.connect(host="outhouse",port=3307,passwd="moonpie",db="thangs") + db=_mysql.connect(host="outhouse",port=3307,password="moonpie",database="thangs") If you really had to, you could connect to the local host with TCP by specifying the full host name, or 127.0.0.1. @@ -146,7 +145,7 @@ specifying the full host name, or 127.0.0.1. Generally speaking, putting passwords in your code is not such a good idea:: - db=_mysql.connect(host="outhouse",db="thangs",read_default_file="~/.my.cnf") + db=_mysql.connect(host="outhouse",database="thangs",read_default_file="~/.my.cnf") This does what the previous example does, but gets the username and password and other parameters from ~/.my.cnf (UNIX-like systems). Read @@ -163,8 +162,8 @@ substitution, so you have to pass a complete query string to WHERE price < 5""") There's no return value from this, but exceptions can be raised. The -exceptions are defined in a separate module, ``_mysql_exceptions``, -but ``_mysql`` exports them. Read DB API specification PEP-249_ to +exceptions are defined in a separate module, ``MySQLdb._exceptions``, +but ``MySQLdb._mysql`` exports them. Read DB API specification PEP-249_ to find out what they are, or you can use the catch-all ``MySQLError``. .. _PEP-249: https://www.python.org/dev/peps/pep-0249/ @@ -214,7 +213,7 @@ implicitly asked for one row, since we didn't specify ``maxrows``. The other oddity is: Assuming these are numeric columns, why are they returned as strings? Because MySQL returns all data as strings and expects you to convert it yourself. This would be a real pain in the -ass, but in fact, ``_mysql`` can do this for you. (And ``MySQLdb`` +ass, but in fact, ``MySQLdb._mysql`` can do this for you. (And ``MySQLdb`` does do this for you.) To have automatic type conversion done, you need to create a type converter dictionary, and pass this to ``connect()`` as the ``conv`` keyword parameter. @@ -260,35 +259,35 @@ Functions and attributes Only a few top-level functions and attributes are defined within MySQLdb. -connect(parameters...) - Constructor for creating a connection to the - database. Returns a Connection Object. Parameters are the - same as for the MySQL C API. In addition, there are a few - additional keywords that correspond to what you would pass - ``mysql_options()`` before connecting. Note that some - parameters must be specified as keyword arguments! The - default value for each parameter is NULL or zero, as - appropriate. Consult the MySQL documentation for more - details. The important parameters are: +connect(parameters...) + Constructor for creating a connection to the + database. Returns a Connection Object. Parameters are the + same as for the MySQL C API. In addition, there are a few + additional keywords that correspond to what you would pass + ``mysql_options()`` before connecting. Note that some + parameters must be specified as keyword arguments! The + default value for each parameter is NULL or zero, as + appropriate. Consult the MySQL documentation for more + details. The important parameters are: host - name of host to connect to. Default: use the local host + name of host to connect to. Default: use the local host via a UNIX socket (where applicable) user user to authenticate as. Default: current effective user. - passwd + password password to authenticate with. Default: no password. - db + database database to use. Default: no default database. - port - TCP port of MySQL server. Default: standard port (3306). + port + TCP port of MySQL server. Default: standard port (3306). unix_socket - location of UNIX socket. Default: use default location or + location of UNIX socket. Default: use default location or TCP for remote hosts. conv @@ -331,45 +330,88 @@ connect(parameters...) connecting (MySQL-4.1 and later), you'll need to put the correct character set name in connection.charset. - If False, text-like columns are returned as normal strings, - but you can always write Unicode strings. + If False, text-like columns are returned as normal strings, + but you can always write Unicode strings. - *This must be a keyword parameter.* + *This must be a keyword parameter.* - charset - If present, the connection character set will be changed - to this character set, if they are not equal. Support for - changing the character set requires MySQL-4.1 and later - server; if the server is too old, UnsupportedError will be - raised. This option implies use_unicode=True, but you can - override this with use_unicode=False, though you probably - shouldn't. + charset + If present, the connection character set will be changed + to this character set, if they are not equal. Support for + changing the character set requires MySQL-4.1 and later + server; if the server is too old, UnsupportedError will be + raised. This option implies use_unicode=True, but you can + override this with use_unicode=False, though you probably + shouldn't. - If not present, the default character set is used. + If not present, the default character set is used. - *This must be a keyword parameter.* + *This must be a keyword parameter.* - sql_mode - If present, the session SQL mode will be set to the given - string. For more information on sql_mode, see the MySQL - documentation. Only available for 4.1 and newer servers. + collation + If ``charset`` and ``collation`` are both supplied, the + character set and collation for the current connection + will be set. - If not present, the session SQL mode will be unchanged. + If omitted, empty string, or None, the default collation + for the ``charset`` is implied by the database server. - *This must be a keyword parameter.* + To learn more about the quiddities of character sets and + collations, consult the `MySQL docs + `_ + and `MariaDB docs + `_ - ssl - This parameter takes a dictionary or mapping, where the - keys are parameter names used by the mysql_ssl_set_ MySQL - C API call. If this is set, it initiates an SSL connection - to the server; if there is no SSL support in the client, - an exception is raised. *This must be a keyword - parameter.* + *This must be a keyword parameter.* + + sql_mode + If present, the session SQL mode will be set to the given + string. For more information on sql_mode, see the MySQL + documentation. Only available for 4.1 and newer servers. + + If not present, the session SQL mode will be unchanged. + + *This must be a keyword parameter.* + + ssl_mode + If present, specify the security settings for the + connection to the server. For more information on ssl_mode, + see the MySQL documentation. Only one of 'DISABLED', + 'PREFERRED', 'REQUIRED', 'VERIFY_CA', 'VERIFY_IDENTITY' + can be specified. + + If not present, the session ssl_mode will be unchanged, + but in version 5.7 and later, the default is PREFERRED. + + *This must be a keyword parameter.* + + ssl + This parameter takes a dictionary or mapping, where the + keys are parameter names used by the mysql_ssl_set_ MySQL + C API call. If this is set, it initiates an SSL connection + to the server; if there is no SSL support in the client, + an exception is raised. *This must be a keyword + parameter.* + + server_public_key_path + specifies path to a RSA public key used by caching sha2 password authentication. + See https://dev.mysql.com/doc/refman/9.0/en/caching-sha2-pluggable-authentication.html + + local_infile + sets ``MYSQL_OPT_LOCAL_INFILE`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; zero disables; + + *This must be a keyword parameter.* + + local_infile_dir + sets ``MYSQL_OPT_LOAD_DATA_LOCAL_DIR`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; + if ``local_infile`` is set to ``True`` then this is ignored; + + *This must be a keyword parameter.* .. _mysql_ssl_set: http://dev.mysql.com/doc/refman/en/mysql-ssl-set.html -apilevel +apilevel String constant stating the supported DB API level. '2.0' threadsafety @@ -437,11 +479,11 @@ conv value), returning a Python value * a sequence of 2-tuples, where the first value is a combination - of flags from ``MySQLdb.constants.FLAG``, and the second value - is a function as above. The sequence is tested until the flags - on the field match those of the first value. If both values - are None, then the default conversion is done. Presently this - is only used to distinguish TEXT and BLOB columns. + of flags from ``MySQLdb.constants.FLAG``, and the second value + is a function as above. The sequence is tested until the flags + on the field match those of the first value. If both values + are None, then the default conversion is done. Presently this + is only used to distinguish TEXT and BLOB columns. If the key is a Python type or class, then the value is a callable Python object (usually a function) taking two arguments @@ -500,7 +542,7 @@ callproc(procname, args) can only be returned with a SELECT statement. Since a stored procedure may return zero or more result sets, it is impossible for MySQLdb to determine if there are result sets to fetch - before the modified parmeters are accessible. + before the modified parameters are accessible. The parameters are stored in the server as @_*procname*_*n*, where *n* is the position of the parameter. I.e., if you @@ -522,7 +564,7 @@ close() info() Returns some information about the last query. Normally - you don't need to check this. If there are any MySQL + you don't need to check this. If there are any MySQL warnings, it will cause a Warning to be issued through the Python warning module. By default, Warning causes a message to appear on the console. However, it is possible @@ -542,15 +584,15 @@ nextset() sets, it returns None; otherwise it returns a true value. Note that MySQL doesn't support multiple result sets until 4.1. - + Some examples ............. -The ``connect()`` method works nearly the same as with `_mysql`_:: +The ``connect()`` method works nearly the same as with `MySQLDB._mysql`_:: import MySQLdb - db=MySQLdb.connect(passwd="moonpie",db="thangs") + db=MySQLdb.connect(password="moonpie",database="thangs") To perform a query, you first need a cursor, and then you can execute queries on it:: @@ -598,13 +640,13 @@ The only other method you are very likely to use is when you have to do a multi-row insert:: c.executemany( - """INSERT INTO breakfast (name, spam, eggs, sausage, price) - VALUES (%s, %s, %s, %s, %s)""", - [ - ("Spam and Sausage Lover's Plate", 5, 1, 8, 7.95 ), - ("Not So Much Spam Plate", 3, 2, 0, 3.95 ), - ("Don't Wany ANY SPAM! Plate", 0, 4, 3, 5.95 ) - ] ) + """INSERT INTO breakfast (name, spam, eggs, sausage, price) + VALUES (%s, %s, %s, %s, %s)""", + [ + ("Spam and Sausage Lover's Plate", 5, 1, 8, 7.95 ), + ("Not So Much Spam Plate", 3, 2, 0, 3.95 ), + ("Don't Wany ANY SPAM! Plate", 0, 4, 3, 5.95 ) + ] ) Here we are inserting three rows of five values. Notice that there is a mix of types (strings, ints, floats) though we still only use @@ -663,10 +705,9 @@ CursorDictRowsMixIn Cursor The default cursor class. This class is composed of - ``CursorWarningMixIn``, ``CursorStoreResultMixIn``, - ``CursorTupleRowsMixIn,`` and ``BaseCursor``, i.e. it raises - ``Warning``, uses ``mysql_store_result()``, and returns rows as - tuples. + ``CursorStoreResultMixIn``, ``CursorTupleRowsMixIn``, and + ``BaseCursor``, i.e. uses ``mysql_store_result()`` and returns + rows as tuples. DictCursor Like ``Cursor`` except it returns rows as dictionaries. diff --git a/metadata.cfg b/metadata.cfg deleted file mode 100644 index 6901e7af..00000000 --- a/metadata.cfg +++ /dev/null @@ -1,45 +0,0 @@ -[metadata] -version: 1.3.14 -version_info: (1,3,14,'final',0) -description: Python interface to MySQL -author: Andy Dustman -author_email: farcepest@gmail.com -maintainer: INADA Naoki -maintainer_email: songofacandy@gmail.com -license: GPL -platforms: ALL -url: https://github.com/PyMySQL/mysqlclient-python -classifiers: - Development Status :: 5 - Production/Stable - Environment :: Other Environment - License :: OSI Approved :: GNU General Public License (GPL) - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows :: Windows NT/2000 - Operating System :: OS Independent - Operating System :: POSIX - Operating System :: POSIX :: Linux - Operating System :: Unix - Programming Language :: C - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Topic :: Database - Topic :: Database :: Database Engines/Servers -py_modules: - MySQLdb._mysql_exceptions - MySQLdb.compat - MySQLdb.connections - MySQLdb.converters - MySQLdb.cursors - MySQLdb.release - MySQLdb.times - MySQLdb.constants.CLIENT - MySQLdb.constants.CR - MySQLdb.constants.ER - MySQLdb.constants.FIELD_TYPE - MySQLdb.constants.FLAG - MySQLdb.constants.REFRESH diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d786f336 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[project] +name = "mysqlclient" +description = "Python interface to MySQL" +readme = "README.md" +requires-python = ">=3.8" +authors = [ + {name = "Inada Naoki", email = "songofacandy@gmail.com"} +] +license = {text = "GNU General Public License v2 or later (GPLv2+)"} +keywords = ["MySQL"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Other Environment", + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows :: Windows NT/2000", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: POSIX :: Linux", + "Operating System :: Unix", + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", + "Topic :: Database :: Database Engines/Servers", +] +dynamic = ["version"] + +[project.urls] +Project = "https://github.com/PyMySQL/mysqlclient" +Documentation = "https://mysqlclient.readthedocs.io/" + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +namespaces = false +where = ["src"] +include = ["MySQLdb*"] + +[tool.setuptools.dynamic] +version = {attr = "MySQLdb.release.__version__"} diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e2546870 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# This file is for GitHub Action +coverage +pytest +pytest-cov +tblib diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index eb5b5513..00000000 --- a/setup.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[build_ext] -## Only uncomment/set these if the default configuration doesn't work -## Also see https://docs.python.org/distutils/configfile.html -# include-dirs = ? -# library-dirs = ? -# link-objects = ? -# rpath = ? -# libraries = ? - -[bdist_rpm] -doc_files = README MANIFEST doc/*.txt -vendor = MySQL-python SourceForge Project -packager = Andy Dustman -distribution-name = Red Stains Linux -requires = python -build-requires = python-devel mysql-devel zlib-devel openssl-devel diff --git a/setup.py b/setup.py index d1029962..9a1d26b8 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,169 @@ #!/usr/bin/env python - import os -import io +import subprocess +import sys import setuptools +from configparser import ConfigParser + + +release_info = {} +with open("src/MySQLdb/release.py", encoding="utf-8") as f: + exec(f.read(), None, release_info) + + +def find_package_name(): + """Get available pkg-config package name""" + # Ubuntu uses mariadb.pc, but CentOS uses libmariadb.pc + packages = ["mysqlclient", "mariadb", "libmariadb", "perconaserverclient"] + for pkg in packages: + try: + cmd = f"pkg-config --exists {pkg}" + print(f"Trying {cmd}") + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print(err) + else: + return pkg + raise Exception( + "Can not find valid pkg-config name.\n" + "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" + ) + + +def get_config_posix(options=None): + # allow a command-line option to override the base config file to permit + # a static build to be created via requirements.txt + # TODO: find a better way for + static = False + if "--static" in sys.argv: + static = True + sys.argv.remove("--static") + + ldflags = os.environ.get("MYSQLCLIENT_LDFLAGS") + cflags = os.environ.get("MYSQLCLIENT_CFLAGS") + + pkg_name = None + static_opt = " --static" if static else "" + if not (cflags and ldflags): + pkg_name = find_package_name() + if not cflags: + cflags = subprocess.check_output( + f"pkg-config{static_opt} --cflags {pkg_name}", encoding="utf-8", shell=True + ) + if not ldflags: + ldflags = subprocess.check_output( + f"pkg-config{static_opt} --libs {pkg_name}", encoding="utf-8", shell=True + ) + + cflags = cflags.split() + for f in cflags: + if f.startswith("-std="): + break + else: + cflags += ["-std=c99"] + + ldflags = ldflags.split() + + define_macros = [ + ("version_info", release_info["version_info"]), + ("__version__", release_info["__version__"]), + ] + + ext_options = dict( + extra_compile_args=cflags, + extra_link_args=ldflags, + define_macros=define_macros, + ) + # newer versions of gcc require libstdc++ if doing a static build + if static: + ext_options["language"] = "c++" + + return ext_options + + +def get_config_win32(options): + client = "mariadbclient" + connector = os.environ.get("MYSQLCLIENT_CONNECTOR", options.get("connector")) + if not connector: + connector = os.path.join( + os.environ["ProgramFiles"], "MariaDB", "MariaDB Connector C" + ) + + extra_objects = [] + + library_dirs = [ + os.path.join(connector, "lib", "mariadb"), + os.path.join(connector, "lib"), + ] + libraries = [ + "kernel32", + "advapi32", + "wsock32", + "shlwapi", + "Ws2_32", + "crypt32", + "secur32", + "bcrypt", + client, + ] + include_dirs = [ + os.path.join(connector, "include", "mariadb"), + os.path.join(connector, "include", "mysql"), + os.path.join(connector, "include"), + ] + + extra_link_args = ["/MANIFEST"] + + define_macros = [ + ("version_info", release_info["version_info"]), + ("__version__", release_info["__version__"]), + ] + + ext_options = dict( + library_dirs=library_dirs, + libraries=libraries, + extra_link_args=extra_link_args, + include_dirs=include_dirs, + extra_objects=extra_objects, + define_macros=define_macros, + ) + return ext_options + + +def enabled(options, option): + value = options[option] + s = value.lower() + if s in ("yes", "true", "1", "y"): + return True + elif s in ("no", "false", "0", "n"): + return False + else: + raise ValueError(f"Unknown value {value} for option {option}") + + +def get_options(): + config = ConfigParser() + config.read(["site.cfg"]) + options = dict(config.items("options")) + options["static"] = enabled(options, "static") + return options + -if os.name == "posix": - from setup_posix import get_config -else: # assume windows - from setup_windows import get_config +if sys.platform == "win32": + ext_options = get_config_win32(get_options()) +else: + ext_options = get_config_posix(get_options()) -with io.open('README.md', encoding='utf-8') as f: - readme = f.read() +print("# Options for building extension module:") +for k, v in ext_options.items(): + print(f" {k}: {v}") -metadata, options = get_config() -metadata['ext_modules'] = [ - setuptools.Extension("MySQLdb._mysql", sources=['MySQLdb/_mysql.c'], **options) +ext_modules = [ + setuptools.Extension( + "MySQLdb._mysql", + sources=["src/MySQLdb/_mysql.c"], + **ext_options, + ) ] -metadata['long_description'] = readme -metadata['long_description_content_type'] = "text/markdown" -setuptools.setup(**metadata) +setuptools.setup(ext_modules=ext_modules) diff --git a/setup_common.py b/setup_common.py deleted file mode 100644 index 03c39bb7..00000000 --- a/setup_common.py +++ /dev/null @@ -1,37 +0,0 @@ -try: - # Python 2.x - from ConfigParser import SafeConfigParser -except ImportError: - # Python 3.x - from configparser import ConfigParser as SafeConfigParser - -def get_metadata_and_options(): - config = SafeConfigParser() - config.read(['metadata.cfg', 'site.cfg']) - - metadata = dict(config.items('metadata')) - options = dict(config.items('options')) - - metadata['py_modules'] = list(filter(None, metadata['py_modules'].split('\n'))) - metadata['classifiers'] = list(filter(None, metadata['classifiers'].split('\n'))) - - return metadata, options - -def enabled(options, option): - value = options[option] - s = value.lower() - if s in ('yes','true','1','y'): - return True - elif s in ('no', 'false', '0', 'n'): - return False - else: - raise ValueError("Unknown value %s for option %s" % (value, option)) - -def create_release_file(metadata): - rel = open("MySQLdb/release.py",'w') - rel.write(""" -__author__ = "%(author)s <%(author_email)s>" -version_info = %(version_info)s -__version__ = "%(version)s" -""" % metadata) - rel.close() diff --git a/setup_posix.py b/setup_posix.py deleted file mode 100644 index 9289bb50..00000000 --- a/setup_posix.py +++ /dev/null @@ -1,113 +0,0 @@ -import os, sys -try: - from ConfigParser import SafeConfigParser -except ImportError: - from configparser import ConfigParser as SafeConfigParser - -# This dequote() business is required for some older versions -# of mysql_config - -def dequote(s): - if not s: - raise Exception("Wrong MySQL configuration: maybe https://bugs.mysql.com/bug.php?id=86971 ?") - if s[0] in "\"'" and s[0] == s[-1]: - s = s[1:-1] - return s - -_mysql_config_path = "mysql_config" - -def mysql_config(what): - from os import popen - - f = popen("%s --%s" % (_mysql_config_path, what)) - data = f.read().strip().split() - ret = f.close() - if ret: - if ret/256: - data = [] - if ret/256 > 1: - raise EnvironmentError("%s not found" % (_mysql_config_path,)) - return data - -def get_config(): - from setup_common import get_metadata_and_options, enabled, create_release_file - global _mysql_config_path - - metadata, options = get_metadata_and_options() - - if 'mysql_config' in options: - _mysql_config_path = options['mysql_config'] - - extra_objects = [] - static = enabled(options, 'static') - - # allow a command-line option to override the base config file to permit - # a static build to be created via requirements.txt - # - if '--static' in sys.argv: - static = True - sys.argv.remove('--static') - - libs = mysql_config("libs") - library_dirs = [dequote(i[2:]) for i in libs if i.startswith('-L')] - libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')] - extra_link_args = [x for x in libs if not x.startswith(('-l', '-L'))] - - removable_compile_args = ('-I', '-L', '-l') - extra_compile_args = [i.replace("%", "%%") for i in mysql_config("cflags") - if i[:2] not in removable_compile_args] - - # Copy the arch flags for linking as well - for i in range(len(extra_compile_args)): - if extra_compile_args[i] == '-arch': - extra_link_args += ['-arch', extra_compile_args[i + 1]] - - include_dirs = [dequote(i[2:]) - for i in mysql_config('include') if i.startswith('-I')] - - if static: - # properly handle mysql client libraries that are not called libmysqlclient - client = None - CLIENT_LIST = ['mysqlclient', 'mysqlclient_r', 'mysqld', 'mariadb', - 'perconaserverclient', 'perconaserverclient_r'] - for c in CLIENT_LIST: - if c in libraries: - client = c - break - - if client == 'mariadb': - client = 'mariadbclient' - if client is None: - raise ValueError("Couldn't identify mysql client library") - - extra_objects.append(os.path.join(library_dirs[0], 'lib%s.a' % client)) - if client in libraries: - libraries.remove(client) - - name = "mysqlclient" - metadata['name'] = name - - define_macros = [ - ('version_info', metadata['version_info']), - ('__version__', metadata['version']), - ] - create_release_file(metadata) - del metadata['version_info'] - ext_options = dict( - library_dirs = library_dirs, - libraries = libraries, - extra_compile_args = extra_compile_args, - extra_link_args = extra_link_args, - include_dirs = include_dirs, - extra_objects = extra_objects, - define_macros = define_macros, - ) - - # newer versions of gcc require libstdc++ if doing a static build - if static: - ext_options['language'] = 'c++' - - return metadata, ext_options - -if __name__ == "__main__": - sys.stderr.write("""You shouldn't be running this directly; it is used by setup.py.""") diff --git a/setup_windows.py b/setup_windows.py deleted file mode 100644 index 0811e127..00000000 --- a/setup_windows.py +++ /dev/null @@ -1,50 +0,0 @@ -import os, sys -from distutils.msvccompiler import get_build_version - - -def get_config(): - from setup_common import get_metadata_and_options, enabled, create_release_file - - metadata, options = get_metadata_and_options() - - connector = options["connector"] - - extra_objects = [] - - if enabled(options, 'embedded'): - client = "mysqld" - else: - client = "mysqlclient" - - vcversion = int(get_build_version()) - library_dirs = [ os.path.join(connector, r'lib\vs%d' % vcversion) ] - libraries = [ 'kernel32', 'advapi32', 'wsock32', client ] - include_dirs = [ os.path.join(connector, r'include') ] - extra_compile_args = [ '/Zl' ] - extra_link_args = ['/MANIFEST'] - - name = "mysqlclient" - if enabled(options, 'embedded'): - name = name + "-embedded" - metadata['name'] = name - - define_macros = [ - ('version_info', metadata['version_info']), - ('__version__', metadata['version']), - ] - create_release_file(metadata) - del metadata['version_info'] - ext_options = dict( - library_dirs = library_dirs, - libraries = libraries, - extra_compile_args = extra_compile_args, - extra_link_args = extra_link_args, - include_dirs = include_dirs, - extra_objects = extra_objects, - define_macros = define_macros, - ) - return metadata, ext_options - -if __name__ == "__main__": - sys.stderr.write("""You shouldn't be running this directly; it is used by setup.py.""") - diff --git a/site.cfg b/site.cfg index 6b4596a4..39e3c2b1 100644 --- a/site.cfg +++ b/site.cfg @@ -2,11 +2,6 @@ # static: link against a static library static = False -# The path to mysql_config. -# Only use this if mysql_config is not on your PATH, or you have some weird -# setup that requires it. -#mysql_config = /usr/local/bin/mysql_config - # http://stackoverflow.com/questions/1972259/mysql-python-install-problem-using-virtualenv-windows-pip # Windows connector libs for MySQL. You need a 32-bit connector for your 32-bit Python build. -connector = C:\Program Files (x86)\MySQL\MySQL Connector C 6.1 +connector = diff --git a/src/MySQLdb/__init__.py b/src/MySQLdb/__init__.py new file mode 100644 index 00000000..153bbdfe --- /dev/null +++ b/src/MySQLdb/__init__.py @@ -0,0 +1,168 @@ +""" +MySQLdb - A DB API v2.0 compatible interface to MySQL. + +This package is a wrapper around _mysql, which mostly implements the +MySQL C API. + +connect() -- connects to server + +See the C API specification and the MySQL documentation for more info +on other items. + +For information on how MySQLdb handles type conversion, see the +MySQLdb.converters module. +""" + +from .release import version_info +from . import _mysql + +if version_info != _mysql.version_info: + raise ImportError( + f"this is MySQLdb version {version_info}, " + f"but _mysql is version {_mysql.version_info!r}\n" + f"_mysql: {_mysql.__file__!r}" + ) + + +from ._mysql import ( + NotSupportedError, + OperationalError, + get_client_info, + ProgrammingError, + Error, + InterfaceError, + debug, + IntegrityError, + string_literal, + MySQLError, + DataError, + DatabaseError, + InternalError, + Warning, +) +from MySQLdb.constants import FIELD_TYPE +from MySQLdb.times import ( + Date, + Time, + Timestamp, + DateFromTicks, + TimeFromTicks, + TimestampFromTicks, +) + +threadsafety = 1 +apilevel = "2.0" +paramstyle = "format" + + +class DBAPISet(frozenset): + """A special type of set for which A == x is true if A is a + DBAPISet and x is a member of that set.""" + + def __eq__(self, other): + if isinstance(other, DBAPISet): + return not self.difference(other) + return other in self + + +STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING]) +BINARY = DBAPISet( + [ + FIELD_TYPE.BLOB, + FIELD_TYPE.LONG_BLOB, + FIELD_TYPE.MEDIUM_BLOB, + FIELD_TYPE.TINY_BLOB, + ] +) +NUMBER = DBAPISet( + [ + FIELD_TYPE.DECIMAL, + FIELD_TYPE.DOUBLE, + FIELD_TYPE.FLOAT, + FIELD_TYPE.INT24, + FIELD_TYPE.LONG, + FIELD_TYPE.LONGLONG, + FIELD_TYPE.TINY, + FIELD_TYPE.YEAR, + FIELD_TYPE.NEWDECIMAL, + ] +) +DATE = DBAPISet([FIELD_TYPE.DATE]) +TIME = DBAPISet([FIELD_TYPE.TIME]) +TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME]) +DATETIME = TIMESTAMP +ROWID = DBAPISet() + + +def test_DBAPISet_set_equality(): + assert STRING == STRING + + +def test_DBAPISet_set_inequality(): + assert STRING != NUMBER + + +def test_DBAPISet_set_equality_membership(): + assert FIELD_TYPE.VAR_STRING == STRING + + +def test_DBAPISet_set_inequality_membership(): + assert FIELD_TYPE.DATE != STRING + + +def Binary(x): + return bytes(x) + + +def Connect(*args, **kwargs): + """Factory function for connections.Connection.""" + from MySQLdb.connections import Connection + + return Connection(*args, **kwargs) + + +connect = Connection = Connect + +__all__ = [ + "BINARY", + "Binary", + "Connect", + "Connection", + "DATE", + "Date", + "Time", + "Timestamp", + "DateFromTicks", + "TimeFromTicks", + "TimestampFromTicks", + "DataError", + "DatabaseError", + "Error", + "FIELD_TYPE", + "IntegrityError", + "InterfaceError", + "InternalError", + "MySQLError", + "NUMBER", + "NotSupportedError", + "DBAPISet", + "OperationalError", + "ProgrammingError", + "ROWID", + "STRING", + "TIME", + "TIMESTAMP", + "Warning", + "apilevel", + "connect", + "connections", + "constants", + "converters", + "cursors", + "debug", + "get_client_info", + "paramstyle", + "string_literal", + "threadsafety", + "version_info", +] diff --git a/MySQLdb/_mysql_exceptions.py b/src/MySQLdb/_exceptions.py similarity index 84% rename from MySQLdb/_mysql_exceptions.py rename to src/MySQLdb/_exceptions.py index 74b765a7..a5aca7e1 100644 --- a/MySQLdb/_mysql_exceptions.py +++ b/src/MySQLdb/_exceptions.py @@ -1,87 +1,91 @@ -"""_mysql_exceptions: Exception classes for _mysql and MySQLdb. +"""Exception classes for _mysql and MySQLdb. These classes are dictated by the DB API v2.0: https://www.python.org/dev/peps/pep-0249/ """ -try: - from exceptions import Exception, StandardError, Warning -except ImportError: - # Python 3 - StandardError = Exception - -class MySQLError(StandardError): - +class MySQLError(Exception): """Exception related to operation with MySQL.""" + __module__ = "MySQLdb" -class Warning(Warning, MySQLError): +class Warning(Warning, MySQLError): """Exception raised for important warnings like data truncations while inserting, etc.""" -class Error(MySQLError): + __module__ = "MySQLdb" + +class Error(MySQLError): """Exception that is the base class of all other error exceptions (not Warning).""" + __module__ = "MySQLdb" -class InterfaceError(Error): +class InterfaceError(Error): """Exception raised for errors that are related to the database interface rather than the database itself.""" + __module__ = "MySQLdb" -class DatabaseError(Error): +class DatabaseError(Error): """Exception raised for errors that are related to the database.""" + __module__ = "MySQLdb" -class DataError(DatabaseError): +class DataError(DatabaseError): """Exception raised for errors that are due to problems with the processed data like division by zero, numeric value out of range, etc.""" + __module__ = "MySQLdb" -class OperationalError(DatabaseError): +class OperationalError(DatabaseError): """Exception raised for errors that are related to the database's operation and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs, the data source name is not found, a transaction could not be processed, a memory allocation error occurred during processing, etc.""" + __module__ = "MySQLdb" -class IntegrityError(DatabaseError): +class IntegrityError(DatabaseError): """Exception raised when the relational integrity of the database is affected, e.g. a foreign key check fails, duplicate key, etc.""" + __module__ = "MySQLdb" -class InternalError(DatabaseError): +class InternalError(DatabaseError): """Exception raised when the database encounters an internal error, e.g. the cursor is not valid anymore, the transaction is out of sync, etc.""" + __module__ = "MySQLdb" -class ProgrammingError(DatabaseError): +class ProgrammingError(DatabaseError): """Exception raised for programming errors, e.g. table not found or already exists, syntax error in the SQL statement, wrong number of parameters specified, etc.""" + __module__ = "MySQLdb" -class NotSupportedError(DatabaseError): +class NotSupportedError(DatabaseError): """Exception raised in case a method or database API was used which is not supported by the database, e.g. requesting a .rollback() on a connection that does not support transaction or has transactions turned off.""" - + __module__ = "MySQLdb" diff --git a/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c similarity index 75% rename from MySQLdb/_mysql.c rename to src/MySQLdb/_mysql.c index ce88d03c..cd95b641 100644 --- a/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -25,22 +25,38 @@ USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ - +#include #include "mysql.h" #include "mysqld_error.h" #if MYSQL_VERSION_ID >= 80000 // https://github.com/mysql/mysql-server/commit/eb821c023cedc029ca0b06456dfae365106bee84 -#define my_bool _Bool +// my_bool was typedef of char before MySQL 8.0.0. +#define my_bool bool +#endif + +#if ((MYSQL_VERSION_ID >= 50555 && MYSQL_VERSION_ID <= 50599) || \ + (MYSQL_VERSION_ID >= 50636 && MYSQL_VERSION_ID <= 50699) || \ + (MYSQL_VERSION_ID >= 50711 && MYSQL_VERSION_ID <= 50799) || \ + (MYSQL_VERSION_ID >= 80000)) && \ + !defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID) +#define HAVE_ENUM_MYSQL_OPT_SSL_MODE +#endif + +#if defined(MARIADB_VERSION_ID) && MARIADB_VERSION_ID >= 100403 || \ + !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 50723 +#define HAVE_MYSQL_SERVER_PUBLIC_KEY #endif +#if !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 80021 +#define HAVE_MYSQL_OPT_LOCAL_INFILE_DIR +#endif + +#define PY_SSIZE_T_CLEAN 1 #include "Python.h" -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#define PyInt_FromLong(n) PyLong_FromLong(n) -#define PyInt_Check(n) PyLong_Check(n) -#define PyInt_AS_LONG(n) PyLong_AS_LONG(n) -#define PyString_FromString(s) PyUnicode_FromString(s) + +#if PY_MAJOR_VERSION == 2 +#error "Python 2 is not supported" #endif #include "bytesobject.h" @@ -65,7 +81,8 @@ static PyObject *_mysql_NotSupportedError; typedef struct { PyObject_HEAD MYSQL connection; - int open; + bool open; + bool reconnect; PyObject *converter; } _mysql_ConnectionObject; @@ -87,17 +104,12 @@ typedef struct { int use; char has_next; PyObject *converter; + const char *encoding; } _mysql_ResultObject; extern PyTypeObject _mysql_ResultObject_Type; -/* According to https://dev.mysql.com/doc/refman/5.1/en/mysql-options.html - The MYSQL_OPT_READ_TIMEOUT appear in the version 5.1.12 */ -#if MYSQL_VERSION_ID > 50112 -#define HAVE_MYSQL_OPT_TIMEOUTS 1 -#endif - PyObject * _mysql_Exception(_mysql_ConnectionObject *c) { @@ -179,6 +191,7 @@ _mysql_Exception(_mysql_ConnectionObject *c) #ifdef ER_NO_DEFAULT_FOR_FIELD case ER_NO_DEFAULT_FOR_FIELD: #endif + case ER_BAD_NULL_ERROR: e = _mysql_IntegrityError; break; #ifdef ER_WARNING_NOT_COMPLETE_ROLLBACK @@ -202,21 +215,33 @@ _mysql_Exception(_mysql_ConnectionObject *c) e = _mysql_OperationalError; break; } - PyTuple_SET_ITEM(t, 0, PyInt_FromLong((long)merr)); - PyTuple_SET_ITEM(t, 1, PyString_FromString(mysql_error(&(c->connection)))); + PyTuple_SET_ITEM(t, 0, PyLong_FromLong((long)merr)); + PyTuple_SET_ITEM(t, 1, PyUnicode_FromString(mysql_error(&(c->connection)))); PyErr_SetObject(e, t); Py_DECREF(t); return NULL; } -static char _mysql_thread_safe__doc__[] = -"Indicates whether the client is compiled as thread-safe."; +static const char *utf8 = "utf8"; -static PyObject *_mysql_thread_safe( - PyObject *self, - PyObject *noargs) +static const char* +_get_encoding(MYSQL *mysql) { - return PyInt_FromLong((long)mysql_thread_safe()); + MY_CHARSET_INFO cs; + mysql_get_character_set_info(mysql, &cs); + if (strncmp(utf8, cs.csname, 4) == 0) { // utf8, utf8mb3, utf8mb4 + return utf8; + } + else if (strncmp("latin1", cs.csname, 6) == 0) { + return "cp1252"; + } + else if (strncmp("koi8r", cs.csname, 5) == 0) { + return "koi8_r"; + } + else if (strncmp("koi8u", cs.csname, 5) == 0) { + return "koi8_u"; + } + return cs.csname; } static char _mysql_ResultObject__doc__[] = @@ -257,6 +282,9 @@ _mysql_ResultObject_Initialize( self->result = result; self->has_next = (char)mysql_more_results(&(conn->connection)); Py_END_ALLOW_THREADS ; + + self->encoding = _get_encoding(&(conn->connection)); + //fprintf(stderr, "encoding=%s\n", self->encoding); if (!result) { if (mysql_errno(&(conn->connection))) { _mysql_Exception(conn); @@ -273,7 +301,7 @@ _mysql_ResultObject_Initialize( fields = mysql_fetch_fields(result); for (i=0; iconverter = NULL; - self->open = 0; + self->open = false; + self->reconnect = false; if (!PyArg_ParseTupleAndKeywords(args, kwargs, -#ifdef HAVE_MYSQL_OPT_TIMEOUTS - "|ssssisOiiisssiOiii:connect", -#else - "|ssssisOiiisssiOi:connect", -#endif + "|ssssisOiiisssiOsiiissss:connect", kwlist, &host, &user, &passwd, &db, &port, &unix_socket, &conv, @@ -413,42 +468,75 @@ _mysql_ConnectionObject_Initialize( &compress, &named_pipe, &init_command, &read_default_file, &read_default_group, - &client_flag, &ssl, - &local_infile -#ifdef HAVE_MYSQL_OPT_TIMEOUTS - , &read_timeout - , &write_timeout -#endif + &client_flag, &ssl, &ssl_mode, + &local_infile, + &read_timeout, + &write_timeout, + &charset, + &auth_plugin, + &server_public_key_path, + &local_infile_dir )) return -1; -#ifdef IS_PY3K +#ifndef HAVE_MYSQL_SERVER_PUBLIC_KEY + if (server_public_key_path) { + PyErr_SetString(_mysql_NotSupportedError, "server_public_key_path is not supported"); + return -1; + } +#endif + +#ifndef HAVE_MYSQL_OPT_LOCAL_INFILE_DIR + if (local_infile_dir) { + PyErr_SetString(_mysql_NotSupportedError, "local_infile_dir is not supported"); + return -1; + } +#endif + // For compatibility with PyPy, we need to keep strong reference + // to unicode objects until we use UTF8. #define _stringsuck(d,t,s) {t=PyMapping_GetItemString(s,#d);\ if(t){d=PyUnicode_AsUTF8(t);ssl_keepref[n_ssl_keepref++]=t;}\ PyErr_Clear();} -#else -#define _stringsuck(d,t,s) {t=PyMapping_GetItemString(s,#d);\ - if(t){d=PyString_AsString(t);ssl_keepref[n_ssl_keepref++]=t;}\ - PyErr_Clear();} -#endif + char ssl_mode_set = 0; if (ssl) { - PyObject *value = NULL; - _stringsuck(ca, value, ssl); - _stringsuck(capath, value, ssl); - _stringsuck(cert, value, ssl); - _stringsuck(key, value, ssl); - _stringsuck(cipher, value, ssl); + if (PyMapping_Check(ssl)) { + PyObject *value = NULL; + _stringsuck(ca, value, ssl); + _stringsuck(capath, value, ssl); + _stringsuck(cert, value, ssl); + _stringsuck(key, value, ssl); + _stringsuck(cipher, value, ssl); + } else if (PyObject_IsTrue(ssl)) { + // Support ssl=True from mysqlclient 2.2.4. + // for compatibility with PyMySQL and mysqlclient==2.2.1&libmariadb. + ssl_mode_num = SSLMODE_REQUIRED; + ssl_mode_set = 1; + } else { + ssl_mode_num = SSLMODE_DISABLED; + ssl_mode_set = 1; + } + } + if (ssl_mode) { + if ((ssl_mode_num = _get_ssl_mode_num(ssl_mode)) <= 0) { + PyErr_SetString(_mysql_NotSupportedError, "Unknown ssl_mode specification"); + return -1; + } + ssl_mode_set = 1; } - Py_BEGIN_ALLOW_THREADS ; conn = mysql_init(&(self->connection)); + if (!conn) { + PyErr_SetNone(PyExc_MemoryError); + return -1; + } + self->open = true; + if (connect_timeout) { unsigned int timeout = connect_timeout; mysql_options(&(self->connection), MYSQL_OPT_CONNECT_TIMEOUT, (char *)&timeout); } -#ifdef HAVE_MYSQL_OPT_TIMEOUTS if (read_timeout) { unsigned int timeout = read_timeout; mysql_options(&(self->connection), MYSQL_OPT_READ_TIMEOUT, @@ -459,7 +547,6 @@ _mysql_ConnectionObject_Initialize( mysql_options(&(self->connection), MYSQL_OPT_WRITE_TIMEOUT, (char *)&timeout); } -#endif if (compress != -1) { mysql_options(&(self->connection), MYSQL_OPT_COMPRESS, 0); client_flag |= CLIENT_COMPRESS; @@ -477,21 +564,64 @@ _mysql_ConnectionObject_Initialize( mysql_options(&(self->connection), MYSQL_OPT_LOCAL_INFILE, (char *) &local_infile); if (ssl) { - mysql_ssl_set(&(self->connection), key, cert, ca, capath, cipher); + mysql_options(&(self->connection), MYSQL_OPT_SSL_KEY, key); + mysql_options(&(self->connection), MYSQL_OPT_SSL_CERT, cert); + mysql_options(&(self->connection), MYSQL_OPT_SSL_CA, ca); + mysql_options(&(self->connection), MYSQL_OPT_SSL_CAPATH, capath); + mysql_options(&(self->connection), MYSQL_OPT_SSL_CIPHER, cipher); + } + for (int i=0 ; iconnection), host, user, passwd, db, - port, unix_socket, client_flag); +#ifdef HAVE_ENUM_MYSQL_OPT_SSL_MODE + if (ssl_mode_set) { + mysql_options(&(self->connection), MYSQL_OPT_SSL_MODE, &ssl_mode_num); + } +#else + // MariaDB doesn't support MYSQL_OPT_SSL_MODE. + // See https://github.com/PyMySQL/mysqlclient/issues/474 + // And MariDB 11.4 changed the default value of MYSQL_OPT_SSL_ENFORCE and + // MYSQL_OPT_SSL_VERIFY_SERVER_CERT to 1. + // https://github.com/mariadb-corporation/mariadb-connector-c/commit/8dffd56936df3d03eeccf47904773860a0cdeb57 + // We emulate the ssl_mode and old behavior. + my_bool my_true = 1; + my_bool my_false = 0; + if (ssl_mode_num >= SSLMODE_REQUIRED) { + mysql_options(&(self->connection), MYSQL_OPT_SSL_ENFORCE, (void *)&my_true); + } else { + mysql_options(&(self->connection), MYSQL_OPT_SSL_ENFORCE, (void *)&my_false); + } + if (ssl_mode_num >= SSLMODE_VERIFY_CA) { + mysql_options(&(self->connection), MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (void *)&my_true); + } else { + mysql_options(&(self->connection), MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (void *)&my_false); + } +#endif - Py_END_ALLOW_THREADS ; + if (charset) { + mysql_options(&(self->connection), MYSQL_SET_CHARSET_NAME, charset); + } + if (auth_plugin) { + mysql_options(&(self->connection), MYSQL_DEFAULT_AUTH, auth_plugin); + } +#ifdef HAVE_MYSQL_SERVER_PUBLIC_KEY + if (server_public_key_path) { + mysql_options(&(self->connection), MYSQL_SERVER_PUBLIC_KEY, server_public_key_path); + } +#endif - if (ssl) { - int i; - for (i=0; iconnection), MYSQL_OPT_LOAD_DATA_LOCAL_DIR, local_infile_dir); } +#endif + + Py_BEGIN_ALLOW_THREADS + conn = mysql_real_connect(&(self->connection), host, user, passwd, db, + port, unix_socket, client_flag); + Py_END_ALLOW_THREADS if (!conn) { _mysql_Exception(self); @@ -514,7 +644,6 @@ _mysql_ConnectionObject_Initialize( be done here. tp_dealloc still needs to call PyObject_GC_UnTrack(), however. */ - self->open = 1; return 0; } @@ -529,10 +658,10 @@ host\n\ user\n\ string, user to connect as\n\ \n\ -passwd\n\ +password\n\ string, password to use\n\ \n\ -db\n\ +database\n\ string, database to use\n\ \n\ port\n\ @@ -619,7 +748,7 @@ _mysql_ConnectionObject_close( Py_BEGIN_ALLOW_THREADS mysql_close(&(self->connection)); Py_END_ALLOW_THREADS - self->open = 0; + self->open = false; _mysql_ConnectionObject_clear(self); Py_RETURN_NONE; } @@ -638,7 +767,7 @@ _mysql_ConnectionObject_affected_rows( check_connection(self); ret = mysql_affected_rows(&(self->connection)); if (ret == (my_ulonglong)-1) - return PyInt_FromLong(-1); + return PyLong_FromLong(-1); return PyLong_FromUnsignedLongLong(ret); } @@ -656,8 +785,7 @@ _mysql_debug( char *debug; if (!PyArg_ParseTuple(args, "s", &debug)) return NULL; mysql_debug(debug); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_dump_debug_info__doc__[] = @@ -677,8 +805,7 @@ _mysql_ConnectionObject_dump_debug_info( err = mysql_dump_debug_info(&(self->connection)); Py_END_ALLOW_THREADS if (err) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_autocommit__doc__[] = @@ -696,8 +823,7 @@ _mysql_ConnectionObject_autocommit( err = mysql_autocommit(&(self->connection), flag); Py_END_ALLOW_THREADS if (err) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_get_autocommit__doc__[] = @@ -729,12 +855,11 @@ _mysql_ConnectionObject_commit( err = mysql_commit(&(self->connection)); Py_END_ALLOW_THREADS if (err) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_rollback__doc__[] = -"Rolls backs the current transaction\n\ +"Rolls back the current transaction\n\ "; static PyObject * _mysql_ConnectionObject_rollback( @@ -774,7 +899,7 @@ _mysql_ConnectionObject_next_result( err = mysql_next_result(&(self->connection)); Py_END_ALLOW_THREADS if (err > 0) return _mysql_Exception(self); - return PyInt_FromLong(err); + return PyLong_FromLong(err); } @@ -797,7 +922,7 @@ _mysql_ConnectionObject_set_server_option( err = mysql_set_server_option(&(self->connection), flags); Py_END_ALLOW_THREADS if (err) return _mysql_Exception(self); - return PyInt_FromLong(err); + return PyLong_FromLong(err); } static char _mysql_ConnectionObject_sqlstate__doc__[] = @@ -818,7 +943,7 @@ _mysql_ConnectionObject_sqlstate( PyObject *noargs) { check_connection(self); - return PyString_FromString(mysql_sqlstate(&(self->connection))); + return PyUnicode_FromString(mysql_sqlstate(&(self->connection))); } static char _mysql_ConnectionObject_warning_count__doc__[] = @@ -833,7 +958,7 @@ _mysql_ConnectionObject_warning_count( PyObject *noargs) { check_connection(self); - return PyInt_FromLong(mysql_warning_count(&(self->connection))); + return PyLong_FromLong(mysql_warning_count(&(self->connection))); } static char _mysql_ConnectionObject_errno__doc__[] = @@ -848,7 +973,7 @@ _mysql_ConnectionObject_errno( PyObject *noargs) { check_connection(self); - return PyInt_FromLong((long)mysql_errno(&(self->connection))); + return PyLong_FromLong((long)mysql_errno(&(self->connection))); } static char _mysql_ConnectionObject_error__doc__[] = @@ -863,7 +988,7 @@ _mysql_ConnectionObject_error( PyObject *noargs) { check_connection(self); - return PyString_FromString(mysql_error(&(self->connection))); + return PyUnicode_FromString(mysql_error(&(self->connection))); } static char _mysql_escape_string__doc__[] = @@ -881,7 +1006,8 @@ _mysql_escape_string( { PyObject *str; char *in, *out; - int len, size; + unsigned long len; + Py_ssize_t size; if (!PyArg_ParseTuple(args, "s#:escape_string", &in, &size)) return NULL; str = PyBytes_FromStringAndSize((char *) NULL, size*2+1); if (!str) return PyErr_NoMemory(); @@ -915,37 +1041,54 @@ _mysql.string_literal(obj) cannot handle character sets."; static PyObject * _mysql_string_literal( _mysql_ConnectionObject *self, - PyObject *args) + PyObject *o) { - PyObject *str, *s, *o, *d; - char *in, *out; - int len, size; + PyObject *s; // input string or bytes. need to decref. + if (self && PyModule_Check((PyObject*)self)) self = NULL; - if (!PyArg_ParseTuple(args, "O|O:string_literal", &o, &d)) return NULL; + if (PyBytes_Check(o)) { s = o; Py_INCREF(s); - } else { - s = PyObject_Str(o); - if (!s) return NULL; -#ifdef IS_PY3K - { - PyObject *t = PyUnicode_AsASCIIString(s); - Py_DECREF(s); - if (!t) return NULL; + } + else { + PyObject *t = PyObject_Str(o); + if (!t) return NULL; + + const char *encoding = (self && self->open) ? + _get_encoding(&self->connection) : utf8; + if (encoding == utf8) { s = t; } -#endif + else { + s = PyUnicode_AsEncodedString(t, encoding, "strict"); + Py_DECREF(t); + if (!s) return NULL; + } + } + + // Prepare input string (in, size) + const char *in; + Py_ssize_t size; + if (PyUnicode_Check(s)) { + in = PyUnicode_AsUTF8AndSize(s, &size); + } else { + assert(PyBytes_Check(s)); + in = PyBytes_AsString(s); + size = PyBytes_GET_SIZE(s); } - in = PyBytes_AsString(s); - size = PyBytes_GET_SIZE(s); - str = PyBytes_FromStringAndSize((char *) NULL, size*2+3); + + // Prepare output buffer (str, out) + PyObject *str = PyBytes_FromStringAndSize((char *) NULL, size*2+3); if (!str) { Py_DECREF(s); return PyErr_NoMemory(); } - out = PyBytes_AS_STRING(str); + char *out = PyBytes_AS_STRING(str); + + // escape + unsigned long len; if (self && self->open) { #if MYSQL_VERSION_ID >= 50707 && !defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID) len = mysql_real_escape_string_quote(&(self->connection), out+1, in, size, '\''); @@ -955,43 +1098,37 @@ _mysql_string_literal( } else { len = mysql_escape_string(out+1, in, size); } - *out = *(out+len+1) = '\''; - if (_PyBytes_Resize(&str, len+2) < 0) return NULL; + Py_DECREF(s); - return (str); + *out = *(out+len+1) = '\''; + if (_PyBytes_Resize(&str, len+2) < 0) { + Py_DECREF(str); + return NULL; + } + return str; } -static PyObject *_mysql_NULL; - static PyObject * _escape_item( + PyObject *self, PyObject *item, PyObject *d) { PyObject *quoted=NULL, *itemtype, *itemconv; - if (!(itemtype = PyObject_Type(item))) - goto error; + if (!(itemtype = PyObject_Type(item))) { + return NULL; + } itemconv = PyObject_GetItem(d, itemtype); Py_DECREF(itemtype); if (!itemconv) { PyErr_Clear(); - itemconv = PyObject_GetItem(d, -#ifdef IS_PY3K - (PyObject *) &PyUnicode_Type); -#else - (PyObject *) &PyString_Type); -#endif - } - if (!itemconv) { - PyErr_SetString(PyExc_TypeError, - "no default type converter defined"); - goto error; + return _mysql_string_literal((_mysql_ConnectionObject*)self, item); } Py_INCREF(d); quoted = PyObject_CallFunction(itemconv, "OO", item, d); Py_DECREF(d); Py_DECREF(itemconv); -error: + return quoted; } @@ -1013,14 +1150,14 @@ _mysql_escape( "argument 2 must be a mapping"); return NULL; } - return _escape_item(o, d); + return _escape_item(self, o, d); } else { if (!self) { PyErr_SetString(PyExc_TypeError, "argument 2 must be a mapping"); return NULL; } - return _escape_item(o, + return _escape_item(self, o, ((_mysql_ConnectionObject *) self)->converter); } } @@ -1038,14 +1175,26 @@ _mysql_ResultObject_describe( PyObject *d; MYSQL_FIELD *fields; unsigned int i, n; + check_result_connection(self); + n = mysql_num_fields(self->result); fields = mysql_fetch_fields(self->result); if (!(d = PyTuple_New(n))) return NULL; for (i=0; iencoding == utf8) { + name = PyUnicode_DecodeUTF8(fields[i].name, fields[i].name_length, "replace"); + } else { + name = PyUnicode_Decode(fields[i].name, fields[i].name_length, self->encoding, "replace"); + } + if (name == NULL) { + goto error; + } + + t = Py_BuildValue("(Niiiiii)", + name, (long) fields[i].type, (long) fields[i].max_length, (long) fields[i].length, @@ -1079,7 +1228,7 @@ _mysql_ResultObject_field_flags( if (!(d = PyTuple_New(n))) return NULL; for (i=0; itype; - // Return bytes for binary and string types. - int binary = 0; - if (field_type == FIELD_TYPE_TINY_BLOB || - field_type == FIELD_TYPE_MEDIUM_BLOB || - field_type == FIELD_TYPE_LONG_BLOB || - field_type == FIELD_TYPE_BLOB || - field_type == FIELD_TYPE_VAR_STRING || - field_type == FIELD_TYPE_STRING || - field_type == FIELD_TYPE_GEOMETRY || - field_type == FIELD_TYPE_BIT) { - binary = 1; + const char *rowitem, + Py_ssize_t length, + MYSQL_FIELD *field, + const char *encoding) +{ + if (rowitem == NULL) { + Py_RETURN_NONE; } -#endif - if (rowitem) { - if (converter != Py_None) { - v = PyObject_CallFunction(converter, -#ifdef IS_PY3K - binary ? "y#" : "s#", -#else - "s#", -#endif - rowitem, - (int)length); + + // Fast paths for int, string and binary. + if (converter == (PyObject*)&PyUnicode_Type) { + if (encoding == utf8) { + //fprintf(stderr, "decoding with utf8!\n"); + return PyUnicode_DecodeUTF8(rowitem, length, NULL); } else { -#ifdef IS_PY3K - if (!binary) { - v = PyUnicode_FromStringAndSize(rowitem, (int)length); - } else -#endif - v = PyBytes_FromStringAndSize(rowitem, (int)length); + //fprintf(stderr, "decoding with %s\n", encoding); + return PyUnicode_Decode(rowitem, length, encoding, NULL); } - if (!v) - return NULL; - } else { - Py_INCREF(Py_None); - v = Py_None; } - return v; + if (converter == (PyObject*)&PyBytes_Type || converter == Py_None) { + //fprintf(stderr, "decoding with bytes\n", encoding); + return PyBytes_FromStringAndSize(rowitem, length); + } + if (converter == (PyObject*)&PyLong_Type) { + //fprintf(stderr, "decoding with int\n", encoding); + return PyLong_FromString(rowitem, NULL, 10); + } + + //fprintf(stderr, "decoding with callback\n"); + //PyObject_Print(converter, stderr, 0); + //fprintf(stderr, "\n"); + int binary; + switch (field->type) { + case FIELD_TYPE_DECIMAL: + case FIELD_TYPE_NEWDECIMAL: + case FIELD_TYPE_TIMESTAMP: + case FIELD_TYPE_DATETIME: + case FIELD_TYPE_TIME: + case FIELD_TYPE_DATE: + binary = 0; // pass str, because these converters expect it + break; + default: // Default to just passing bytes + binary = 1; + } + return PyObject_CallFunction(converter, + binary ? "y#" : "s#", + rowitem, (Py_ssize_t)length); } static PyObject * _mysql_row_to_tuple( _mysql_ResultObject *self, - MYSQL_ROW row) + MYSQL_ROW row, + PyObject *unused) { unsigned int n, i; unsigned long *length; @@ -1155,7 +1307,7 @@ _mysql_row_to_tuple( for (i=0; iconverter, i); - v = _mysql_field_to_python(c, row[i], length[i], &fields[i]); + v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding); if (!v) goto error; PyTuple_SET_ITEM(r, i, v); } @@ -1168,7 +1320,8 @@ _mysql_row_to_tuple( static PyObject * _mysql_row_to_dict( _mysql_ResultObject *self, - MYSQL_ROW row) + MYSQL_ROW row, + PyObject *cache) { unsigned int n, i; unsigned long *length; @@ -1182,37 +1335,55 @@ _mysql_row_to_dict( for (i=0; iconverter, i); - v = _mysql_field_to_python(c, row[i], length[i], &fields[i]); + v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding); if (!v) goto error; - if (!PyMapping_HasKeyString(r, fields[i].name)) { - PyMapping_SetItemString(r, fields[i].name, v); + + PyObject *pyname = PyUnicode_FromString(fields[i].name); + if (pyname == NULL) { + Py_DECREF(v); + goto error; + } + int err = PyDict_Contains(r, pyname); + if (err < 0) { // error + Py_DECREF(v); + goto error; + } + if (err) { // duplicate + Py_DECREF(pyname); + pyname = PyUnicode_FromFormat("%s.%s", fields[i].table, fields[i].name); + if (pyname == NULL) { + Py_DECREF(v); + goto error; + } + } + + err = PyDict_SetItem(r, pyname, v); + if (cache) { + PyTuple_SET_ITEM(cache, i, pyname); } else { - int len; - char buf[256]; - strncpy(buf, fields[i].table, 256); - len = strlen(buf); - strncat(buf, ".", 256-len); - len = strlen(buf); - strncat(buf, fields[i].name, 256-len); - PyMapping_SetItemString(r, buf, v); + Py_DECREF(pyname); } Py_DECREF(v); + if (err) { + goto error; + } } return r; - error: - Py_XDECREF(r); +error: + Py_DECREF(r); return NULL; } static PyObject * _mysql_row_to_dict_old( _mysql_ResultObject *self, - MYSQL_ROW row) + MYSQL_ROW row, + PyObject *cache) { unsigned int n, i; unsigned long *length; PyObject *r, *c; - MYSQL_FIELD *fields; + MYSQL_FIELD *fields; n = mysql_num_fields(self->result); if (!(r = PyDict_New())) return NULL; @@ -1221,21 +1392,27 @@ _mysql_row_to_dict_old( for (i=0; iconverter, i); - v = _mysql_field_to_python(c, row[i], length[i], &fields[i]); - if (!v) goto error; - { - int len=0; - char buf[256]=""; - if (strlen(fields[i].table)) { - strncpy(buf, fields[i].table, 256); - len = strlen(buf); - strncat(buf, ".", 256-len); - len = strlen(buf); - } - strncat(buf, fields[i].name, 256-len); - PyMapping_SetItemString(r, buf, v); + v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding); + if (!v) { + goto error; + } + + PyObject *pyname; + if (strlen(fields[i].table)) { + pyname = PyUnicode_FromFormat("%s.%s", fields[i].table, fields[i].name); + } else { + pyname = PyUnicode_FromString(fields[i].name); } + int err = PyDict_SetItem(r, pyname, v); Py_DECREF(v); + if (cache) { + PyTuple_SET_ITEM(cache, i, pyname); + } else { + Py_DECREF(pyname); + } + if (err) { + goto error; + } } return r; error: @@ -1243,42 +1420,100 @@ _mysql_row_to_dict_old( return NULL; } -typedef PyObject *_PYFUNC(_mysql_ResultObject *, MYSQL_ROW); +static PyObject * +_mysql_row_to_dict_cached( + _mysql_ResultObject *self, + MYSQL_ROW row, + PyObject *cache) +{ + PyObject *r = PyDict_New(); + if (!r) { + return NULL; + } + + unsigned int n = mysql_num_fields(self->result); + unsigned long *length = mysql_fetch_lengths(self->result); + MYSQL_FIELD *fields = mysql_fetch_fields(self->result); -int + for (unsigned int i=0; iconverter, i); + PyObject *v = _mysql_field_to_python(c, row[i], length[i], &fields[i], self->encoding); + if (!v) { + goto error; + } + + PyObject *pyname = PyTuple_GET_ITEM(cache, i); // borrowed + int err = PyDict_SetItem(r, pyname, v); + Py_DECREF(v); + if (err) { + goto error; + } + } + return r; + error: + Py_XDECREF(r); + return NULL; +} + + +typedef PyObject *_convertfunc(_mysql_ResultObject *, MYSQL_ROW, PyObject *); +static _convertfunc * const row_converters[] = { + _mysql_row_to_tuple, + _mysql_row_to_dict, + _mysql_row_to_dict_old +}; + +Py_ssize_t _mysql__fetch_row( _mysql_ResultObject *self, - PyObject **r, - int skiprows, - int maxrows, - _PYFUNC *convert_row) + PyObject *r, /* list object */ + Py_ssize_t maxrows, + int how) { - int i; - MYSQL_ROW row; + _convertfunc *convert_row = row_converters[how]; - for (i = skiprows; i<(skiprows+maxrows); i++) { - PyObject *v; + PyObject *cache = NULL; + if (maxrows > 0 && how > 0) { + cache = PyTuple_New(mysql_num_fields(self->result)); + if (!cache) { + return -1; + } + } + + Py_ssize_t i; + for (i = 0; i < maxrows; i++) { + MYSQL_ROW row; if (!self->use) row = mysql_fetch_row(self->result); else { - Py_BEGIN_ALLOW_THREADS; + Py_BEGIN_ALLOW_THREADS row = mysql_fetch_row(self->result); - Py_END_ALLOW_THREADS; + Py_END_ALLOW_THREADS } if (!row && mysql_errno(&(((_mysql_ConnectionObject *)(self->conn))->connection))) { _mysql_Exception((_mysql_ConnectionObject *)self->conn); goto error; } if (!row) { - if (_PyTuple_Resize(r, i) == -1) goto error; break; } - v = convert_row(self, row); - if (!v) goto error; - PyTuple_SET_ITEM(*r, i, v); + PyObject *v = convert_row(self, row, cache); + if (!v) { + goto error; + } + if (cache) { + convert_row = _mysql_row_to_dict_cached; + } + if (PyList_Append(r, v)) { + Py_DECREF(v); + goto error; + } + Py_DECREF(v); } - return i-skiprows; - error: + Py_XDECREF(cache); + return i; +error: + Py_XDECREF(cache); return -1; } @@ -1297,60 +1532,64 @@ _mysql_ResultObject_fetch_row( PyObject *args, PyObject *kwargs) { - typedef PyObject *_PYFUNC(_mysql_ResultObject *, MYSQL_ROW); - static char *kwlist[] = { "maxrows", "how", NULL }; - static _PYFUNC *row_converters[] = - { - _mysql_row_to_tuple, - _mysql_row_to_dict, - _mysql_row_to_dict_old - }; - _PYFUNC *convert_row; - int maxrows=1, how=0, skiprows=0, rowsadded; + static char *kwlist[] = {"maxrows", "how", NULL }; + int maxrows=1, how=0; PyObject *r=NULL; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii:fetch_row", kwlist, &maxrows, &how)) return NULL; check_result_connection(self); - if (how >= (int)sizeof(row_converters)) { + if (how >= (int)(sizeof(row_converters) / sizeof(row_converters[0]))) { PyErr_SetString(PyExc_ValueError, "how out of range"); return NULL; } - convert_row = row_converters[how]; - if (maxrows) { - if (!(r = PyTuple_New(maxrows))) goto error; - rowsadded = _mysql__fetch_row(self, &r, skiprows, maxrows, - convert_row); - if (rowsadded == -1) goto error; - } else { + if (!maxrows) { if (self->use) { - maxrows = 1000; - if (!(r = PyTuple_New(maxrows))) goto error; - while (1) { - rowsadded = _mysql__fetch_row(self, &r, skiprows, - maxrows, convert_row); - if (rowsadded == -1) goto error; - skiprows += rowsadded; - if (rowsadded < maxrows) break; - if (_PyTuple_Resize(&r, skiprows+maxrows) == -1) - goto error; - } + maxrows = INT_MAX; } else { - /* XXX if overflow, maxrows<0? */ - maxrows = (int) mysql_num_rows(self->result); - if (!(r = PyTuple_New(maxrows))) goto error; - rowsadded = _mysql__fetch_row(self, &r, 0, - maxrows, convert_row); - if (rowsadded == -1) goto error; + // todo: preallocate. + maxrows = (Py_ssize_t) mysql_num_rows(self->result); } } - return r; + if (!(r = PyList_New(0))) goto error; + Py_ssize_t rowsadded = _mysql__fetch_row(self, r, maxrows, how); + if (rowsadded == -1) goto error; + + /* DB-API allows return rows as list. + * But we need to return list because Django expecting tuple. + */ + PyObject *t = PyList_AsTuple(r); + Py_DECREF(r); + return t; error: Py_XDECREF(r); return NULL; } +static const char _mysql_ResultObject_discard__doc__[] = +"discard() -- Discard remaining rows in the resultset."; + +static PyObject * +_mysql_ResultObject_discard( + _mysql_ResultObject *self, + PyObject *noargs) +{ + check_result_connection(self); + + MYSQL_ROW row; + Py_BEGIN_ALLOW_THREADS + while (NULL != (row = mysql_fetch_row(self->result))) { + // do nothing + } + Py_END_ALLOW_THREADS + _mysql_ConnectionObject *conn = (_mysql_ConnectionObject *)self->conn; + if (mysql_errno(&conn->connection)) { + return _mysql_Exception(conn); + } + Py_RETURN_NONE; +} + static char _mysql_ConnectionObject_change_user__doc__[] = "Changes the user and causes the database specified by db to\n\ become the default (current) database on the connection\n\ @@ -1376,7 +1615,7 @@ _mysql_ConnectionObject_change_user( { char *user, *pwd=NULL, *db=NULL; int r; - static char *kwlist[] = { "user", "passwd", "db", NULL } ; + static char *kwlist[] = { "user", "passwd", "db", NULL } ; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|ss:change_user", kwlist, &user, &pwd, &db)) @@ -1386,8 +1625,7 @@ _mysql_ConnectionObject_change_user( r = mysql_change_user(&(self->connection), user, pwd, db); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_character_set_name__doc__[] = @@ -1403,7 +1641,7 @@ _mysql_ConnectionObject_character_set_name( const char *s; check_connection(self); s = mysql_character_set_name(&(self->connection)); - return PyString_FromString(s); + return PyUnicode_FromString(s); } static char _mysql_ConnectionObject_set_character_set__doc__[] = @@ -1424,8 +1662,7 @@ _mysql_ConnectionObject_set_character_set( err = mysql_set_character_set(&(self->connection), s); Py_END_ALLOW_THREADS if (err) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #if MYSQL_VERSION_ID >= 50010 @@ -1462,15 +1699,15 @@ _mysql_ConnectionObject_get_character_set_info( mysql_get_character_set_info(&(self->connection), &cs); if (!(result = PyDict_New())) return NULL; if (cs.csname) - PyDict_SetItemString(result, "name", PyString_FromString(cs.csname)); + PyDict_SetItemString(result, "name", PyUnicode_FromString(cs.csname)); if (cs.name) - PyDict_SetItemString(result, "collation", PyString_FromString(cs.name)); + PyDict_SetItemString(result, "collation", PyUnicode_FromString(cs.name)); if (cs.comment) - PyDict_SetItemString(result, "comment", PyString_FromString(cs.comment)); + PyDict_SetItemString(result, "comment", PyUnicode_FromString(cs.comment)); if (cs.dir) - PyDict_SetItemString(result, "dir", PyString_FromString(cs.dir)); - PyDict_SetItemString(result, "mbminlen", PyInt_FromLong(cs.mbminlen)); - PyDict_SetItemString(result, "mbmaxlen", PyInt_FromLong(cs.mbmaxlen)); + PyDict_SetItemString(result, "dir", PyUnicode_FromString(cs.dir)); + PyDict_SetItemString(result, "mbminlen", PyLong_FromLong(cs.mbminlen)); + PyDict_SetItemString(result, "mbmaxlen", PyLong_FromLong(cs.mbmaxlen)); return result; } #endif @@ -1505,7 +1742,7 @@ _mysql_get_client_info( PyObject *self, PyObject *noargs) { - return PyString_FromString(mysql_get_client_info()); + return PyUnicode_FromString(mysql_get_client_info()); } static char _mysql_ConnectionObject_get_host_info__doc__[] = @@ -1519,7 +1756,7 @@ _mysql_ConnectionObject_get_host_info( PyObject *noargs) { check_connection(self); - return PyString_FromString(mysql_get_host_info(&(self->connection))); + return PyUnicode_FromString(mysql_get_host_info(&(self->connection))); } static char _mysql_ConnectionObject_get_proto_info__doc__[] = @@ -1533,7 +1770,7 @@ _mysql_ConnectionObject_get_proto_info( PyObject *noargs) { check_connection(self); - return PyInt_FromLong((long)mysql_get_proto_info(&(self->connection))); + return PyLong_FromLong((long)mysql_get_proto_info(&(self->connection))); } static char _mysql_ConnectionObject_get_server_info__doc__[] = @@ -1547,7 +1784,7 @@ _mysql_ConnectionObject_get_server_info( PyObject *noargs) { check_connection(self); - return PyString_FromString(mysql_get_server_info(&(self->connection))); + return PyUnicode_FromString(mysql_get_server_info(&(self->connection))); } static char _mysql_ConnectionObject_info__doc__[] = @@ -1564,9 +1801,8 @@ _mysql_ConnectionObject_info( const char *s; check_connection(self); s = mysql_info(&(self->connection)); - if (s) return PyString_FromString(s); - Py_INCREF(Py_None); - return Py_None; + if (s) return PyUnicode_FromString(s); + Py_RETURN_NONE; } static char _mysql_ConnectionObject_insert_id__doc__[] = @@ -1597,15 +1833,13 @@ _mysql_ConnectionObject_insert_id( { my_ulonglong r; check_connection(self); - Py_BEGIN_ALLOW_THREADS r = mysql_insert_id(&(self->connection)); - Py_END_ALLOW_THREADS return PyLong_FromUnsignedLongLong(r); } static char _mysql_ConnectionObject_kill__doc__[] = "Asks the server to kill the thread specified by pid.\n\ -Non-standard."; +Non-standard. Deprecated."; static PyObject * _mysql_ConnectionObject_kill( @@ -1614,14 +1848,15 @@ _mysql_ConnectionObject_kill( { unsigned long pid; int r; + char query[50]; if (!PyArg_ParseTuple(args, "k:kill", &pid)) return NULL; check_connection(self); + snprintf(query, 50, "KILL %lu", pid); Py_BEGIN_ALLOW_THREADS - r = mysql_kill(&(self->connection), pid); + r = mysql_query(&(self->connection), query); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_field_count__doc__[] = @@ -1636,7 +1871,21 @@ _mysql_ConnectionObject_field_count( PyObject *noargs) { check_connection(self); - return PyInt_FromLong((long)mysql_field_count(&(self->connection))); + return PyLong_FromLong((long)mysql_field_count(&(self->connection))); +} + +static char _mysql_ConnectionObject_fileno__doc__[] = +"Return file descriptor of the underlying libmysqlclient connection.\n\ +This provides raw access to the underlying network connection.\n\ +"; + +static PyObject * +_mysql_ConnectionObject_fileno( + _mysql_ConnectionObject *self, + PyObject *noargs) +{ + check_connection(self); + return PyLong_FromLong(self->connection.net.fd); } static char _mysql_ResultObject_num_fields__doc__[] = @@ -1648,7 +1897,7 @@ _mysql_ResultObject_num_fields( PyObject *noargs) { check_result_connection(self); - return PyInt_FromLong((long)mysql_num_fields(self->result)); + return PyLong_FromLong((long)mysql_num_fields(self->result)); } static char _mysql_ResultObject_num_rows__doc__[] = @@ -1667,18 +1916,18 @@ _mysql_ResultObject_num_rows( } static char _mysql_ConnectionObject_ping__doc__[] = -"Checks whether or not the connection to the server is\n\ -working. If it has gone down, an automatic reconnection is\n\ -attempted.\n\ +"Checks whether or not the connection to the server is working.\n\ \n\ This function can be used by clients that remain idle for a\n\ long while, to check whether or not the server has closed the\n\ -connection and reconnect if necessary.\n\ +connection.\n\ \n\ New in 1.2.2: Accepts an optional reconnect parameter. If True,\n\ then the client will attempt reconnection. Note that this setting\n\ is persistent. By default, this is on in MySQL<5.0.3, and off\n\ thereafter.\n\ +MySQL 8.0.33 deprecated the MYSQL_OPT_RECONNECT option so reconnect\n\ +parameter is also deprecated in mysqlclient 2.2.1.\n\ \n\ Non-standard. You should assume that ping() performs an\n\ implicit rollback; use only when starting a new transaction.\n\ @@ -1690,19 +1939,25 @@ _mysql_ConnectionObject_ping( _mysql_ConnectionObject *self, PyObject *args) { - int r, reconnect = -1; - if (!PyArg_ParseTuple(args, "|I", &reconnect)) return NULL; + int reconnect = 0; + if (!PyArg_ParseTuple(args, "|p", &reconnect)) return NULL; check_connection(self); - if (reconnect != -1) { + if (reconnect != (self->reconnect == true)) { + // libmysqlclient show warning to stderr when MYSQL_OPT_RECONNECT is used. + // so we avoid using it as possible for now. + // TODO: Warn when reconnect is true. + // MySQL 8.0.33 show warning to stderr already. + // We will emit Pytohn warning in future. my_bool recon = (my_bool)reconnect; mysql_options(&self->connection, MYSQL_OPT_RECONNECT, &recon); + self->reconnect = (bool)reconnect; } + int r; Py_BEGIN_ALLOW_THREADS r = mysql_ping(&(self->connection)); Py_END_ALLOW_THREADS - if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + if (r) return _mysql_Exception(self); + Py_RETURN_NONE; } static char _mysql_ConnectionObject_query__doc__[] = @@ -1717,7 +1972,8 @@ _mysql_ConnectionObject_query( PyObject *args) { char *query; - int len, r; + Py_ssize_t len; + int r; if (!PyArg_ParseTuple(args, "s#:query", &query, &len)) return NULL; check_connection(self); @@ -1725,8 +1981,7 @@ _mysql_ConnectionObject_query( r = mysql_real_query(&(self->connection), query, len); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } @@ -1740,7 +1995,8 @@ _mysql_ConnectionObject_send_query( PyObject *args) { char *query; - int len, r; + Py_ssize_t len; + int r; MYSQL *mysql = &(self->connection); if (!PyArg_ParseTuple(args, "s#:query", &query, &len)) return NULL; check_connection(self); @@ -1749,8 +2005,7 @@ _mysql_ConnectionObject_send_query( r = mysql_send_query(mysql, query, len); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } @@ -1770,8 +2025,7 @@ _mysql_ConnectionObject_read_query_result( r = (int)mysql_read_query_result(mysql); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_select_db__doc__[] = @@ -1799,13 +2053,12 @@ _mysql_ConnectionObject_select_db( r = mysql_select_db(&(self->connection), db); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_shutdown__doc__[] = "Asks the database server to shut down. The connected user must\n\ -have shutdown privileges. Non-standard.\n\ +have shutdown privileges. Non-standard. Deprecated.\n\ "; static PyObject * @@ -1816,11 +2069,10 @@ _mysql_ConnectionObject_shutdown( int r; check_connection(self); Py_BEGIN_ALLOW_THREADS - r = mysql_shutdown(&(self->connection), SHUTDOWN_DEFAULT); + r = mysql_query(&(self->connection), "SHUTDOWN"); Py_END_ALLOW_THREADS if (r) return _mysql_Exception(self); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char _mysql_ConnectionObject_stat__doc__[] = @@ -1841,7 +2093,7 @@ _mysql_ConnectionObject_stat( s = mysql_stat(&(self->connection)); Py_END_ALLOW_THREADS if (!s) return _mysql_Exception(self); - return PyString_FromString(s); + return PyUnicode_FromString(s); } static char _mysql_ConnectionObject_store_result__doc__[] = @@ -1865,8 +2117,10 @@ _mysql_ConnectionObject_store_result( if (!kwarglist) goto error; r = MyAlloc(_mysql_ResultObject, _mysql_ResultObject_Type); if (!r) goto error; - if (_mysql_ResultObject_Initialize(r, arglist, kwarglist)) + if (_mysql_ResultObject_Initialize(r, arglist, kwarglist)) { + Py_DECREF(r); goto error; + } result = (PyObject *) r; if (!(r->result)) { Py_DECREF(result); @@ -1897,10 +2151,8 @@ _mysql_ConnectionObject_thread_id( { unsigned long pid; check_connection(self); - Py_BEGIN_ALLOW_THREADS pid = mysql_thread_id(&(self->connection)); - Py_END_ALLOW_THREADS - return PyInt_FromLong((long)pid); + return PyLong_FromLong((long)pid); } static char _mysql_ConnectionObject_use_result__doc__[] = @@ -1924,9 +2176,11 @@ _mysql_ConnectionObject_use_result( if (!kwarglist) goto error; r = MyAlloc(_mysql_ResultObject, _mysql_ResultObject_Type); if (!r) goto error; - result = (PyObject *) r; - if (_mysql_ResultObject_Initialize(r, arglist, kwarglist)) + if (_mysql_ResultObject_Initialize(r, arglist, kwarglist)) { + Py_DECREF(r); goto error; + } + result = (PyObject *) r; if (!(r->result)) { Py_DECREF(result); Py_INCREF(Py_None); @@ -1938,6 +2192,43 @@ _mysql_ConnectionObject_use_result( return result; } +static const char _mysql_ConnectionObject_discard_result__doc__[] = +"Discard current result set.\n\n" +"This function can be called instead of use_result() or store_result(). Non-standard."; + +static PyObject * +_mysql_ConnectionObject_discard_result( + _mysql_ConnectionObject *self, + PyObject *noargs) +{ + check_connection(self); + MYSQL *conn = &(self->connection); + + Py_BEGIN_ALLOW_THREADS; + + MYSQL_RES *res = mysql_use_result(conn); + if (res == NULL) { + Py_BLOCK_THREADS; + if (mysql_errno(conn) != 0) { + // fprintf(stderr, "mysql_use_result failed: %s\n", mysql_error(conn)); + return _mysql_Exception(self); + } + Py_RETURN_NONE; + } + + MYSQL_ROW row; + while (NULL != (row = mysql_fetch_row(res))) { + // do nothing. + } + mysql_free_result(res); + Py_END_ALLOW_THREADS; + if (mysql_errno(conn)) { + // fprintf(stderr, "mysql_free_result failed: %s\n", mysql_error(conn)); + return _mysql_Exception(self); + } + Py_RETURN_NONE; +} + static void _mysql_ConnectionObject_dealloc( _mysql_ConnectionObject *self) @@ -1945,7 +2236,7 @@ _mysql_ConnectionObject_dealloc( PyObject_GC_UnTrack(self); if (self->open) { mysql_close(&(self->connection)); - self->open = 0; + self->open = false; } Py_CLEAR(self->converter); MyFree(self); @@ -1957,13 +2248,11 @@ _mysql_ConnectionObject_repr( { char buf[300]; if (self->open) - sprintf(buf, "<_mysql.connection open to '%.256s' at %lx>", - self->connection.host, - (long)self); + snprintf(buf, 300, "<_mysql.connection open to '%.256s' at %p>", + self->connection.host, self); else - sprintf(buf, "<_mysql.connection closed at %lx>", - (long)self); - return PyString_FromString(buf); + snprintf(buf, 300, "<_mysql.connection closed at %p>", self); + return PyUnicode_FromString(buf); } static char _mysql_ResultObject_data_seek__doc__[] = @@ -1977,8 +2266,7 @@ _mysql_ResultObject_data_seek( if (!PyArg_ParseTuple(args, "i:data_seek", &row)) return NULL; check_result_connection(self); mysql_data_seek(self->result, row); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static void @@ -1996,8 +2284,8 @@ _mysql_ResultObject_repr( _mysql_ResultObject *self) { char buf[300]; - sprintf(buf, "<_mysql.result object at %lx>", (long)self); - return PyString_FromString(buf); + snprintf(buf, 300, "<_mysql.result object at %p>", self); + return PyUnicode_FromString(buf); } static PyMethodDef _mysql_ConnectionObject_methods[] = { @@ -2129,6 +2417,12 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = { METH_NOARGS, _mysql_ConnectionObject_field_count__doc__ }, + { + "fileno", + (PyCFunction)_mysql_ConnectionObject_fileno, + METH_NOARGS, + _mysql_ConnectionObject_fileno__doc__ + }, { "get_host_info", (PyCFunction)_mysql_ConnectionObject_get_host_info, @@ -2216,7 +2510,7 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = { { "string_literal", (PyCFunction)_mysql_string_literal, - METH_VARARGS, + METH_O, _mysql_string_literal__doc__}, { "thread_id", @@ -2230,6 +2524,12 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = { METH_NOARGS, _mysql_ConnectionObject_use_result__doc__ }, + { + "discard_result", + (PyCFunction)_mysql_ConnectionObject_discard_result, + METH_NOARGS, + _mysql_ConnectionObject_discard_result__doc__ + }, {NULL, NULL} /* sentinel */ }; @@ -2250,7 +2550,7 @@ static struct PyMemberDef _mysql_ConnectionObject_memberlist[] = { }, { "server_capabilities", - T_UINT, + T_ULONG, offsetof(_mysql_ConnectionObject,connection.server_capabilities), READONLY, "Capabilities of server; consult MySQLdb.constants.CLIENT" @@ -2264,7 +2564,7 @@ static struct PyMemberDef _mysql_ConnectionObject_memberlist[] = { }, { "client_flag", - T_UINT, + T_ULONG, offsetof(_mysql_ConnectionObject,connection.client_flag), READONLY, "Client flags; refer to MySQLdb.constants.CLIENT" @@ -2291,6 +2591,12 @@ static PyMethodDef _mysql_ResultObject_methods[] = { METH_VARARGS | METH_KEYWORDS, _mysql_ResultObject_fetch_row__doc__ }, + { + "discard", + (PyCFunction)_mysql_ResultObject_discard, + METH_NOARGS, + _mysql_ResultObject_discard__doc__ + }, { "field_flags", (PyCFunction)_mysql_ResultObject_field_flags, @@ -2336,13 +2642,9 @@ _mysql_ConnectionObject_getattro( PyObject *name) { const char *cname; -#ifdef IS_PY3K cname = PyUnicode_AsUTF8(name); -#else - cname = PyString_AsString(name); -#endif if (strcmp(cname, "closed") == 0) - return PyInt_FromLong((long)!(self->open)); + return PyLong_FromLong((long)!(self->open)); return PyObject_GenericGetAttr((PyObject *)self, name); } @@ -2435,7 +2737,7 @@ PyTypeObject _mysql_ConnectionObject_Type = { 0, /* (long) tp_dictoffset */ (initproc)_mysql_ConnectionObject_Initialize, /* tp_init */ NULL, /* tp_alloc */ - NULL, /* tp_new */ + PyType_GenericNew, /* tp_new */ NULL, /* tp_free Low-level free-memory routine */ 0, /* (PyObject *) tp_bases */ 0, /* (PyObject *) tp_mro method resolution order */ @@ -2503,7 +2805,7 @@ PyTypeObject _mysql_ResultObject_Type = { 0, /* (long) tp_dictoffset */ (initproc)_mysql_ResultObject_Initialize, /* tp_init */ NULL, /* tp_alloc */ - NULL, /* tp_new */ + PyType_GenericNew, /* tp_new */ NULL, /* tp_free Low-level free-memory routine */ 0, /* (PyObject *) tp_bases */ 0, /* (PyObject *) tp_mro method resolution order */ @@ -2539,7 +2841,7 @@ _mysql_methods[] = { { "string_literal", (PyCFunction)_mysql_string_literal, - METH_VARARGS, + METH_O, _mysql_string_literal__doc__ }, { @@ -2548,12 +2850,6 @@ _mysql_methods[] = { METH_NOARGS, _mysql_get_client_info__doc__ }, - { - "thread_safe", - (PyCFunction)_mysql_thread_safe, - METH_NOARGS, - _mysql_thread_safe__doc__ - }, {NULL, NULL} /* sentinel */ }; @@ -2589,7 +2885,6 @@ an argument are now methods of the result object. Deprecated functions\n\ (as of 3.23) are NOT implemented.\n\ "; -#ifdef IS_PY3K static struct PyModuleDef _mysqlmodule = { PyModuleDef_HEAD_INIT, "_mysql", /* name of module */ @@ -2601,24 +2896,14 @@ static struct PyModuleDef _mysqlmodule = { PyMODINIT_FUNC PyInit__mysql(void) -#else -DL_EXPORT(void) -init_mysql(void) -#endif { PyObject *dict, *module, *emod, *edict; -#ifndef IS_PY3K - _mysql_ConnectionObject_Type.ob_type = &PyType_Type; - _mysql_ResultObject_Type.ob_type = &PyType_Type; -#endif -#if PY_VERSION_HEX >= 0x02020000 - _mysql_ConnectionObject_Type.tp_alloc = PyType_GenericAlloc; - _mysql_ConnectionObject_Type.tp_new = PyType_GenericNew; - _mysql_ResultObject_Type.tp_alloc = PyType_GenericAlloc; - _mysql_ResultObject_Type.tp_new = PyType_GenericNew; -#endif -#ifdef IS_PY3K + if (mysql_library_init(0, NULL, NULL)) { + PyErr_SetString(PyExc_ImportError, "_mysql: mysql_library_init failed"); + return NULL; + } + if (PyType_Ready(&_mysql_ConnectionObject_Type) < 0) return NULL; if (PyType_Ready(&_mysql_ResultObject_Type) < 0) @@ -2626,13 +2911,6 @@ init_mysql(void) module = PyModule_Create(&_mysqlmodule); if (!module) return module; /* this really should never happen */ -#else - _mysql_ConnectionObject_Type.tp_free = _PyObject_GC_Del; - _mysql_ResultObject_Type.tp_free = _PyObject_GC_Del; - module = Py_InitModule4("_mysql", _mysql_methods, _mysql___doc__, - (PyObject *)NULL, PYTHON_API_VERSION); - if (!module) return; /* this really should never happen */ -#endif if (!(dict = PyModule_GetDict(module))) goto error; if (PyDict_SetItemString(dict, "version_info", @@ -2640,7 +2918,7 @@ init_mysql(void) dict, dict))) goto error; if (PyDict_SetItemString(dict, "__version__", - PyString_FromString(QUOTE(__version__)))) + PyUnicode_FromString(QUOTE(__version__)))) goto error; if (PyDict_SetItemString(dict, "connection", (PyObject *)&_mysql_ConnectionObject_Type)) @@ -2650,7 +2928,7 @@ init_mysql(void) (PyObject *)&_mysql_ResultObject_Type)) goto error; Py_INCREF(&_mysql_ResultObject_Type); - if (!(emod = PyImport_ImportModule("MySQLdb._mysql_exceptions"))) { + if (!(emod = PyImport_ImportModule("MySQLdb._exceptions"))) { PyErr_Print(); goto error; } @@ -2689,17 +2967,12 @@ init_mysql(void) _mysql_NewException(dict, edict, "NotSupportedError"))) goto error; Py_DECREF(emod); - if (!(_mysql_NULL = PyString_FromString("NULL"))) - goto error; - if (PyDict_SetItemString(dict, "NULL", _mysql_NULL)) goto error; error: if (PyErr_Occurred()) { PyErr_SetString(PyExc_ImportError, "_mysql: init failed"); module = NULL; } -#ifdef IS_PY3K return module; -#endif } /* vim: set ts=4 sts=4 sw=4 expandtab : */ diff --git a/MySQLdb/connections.py b/src/MySQLdb/connections.py similarity index 52% rename from MySQLdb/connections.py rename to src/MySQLdb/connections.py index 96b01528..a61aaaed 100644 --- a/MySQLdb/connections.py +++ b/src/MySQLdb/connections.py @@ -5,31 +5,33 @@ override Connection.default_cursor with a non-standard Cursor class. """ import re -import sys - -from MySQLdb import cursors, _mysql -from MySQLdb.compat import unicode, PY2 -from MySQLdb._mysql_exceptions import ( - Warning, Error, InterfaceError, DataError, - DatabaseError, OperationalError, IntegrityError, InternalError, - NotSupportedError, ProgrammingError, -) - -if not PY2: - if sys.version_info[:2] < (3, 6): - # See http://bugs.python.org/issue24870 - _surrogateescape_table = [chr(i) if i < 0x80 else chr(i + 0xdc00) for i in range(256)] - - def _fast_surrogateescape(s): - return s.decode('latin1').translate(_surrogateescape_table) - else: - def _fast_surrogateescape(s): - return s.decode('ascii', 'surrogateescape') +from . import cursors, _mysql +from ._exceptions import ( + Warning, + Error, + InterfaceError, + DataError, + DatabaseError, + OperationalError, + IntegrityError, + InternalError, + NotSupportedError, + ProgrammingError, +) +# Mapping from MySQL charset name to Python codec name +_charset_to_encoding = { + "utf8mb4": "utf8", + "utf8mb3": "utf8", + "latin1": "cp1252", + "koi8r": "koi8_r", + "koi8u": "koi8_u", +} re_numeric_part = re.compile(r"^(\d+)") + def numeric_part(s): """Returns the leading numeric part of a string. @@ -60,9 +62,9 @@ def __init__(self, *args, **kwargs): :param str host: host to connect :param str user: user to connect as :param str password: password to use - :param str passwd: alias of password, for backward compatibility + :param str passwd: alias of password (deprecated) :param str database: database to use - :param str db: alias of database, for backward compatibility + :param str db: alias of database (deprecated) :param int port: TCP/IP port to connect to :param str unix_socket: location of unix_socket to use :param dict conv: conversion dictionary, see MySQLdb.converters @@ -85,33 +87,68 @@ class object, used to create cursors (keyword only) :param bool use_unicode: If True, text-like columns are returned as unicode objects - using the connection's character set. Otherwise, text-like - columns are returned as strings. columns are returned as - normal strings. Unicode objects will always be encoded to - the connection's character set regardless of this setting. - Default to False on Python 2 and True on Python 3. + using the connection's character set. Otherwise, text-like + columns are returned as bytes. Unicode objects will always + be encoded to the connection's character set regardless of + this setting. + Default to True. :param str charset: If supplied, the connection character set will be changed - to this character set (MySQL-4.1 and newer). This implies - use_unicode=True. + to this character set. + + :param str collation: + If ``charset`` and ``collation`` are both supplied, the + character set and collation for the current connection + will be set. + + If omitted, empty string, or None, the default collation + for the ``charset`` is implied. + + :param str auth_plugin: + If supplied, the connection default authentication plugin will be + changed to this value. Example values: + `mysql_native_password` or `caching_sha2_password` :param str sql_mode: If supplied, the session SQL mode will be changed to this - setting (MySQL-4.1 and newer). For more details and legal - values, see the MySQL documentation. + setting. + For more details and legal values, see the MySQL documentation. :param int client_flag: flags to use or 0 (see MySQL docs or constants/CLIENTS.py) + :param bool multi_statements: + If True, enable multi statements for clients >= 4.1. + Defaults to True. + + :param str ssl_mode: + specify the security settings for connection to the server; + see the MySQL documentation for more details + (mysql_option(), MYSQL_OPT_SSL_MODE). + Only one of 'DISABLED', 'PREFERRED', 'REQUIRED', + 'VERIFY_CA', 'VERIFY_IDENTITY' can be specified. + :param dict ssl: dictionary or mapping contains SSL connection parameters; see the MySQL documentation for more details (mysql_ssl_set()). If this is set, and the client does not support SSL, NotSupportedError will be raised. + Since mysqlclient 2.2.4, ssl=True is alias of ssl_mode=REQUIRED + for better compatibility with PyMySQL and MariaDB. + + :param str server_public_key_path: + specify the path to a file RSA public key file for caching_sha2_password. + See https://dev.mysql.com/doc/refman/9.0/en/caching-sha2-pluggable-authentication.html :param bool local_infile: - enables LOAD LOCAL INFILE; zero disables + sets ``MYSQL_OPT_LOCAL_INFILE`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; zero disables; + + :param str local_infile_dir: + sets ``MYSQL_OPT_LOAD_DATA_LOCAL_DIR`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; + if ``local_infile`` is set to ``True`` then this is ignored; + + supported for mysql version >= 8.0.21 :param bool autocommit: If False (default), autocommit is disabled. @@ -126,18 +163,17 @@ class object, used to create cursors (keyword only) documentation for the MySQL C API for some hints on what they do. """ from MySQLdb.constants import CLIENT, FIELD_TYPE - from MySQLdb.converters import conversions - from weakref import proxy + from MySQLdb.converters import conversions, _bytes_or_str kwargs2 = kwargs.copy() - if 'database' in kwargs2: - kwargs2['db'] = kwargs2.pop('database') - if 'password' in kwargs2: - kwargs2['passwd'] = kwargs2.pop('password') + if "db" in kwargs2: + kwargs2["database"] = kwargs2.pop("db") + if "passwd" in kwargs2: + kwargs2["password"] = kwargs2.pop("passwd") - if 'conv' in kwargs: - conv = kwargs['conv'] + if "conv" in kwargs: + conv = kwargs["conv"] else: conv = conversions @@ -147,81 +183,88 @@ class object, used to create cursors (keyword only) conv2[k] = v[:] else: conv2[k] = v - kwargs2['conv'] = conv2 - - cursorclass = kwargs2.pop('cursorclass', self.default_cursor) - charset = kwargs2.pop('charset', '') - - if charset or not PY2: - use_unicode = True - else: - use_unicode = False - - use_unicode = kwargs2.pop('use_unicode', use_unicode) - sql_mode = kwargs2.pop('sql_mode', '') - self._binary_prefix = kwargs2.pop('binary_prefix', False) - - client_flag = kwargs.get('client_flag', 0) - client_version = tuple([ numeric_part(n) for n in _mysql.get_client_info().split('.')[:2] ]) - if client_version >= (4, 1): + kwargs2["conv"] = conv2 + + cursorclass = kwargs2.pop("cursorclass", self.default_cursor) + charset = kwargs2.get("charset", "") + collation = kwargs2.pop("collation", "") + use_unicode = kwargs2.pop("use_unicode", True) + sql_mode = kwargs2.pop("sql_mode", "") + self._binary_prefix = kwargs2.pop("binary_prefix", False) + + client_flag = kwargs.get("client_flag", 0) + client_flag |= CLIENT.MULTI_RESULTS + multi_statements = kwargs2.pop("multi_statements", True) + if multi_statements: client_flag |= CLIENT.MULTI_STATEMENTS - if client_version >= (5, 0): - client_flag |= CLIENT.MULTI_RESULTS - - kwargs2['client_flag'] = client_flag + kwargs2["client_flag"] = client_flag # PEP-249 requires autocommit to be initially off - autocommit = kwargs2.pop('autocommit', False) - - super(Connection, self).__init__(*args, **kwargs2) - self.cursorclass = cursorclass - self.encoders = dict([ (k, v) for k, v in conv.items() - if type(k) is not int ]) - - self._server_version = tuple([ numeric_part(n) for n in self.get_server_info().split('.')[:2] ]) - - self.encoding = 'ascii' # overridden in set_character_set() - db = proxy(self) - - # Note: string_literal() is called for bytes object on Python 3 (via bytes_literal) - def string_literal(obj, dummy=None): - return db.string_literal(obj) - - if PY2: - # unicode_literal is called for only unicode object. - def unicode_literal(u, dummy=None): - return db.string_literal(u.encode(db.encoding)) - else: - # unicode_literal() is called for arbitrary object. - def unicode_literal(u, dummy=None): - return db.string_literal(str(u).encode(db.encoding)) + autocommit = kwargs2.pop("autocommit", False) - def bytes_literal(obj, dummy=None): - return b'_binary' + db.string_literal(obj) + self._set_attributes(*args, **kwargs2) + super().__init__(*args, **kwargs2) - def string_decoder(s): - return s.decode(db.encoding) + self.cursorclass = cursorclass + self.encoders = { + k: v + for k, v in conv.items() + if type(k) is not int # noqa: E721 + } + self._server_version = tuple( + [numeric_part(n) for n in self.get_server_info().split(".")[:2]] + ) + self.encoding = "ascii" # overridden in set_character_set() if not charset: charset = self.character_set_name() - self.set_character_set(charset) + self.set_character_set(charset, collation) if sql_mode: self.set_sql_mode(sql_mode) if use_unicode: - for t in (FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING, FIELD_TYPE.VARCHAR, FIELD_TYPE.TINY_BLOB, - FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.LONG_BLOB, FIELD_TYPE.BLOB): - self.converter[t].append((None, string_decoder)) + for t in ( + FIELD_TYPE.STRING, + FIELD_TYPE.VAR_STRING, + FIELD_TYPE.VARCHAR, + FIELD_TYPE.TINY_BLOB, + FIELD_TYPE.MEDIUM_BLOB, + FIELD_TYPE.LONG_BLOB, + FIELD_TYPE.BLOB, + ): + self.converter[t] = _bytes_or_str + # Unlike other string/blob types, JSON is always text. + # MySQL may return JSON with charset==binary. + self.converter[FIELD_TYPE.JSON] = str - self.encoders[bytes] = string_literal - self.encoders[unicode] = unicode_literal self._transactional = self.server_capabilities & CLIENT.TRANSACTIONS if self._transactional: if autocommit is not None: self.autocommit(autocommit) self.messages = [] + def _set_attributes(self, host=None, user=None, password=None, database="", port=3306, + unix_socket=None, **kwargs): + """set some attributes for otel""" + if unix_socket and not host: + host = "localhost" + # support opentelemetry-instrumentation-dbapi + self.host = host + # _mysql.Connection provides self.port + self.user = user + self.database = database + # otel-inst-mysqlclient uses db instead of database. + self.db = database + # NOTE: We have not supported semantic conventions yet. + # https://opentelemetry.io/docs/specs/semconv/database/sql/ + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + def autocommit(self, on): on = bool(on) if self.get_autocommit() != on: @@ -246,11 +289,11 @@ def _bytes_literal(self, bs): assert isinstance(bs, (bytes, bytearray)) x = self.string_literal(bs) # x is escaped and quoted bytes if self._binary_prefix: - return b'_binary' + x + return b"_binary" + x return x def _tuple_literal(self, t): - return "(%s)" % (','.join(map(self.literal, t))) + return b"(%s)" % (b",".join(map(self.literal, t))) def literal(self, o): """If o is a single object, returns an SQL literal as a string. @@ -260,21 +303,19 @@ def literal(self, o): Non-standard. For internal use; do not use this in your applications. """ - if isinstance(o, bytearray): + if isinstance(o, str): + s = self.string_literal(o.encode(self.encoding)) + elif isinstance(o, bytearray): s = self._bytes_literal(o) - elif not PY2 and isinstance(o, bytes): + elif isinstance(o, bytes): s = self._bytes_literal(o) elif isinstance(o, (tuple, list)): s = self._tuple_literal(o) else: s = self.escape(o, self.encoders) - # Python 3(~3.4) doesn't support % operation for bytes object. - # We should decode it before using %. - # Decoding with ascii and surrogateescape allows convert arbitrary - # bytes to unicode and back again. - # See http://python.org/dev/peps/pep-0383/ - if not PY2 and isinstance(s, (bytes, bytearray)): - return _fast_surrogateescape(s) + if isinstance(s, str): + s = s.encode(self.encoding) + assert isinstance(s, bytes) return s def begin(self): @@ -282,37 +323,15 @@ def begin(self): This method is not used when autocommit=False (default). """ - self.query("BEGIN") - - if not hasattr(_mysql.connection, 'warning_count'): + self.query(b"BEGIN") - def warning_count(self): - """Return the number of warnings generated from the - last query. This is derived from the info() method.""" - info = self.info() - if info: - return int(info.split()[-1]) - else: - return 0 - - def set_character_set(self, charset): - """Set the connection character set to charset. The character - set can only be changed in MySQL-4.1 and newer. If you try - to change the character set from the current value in an - older version, NotSupportedError will be raised.""" - if charset in ("utf8mb4", "utf8mb3"): - py_charset = "utf8" - else: - py_charset = charset - if self.character_set_name() != charset: - try: - super(Connection, self).set_character_set(charset) - except AttributeError: - if self._server_version < (4, 1): - raise NotSupportedError("server is too old to set charset") - self.query('SET NAMES %s' % charset) - self.store_result() - self.encoding = py_charset + def set_character_set(self, charset, collation=None): + """Set the connection character set to charset.""" + super().set_character_set(charset) + self.encoding = _charset_to_encoding.get(charset, charset) + if collation: + self.query(f"SET NAMES {charset} COLLATE {collation}") + self.store_result() def set_sql_mode(self, sql_mode): """Set the connection sql_mode. See MySQL documentation for @@ -327,7 +346,8 @@ def show_warnings(self): sequence of tuples of (Level, Code, Message). This is only supported in MySQL-4.1 and up. If your server is an earlier version, an empty sequence is returned.""" - if self._server_version < (4,1): return () + if self._server_version < (4, 1): + return () self.query("SHOW WARNINGS") r = self.store_result() warnings = r.fetch_row(0) @@ -344,4 +364,5 @@ def show_warnings(self): ProgrammingError = ProgrammingError NotSupportedError = NotSupportedError + # vim: colorcolumn=100 diff --git a/MySQLdb/constants/CLIENT.py b/src/MySQLdb/constants/CLIENT.py similarity index 90% rename from MySQLdb/constants/CLIENT.py rename to src/MySQLdb/constants/CLIENT.py index 6559917b..35f578cc 100644 --- a/MySQLdb/constants/CLIENT.py +++ b/src/MySQLdb/constants/CLIENT.py @@ -20,10 +20,8 @@ INTERACTIVE = 1024 SSL = 2048 IGNORE_SIGPIPE = 4096 -TRANSACTIONS = 8192 # mysql_com.h was WRONG prior to 3.23.35 +TRANSACTIONS = 8192 # mysql_com.h was WRONG prior to 3.23.35 RESERVED = 16384 SECURE_CONNECTION = 32768 MULTI_STATEMENTS = 65536 MULTI_RESULTS = 131072 - - diff --git a/MySQLdb/constants/CR.py b/src/MySQLdb/constants/CR.py similarity index 85% rename from MySQLdb/constants/CR.py rename to src/MySQLdb/constants/CR.py index 1b047243..9467ae11 100644 --- a/MySQLdb/constants/CR.py +++ b/src/MySQLdb/constants/CR.py @@ -9,16 +9,18 @@ """ Usage: python CR.py [/path/to/mysql/errmsg.h ...] >> CR.py """ - import fileinput, re + import fileinput + import re + data = {} error_last = None for line in fileinput.input(): - line = re.sub(r'/\*.*?\*/', '', line) - m = re.match(r'^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)', line) + line = re.sub(r"/\*.*?\*/", "", line) + m = re.match(r"^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)", line) if m: name = m.group(1) value = int(m.group(2)) - if name == 'ERROR_LAST': + if name == "ERROR_LAST": if error_last is None or error_last < value: error_last = value continue @@ -27,9 +29,9 @@ data[value].add(name) for value, names in sorted(data.items()): for name in sorted(names): - print('%s = %s' % (name, value)) + print(f"{name} = {value}") if error_last is not None: - print('ERROR_LAST = %s' % error_last) + print("ERROR_LAST = %s" % error_last) ERROR_FIRST = 2000 @@ -83,7 +85,6 @@ SHARED_MEMORY_CONNECT_SET_ERROR = 2046 CONN_UNKNOW_PROTOCOL = 2047 INVALID_CONN_HANDLE = 2048 -SECURE_AUTH = 2049 UNUSED_1 = 2049 FETCH_CANCELED = 2050 NO_DATA = 2051 @@ -94,11 +95,11 @@ STMT_CLOSED = 2056 NEW_STMT_METADATA = 2057 ALREADY_CONNECTED = 2058 -AUTH_PLUGIN_CANNOT_LOAD = 2058 -ALREADY_CONNECTED = 2059 AUTH_PLUGIN_CANNOT_LOAD = 2059 DUPLICATE_CONNECTION_ATTR = 2060 -PLUGIN_FUNCTION_NOT_SUPPORTED = 2060 AUTH_PLUGIN_ERR = 2061 +INSECURE_API_ERR = 2062 +FILE_NAME_TOO_LONG = 2063 +SSL_FIPS_MODE_ERR = 2064 MAX_ERROR = 2999 -ERROR_LAST = 2061 +ERROR_LAST = 2064 diff --git a/MySQLdb/constants/ER.py b/src/MySQLdb/constants/ER.py similarity index 75% rename from MySQLdb/constants/ER.py rename to src/MySQLdb/constants/ER.py index 59db2e1f..8c5ece24 100644 --- a/MySQLdb/constants/ER.py +++ b/src/MySQLdb/constants/ER.py @@ -2,25 +2,26 @@ These constants are error codes for the bulk of the error conditions that may occur. - """ if __name__ == "__main__": """ Usage: python ER.py [/path/to/mysql/mysqld_error.h ...] >> ER.py """ - import fileinput, re + import fileinput + import re + data = {} error_last = None for line in fileinput.input(): - line = re.sub(r'/\*.*?\*/', '', line) - m = re.match(r'^\s*#define\s+((ER|WARN)_[A-Z0-9_]+)\s+(\d+)\s*', line) + line = re.sub(r"/\*.*?\*/", "", line) + m = re.match(r"^\s*#define\s+((ER|WARN)_[A-Z0-9_]+)\s+(\d+)\s*", line) if m: name = m.group(1) - if name.startswith('ER_'): + if name.startswith("ER_"): name = name[3:] value = int(m.group(3)) - if name == 'ERROR_LAST': + if name == "ERROR_LAST": if error_last is None or error_last < value: error_last = value continue @@ -29,14 +30,12 @@ data[value].add(name) for value, names in sorted(data.items()): for name in sorted(names): - print('%s = %s' % (name, value)) + print(f"{name} = {value}") if error_last is not None: - print('ERROR_LAST = %s' % error_last) + print("ERROR_LAST = %s" % error_last) ERROR_FIRST = 1000 -HASHCHK = 1000 -NISAMCHK = 1001 NO = 1002 YES = 1003 CANT_CREATE_FILE = 1004 @@ -44,27 +43,20 @@ CANT_CREATE_DB = 1006 DB_CREATE_EXISTS = 1007 DB_DROP_EXISTS = 1008 -DB_DROP_DELETE = 1009 DB_DROP_RMDIR = 1010 -CANT_DELETE_FILE = 1011 CANT_FIND_SYSTEM_REC = 1012 CANT_GET_STAT = 1013 -CANT_GET_WD = 1014 CANT_LOCK = 1015 CANT_OPEN_FILE = 1016 FILE_NOT_FOUND = 1017 CANT_READ_DIR = 1018 -CANT_SET_WD = 1019 CHECKREAD = 1020 -DISK_FULL = 1021 DUP_KEY = 1022 -ERROR_ON_CLOSE = 1023 ERROR_ON_READ = 1024 ERROR_ON_RENAME = 1025 ERROR_ON_WRITE = 1026 FILE_USED = 1027 FILSORT_ABORT = 1028 -FORM_NOT_FOUND = 1029 GET_ERRNO = 1030 ILLEGAL_HA = 1031 KEY_NOT_FOUND = 1032 @@ -74,7 +66,6 @@ OPEN_AS_READONLY = 1036 OUTOFMEMORY = 1037 OUT_OF_SORTMEMORY = 1038 -UNEXPECTED_EOF = 1039 CON_COUNT_ERROR = 1040 OUT_OF_RESOURCES = 1041 BAD_HOST_ERROR = 1042 @@ -112,8 +103,6 @@ TOO_BIG_FIELDLENGTH = 1074 WRONG_AUTO_KEY = 1075 READY = 1076 -NORMAL_SHUTDOWN = 1077 -GOT_SIGNAL = 1078 SHUTDOWN_COMPLETE = 1079 FORCING_CLOSE = 1080 IPSOCK_ERROR = 1081 @@ -128,7 +117,6 @@ CANT_REMOVE_ALL_FIELDS = 1090 CANT_DROP_FIELD_OR_KEY = 1091 INSERT_INFO = 1092 -INSERT_TABLE_USED = 1093 UPDATE_TABLE_USED = 1093 NO_SUCH_THREAD = 1094 KILL_DENIED_ERROR = 1095 @@ -156,7 +144,7 @@ TOO_MANY_FIELDS = 1117 TOO_BIG_ROWSIZE = 1118 STACK_OVERRUN = 1119 -WRONG_OUTER_JOIN = 1120 +WRONG_OUTER_JOIN_UNUSED = 1120 NULL_COLUMN_IN_INDEX = 1121 CANT_FIND_UDF = 1122 CANT_INITIALIZE_UDF = 1123 @@ -186,10 +174,6 @@ NONEXISTING_TABLE_GRANT = 1147 NOT_ALLOWED_COMMAND = 1148 SYNTAX_ERROR = 1149 -DELAYED_CANT_CHANGE_LOCK = 1150 -UNUSED1 = 1150 -TOO_MANY_DELAYED_THREADS = 1151 -UNUSED2 = 1151 ABORTING_CONNECTION = 1152 NET_PACKET_TOO_LARGE = 1153 NET_READ_ERROR_FROM_PIPE = 1154 @@ -203,8 +187,6 @@ TOO_LONG_STRING = 1162 TABLE_CANT_HANDLE_BLOB = 1163 TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164 -DELAYED_INSERT_TABLE_LOCKED = 1165 -UNUSED3 = 1165 WRONG_COLUMN_NAME = 1166 WRONG_KEY_COLUMN = 1167 WRONG_MRG_TABLE = 1168 @@ -213,7 +195,6 @@ PRIMARY_CANT_HAVE_NULL = 1171 TOO_MANY_ROWS = 1172 REQUIRES_PRIMARY_KEY = 1173 -NO_RAID_COMPILED = 1174 UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175 KEY_DOES_NOT_EXITS = 1176 CHECK_NO_SUCH_TABLE = 1177 @@ -222,11 +203,7 @@ ERROR_DURING_COMMIT = 1180 ERROR_DURING_ROLLBACK = 1181 ERROR_DURING_FLUSH_LOGS = 1182 -ERROR_DURING_CHECKPOINT = 1183 NEW_ABORTING_CONNECTION = 1184 -DUMP_NOT_IMPLEMENTED = 1185 -FLUSH_MASTER_BINLOG_CLOSED = 1186 -INDEX_REBUILD = 1187 MASTER = 1188 MASTER_NET_READ = 1189 MASTER_NET_WRITE = 1190 @@ -237,7 +214,6 @@ CRASHED_ON_REPAIR = 1195 WARNING_NOT_COMPLETE_ROLLBACK = 1196 TRANS_CACHE_FULL = 1197 -SLAVE_MUST_STOP = 1198 SLAVE_NOT_RUNNING = 1199 BAD_SLAVE = 1200 MASTER_INFO = 1201 @@ -247,19 +223,14 @@ LOCK_WAIT_TIMEOUT = 1205 LOCK_TABLE_FULL = 1206 READ_ONLY_TRANSACTION = 1207 -DROP_DB_WITH_READ_LOCK = 1208 -CREATE_DB_WITH_READ_LOCK = 1209 WRONG_ARGUMENTS = 1210 NO_PERMISSION_TO_CREATE_USER = 1211 -UNION_TABLES_IN_DIFFERENT_DIR = 1212 LOCK_DEADLOCK = 1213 TABLE_CANT_HANDLE_FT = 1214 -TABLE_CANT_HANDLE_FULLTEXT = 1214 CANNOT_ADD_FOREIGN = 1215 NO_REFERENCED_ROW = 1216 ROW_IS_REFERENCED = 1217 CONNECT_TO_MASTER = 1218 -QUERY_ON_MASTER = 1219 ERROR_WHEN_EXECUTING_COMMAND = 1220 WRONG_USAGE = 1221 WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222 @@ -285,7 +256,6 @@ SUBQUERY_NO_1_ROW = 1242 UNKNOWN_STMT_HANDLER = 1243 CORRUPT_HELP_DB = 1244 -CYCLIC_REFERENCE = 1245 AUTO_CONVERT = 1246 ILLEGAL_REFERENCE = 1247 DERIVED_MUST_HAVE_ALIAS = 1248 @@ -294,8 +264,6 @@ NOT_SUPPORTED_AUTH_MODE = 1251 SPATIAL_CANT_HAVE_NULL = 1252 COLLATION_CHARSET_MISMATCH = 1253 -SLAVE_WAS_RUNNING = 1254 -SLAVE_WAS_NOT_RUNNING = 1255 TOO_BIG_FOR_UNCOMPRESS = 1256 ZLIB_Z_MEM_ERROR = 1257 ZLIB_Z_BUF_ERROR = 1258 @@ -308,7 +276,6 @@ WARN_DATA_TRUNCATED = 1265 WARN_USING_OTHER_HANDLER = 1266 CANT_AGGREGATE_2COLLATIONS = 1267 -DROP_USER = 1268 REVOKE_GRANTS = 1269 CANT_AGGREGATE_3COLLATIONS = 1270 CANT_AGGREGATE_NCOLLATIONS = 1271 @@ -322,7 +289,6 @@ UNTIL_COND_IGNORED = 1279 WRONG_NAME_FOR_INDEX = 1280 WRONG_NAME_FOR_CATALOG = 1281 -WARN_QC_RESIZE = 1282 BAD_FT_COLUMN = 1283 UNKNOWN_KEY_CACHE = 1284 WARN_HOSTNAME_WONT_WORK = 1285 @@ -333,7 +299,6 @@ OPTION_PREVENTS_STATEMENT = 1290 DUPLICATED_VALUE_IN_TYPE = 1291 TRUNCATED_WRONG_VALUE = 1292 -TOO_MUCH_AUTO_TIMESTAMP_COLS = 1293 INVALID_ON_UPDATE = 1294 UNSUPPORTED_PS = 1295 GET_ERRMSG = 1296 @@ -386,10 +351,8 @@ FPARSER_ERROR_IN_PARAMETER = 1343 FPARSER_EOF_IN_UNKNOWN_PARAMETER = 1344 VIEW_NO_EXPLAIN = 1345 -FRM_UNKNOWN_TYPE = 1346 WRONG_OBJECT = 1347 NONUPDATEABLE_COLUMN = 1348 -VIEW_SELECT_DERIVED = 1349 VIEW_SELECT_CLAUSE = 1350 VIEW_SELECT_VARIABLE = 1351 VIEW_SELECT_TMPTABLE = 1352 @@ -398,7 +361,6 @@ WARN_VIEW_WITHOUT_KEY = 1355 VIEW_INVALID = 1356 SP_NO_DROP_SP = 1357 -SP_GOTO_IN_HNDLR = 1358 TRG_ALREADY_EXISTS = 1359 TRG_DOES_NOT_EXIST = 1360 TRG_ON_VIEW_OR_TEMP_TABLE = 1361 @@ -412,7 +374,6 @@ VIEW_CHECK_FAILED = 1369 PROCACCESS_DENIED_ERROR = 1370 RELAY_LOG_FAIL = 1371 -PASSWD_LENGTH = 1372 UNKNOWN_TARGET_BINLOG = 1373 IO_ERR_LOG_INDEX_READ = 1374 BINLOG_PURGE_PROHIBITED = 1375 @@ -423,13 +384,6 @@ RELAY_LOG_INIT = 1380 NO_BINARY_LOGGING = 1381 RESERVED_SYNTAX = 1382 -WSAS_FAILED = 1383 -DIFF_GROUPS_PROC = 1384 -NO_GROUP_FOR_PROC = 1385 -ORDER_WITH_PROC = 1386 -LOGGING_PROHIBIT_CHANGING_OF = 1387 -NO_FILE_MAPPING = 1388 -WRONG_MAGIC = 1389 PS_MANY_PARAM = 1390 KEY_PART_0 = 1391 VIEW_CHECKSUM = 1392 @@ -457,10 +411,8 @@ SP_NOT_VAR_ARG = 1414 SP_NO_RETSET = 1415 CANT_CREATE_GEOMETRY_OBJECT = 1416 -FAILED_ROUTINE_BREAK_BINLOG = 1417 BINLOG_UNSAFE_ROUTINE = 1418 BINLOG_CREATE_ROUTINE_NEED_SUPER = 1419 -EXEC_STMT_WITH_OPEN_CURSOR = 1420 STMT_HAS_NO_OPEN_CURSOR = 1421 COMMIT_NOT_ALLOWED_IN_SF_OR_TRG = 1422 NO_DEFAULT_FOR_VIEW_FIELD = 1423 @@ -474,7 +426,6 @@ FOREIGN_DATA_SOURCE_DOESNT_EXIST = 1431 FOREIGN_DATA_STRING_INVALID_CANT_CREATE = 1432 FOREIGN_DATA_STRING_INVALID = 1433 -CANT_CREATE_FEDERATED_TABLE = 1434 TRG_IN_WRONG_SCHEMA = 1435 STACK_OVERRUN_NEED_MORE = 1436 TOO_LONG_BODY = 1437 @@ -486,7 +437,6 @@ VIEW_PREVENT_UPDATE = 1443 PS_NO_RECURSION = 1444 SP_CANT_SET_AUTOCOMMIT = 1445 -MALFORMED_DEFINER = 1446 VIEW_FRM_NO_USER = 1447 VIEW_OTHER_USER = 1448 NO_SUCH_USER = 1449 @@ -497,7 +447,6 @@ TRG_NO_DEFINER = 1454 OLD_FILE_FORMAT = 1455 SP_RECURSION_LIMIT = 1456 -SP_PROC_TABLE_CORRUPT = 1457 SP_WRONG_NAME = 1458 TABLE_NEEDS_UPGRADE = 1459 SP_NO_AGGREGATE = 1460 @@ -522,14 +471,10 @@ PARTITION_REQUIRES_VALUES_ERROR = 1479 PARTITION_WRONG_VALUES_ERROR = 1480 PARTITION_MAXVALUE_ERROR = 1481 -PARTITION_SUBPARTITION_ERROR = 1482 -PARTITION_SUBPART_MIX_ERROR = 1483 PARTITION_WRONG_NO_PART_ERROR = 1484 PARTITION_WRONG_NO_SUBPART_ERROR = 1485 WRONG_EXPR_IN_PARTITION_FUNC_ERROR = 1486 -NO_CONST_EXPR_IN_RANGE_OR_LIST_ERROR = 1487 FIELD_NOT_FOUND_PART_ERROR = 1488 -LIST_OF_FIELDS_ONLY_IN_HASH_ERROR = 1489 INCONSISTENT_PARTITION_INFO_ERROR = 1490 PARTITION_FUNC_NOT_ALLOWED_ERROR = 1491 PARTITIONS_MUST_BE_DEFINED_ERROR = 1492 @@ -562,7 +507,6 @@ CONSECUTIVE_REORG_PARTITIONS = 1519 REORG_OUTSIDE_RANGE = 1520 PARTITION_FUNCTION_FAILURE = 1521 -PART_STATE_ERROR = 1522 LIMITED_PART_RANGE = 1523 PLUGIN_IS_NOT_LOADED = 1524 WRONG_VALUE = 1525 @@ -575,53 +519,30 @@ SIZE_OVERFLOW_ERROR = 1532 ALTER_FILEGROUP_FAILED = 1533 BINLOG_ROW_LOGGING_FAILED = 1534 -BINLOG_ROW_WRONG_TABLE_DEF = 1535 -BINLOG_ROW_RBR_TO_SBR = 1536 EVENT_ALREADY_EXISTS = 1537 -EVENT_STORE_FAILED = 1538 EVENT_DOES_NOT_EXIST = 1539 -EVENT_CANT_ALTER = 1540 -EVENT_DROP_FAILED = 1541 EVENT_INTERVAL_NOT_POSITIVE_OR_TOO_BIG = 1542 EVENT_ENDS_BEFORE_STARTS = 1543 EVENT_EXEC_TIME_IN_THE_PAST = 1544 -EVENT_OPEN_TABLE_FAILED = 1545 -EVENT_NEITHER_M_EXPR_NOR_M_AT = 1546 -COL_COUNT_DOESNT_MATCH_CORRUPTED = 1547 -OBSOLETE_COL_COUNT_DOESNT_MATCH_CORRUPTED = 1547 -CANNOT_LOAD_FROM_TABLE = 1548 -OBSOLETE_CANNOT_LOAD_FROM_TABLE = 1548 -EVENT_CANNOT_DELETE = 1549 -EVENT_COMPILE_ERROR = 1550 EVENT_SAME_NAME = 1551 -EVENT_DATA_TOO_LONG = 1552 DROP_INDEX_FK = 1553 WARN_DEPRECATED_SYNTAX_WITH_VER = 1554 -CANT_WRITE_LOCK_LOG_TABLE = 1555 CANT_LOCK_LOG_TABLE = 1556 -FOREIGN_DUPLICATE_KEY = 1557 FOREIGN_DUPLICATE_KEY_OLD_UNUSED = 1557 COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE = 1558 TEMP_TABLE_PREVENTS_SWITCH_OUT_OF_RBR = 1559 STORED_FUNCTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1560 -NDB_CANT_SWITCH_BINLOG_FORMAT = 1561 PARTITION_NO_TEMPORARY = 1562 PARTITION_CONST_DOMAIN_ERROR = 1563 PARTITION_FUNCTION_IS_NOT_ALLOWED = 1564 -DDL_LOG_ERROR = 1565 NULL_IN_VALUES_LESS_THAN = 1566 WRONG_PARTITION_NAME = 1567 CANT_CHANGE_TX_CHARACTERISTICS = 1568 -CANT_CHANGE_TX_ISOLATION = 1568 DUP_ENTRY_AUTOINCREMENT_CASE = 1569 -EVENT_MODIFY_QUEUE_ERROR = 1570 EVENT_SET_VAR_ERROR = 1571 PARTITION_MERGE_ERROR = 1572 -CANT_ACTIVATE_LOG = 1573 -RBR_NOT_AVAILABLE = 1574 BASE64_DECODE_ERROR = 1575 EVENT_RECURSION_FORBIDDEN = 1576 -EVENTS_DB_ERROR = 1577 ONLY_INTEGERS_ALLOWED = 1578 UNSUPORTED_LOG_ENGINE = 1579 BAD_LOG_STATEMENT = 1580 @@ -634,41 +555,29 @@ BINLOG_PURGE_EMFILE = 1587 EVENT_CANNOT_CREATE_IN_THE_PAST = 1588 EVENT_CANNOT_ALTER_IN_THE_PAST = 1589 -SLAVE_INCIDENT = 1590 NO_PARTITION_FOR_GIVEN_VALUE_SILENT = 1591 BINLOG_UNSAFE_STATEMENT = 1592 -SLAVE_FATAL_ERROR = 1593 -SLAVE_RELAY_LOG_READ_FAILURE = 1594 -SLAVE_RELAY_LOG_WRITE_FAILURE = 1595 -SLAVE_CREATE_EVENT_FAILURE = 1596 -SLAVE_MASTER_COM_FAILURE = 1597 +BINLOG_FATAL_ERROR = 1593 BINLOG_LOGGING_IMPOSSIBLE = 1598 VIEW_NO_CREATION_CTX = 1599 VIEW_INVALID_CREATION_CTX = 1600 -SR_INVALID_CREATION_CTX = 1601 TRG_CORRUPTED_FILE = 1602 TRG_NO_CREATION_CTX = 1603 TRG_INVALID_CREATION_CTX = 1604 EVENT_INVALID_CREATION_CTX = 1605 TRG_CANT_OPEN_TABLE = 1606 -CANT_CREATE_SROUTINE = 1607 -NEVER_USED = 1608 NO_FORMAT_DESCRIPTION_EVENT_BEFORE_BINLOG_STATEMENT = 1609 SLAVE_CORRUPT_EVENT = 1610 -LOAD_DATA_INVALID_COLUMN = 1611 LOG_PURGE_NO_FILE = 1612 XA_RBTIMEOUT = 1613 XA_RBDEADLOCK = 1614 NEED_REPREPARE = 1615 -DELAYED_NOT_SUPPORTED = 1616 WARN_NO_MASTER_INFO = 1617 WARN_OPTION_IGNORED = 1618 PLUGIN_DELETE_BUILTIN = 1619 -WARN_PLUGIN_DELETE_BUILTIN = 1619 WARN_PLUGIN_BUSY = 1620 VARIABLE_IS_READONLY = 1621 WARN_ENGINE_TRANSACTION_ROLLBACK = 1622 -SLAVE_HEARTBEAT_FAILURE = 1623 SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE = 1624 NDB_REPLICATION_SCHEMA_ERROR = 1625 CONFLICT_FN_PARSE_ERROR = 1626 @@ -696,7 +605,6 @@ COND_ITEM_TOO_LONG = 1648 UNKNOWN_LOCALE = 1649 SLAVE_IGNORE_SERVER_IDS = 1650 -QUERY_CACHE_DISABLED = 1651 SAME_NAME_PARTITION_FIELD = 1652 PARTITION_COLUMN_LIST_ERROR = 1653 WRONG_TYPE_COLUMN_VALUE_ERROR = 1654 @@ -714,8 +622,6 @@ BINLOG_ROW_INJECTION_AND_STMT_MODE = 1666 BINLOG_MULTIPLE_ENGINES_AND_SELF_LOGGING_ENGINE = 1667 BINLOG_UNSAFE_LIMIT = 1668 -BINLOG_UNSAFE_INSERT_DELAYED = 1669 -UNUSED4 = 1669 BINLOG_UNSAFE_SYSTEM_TABLE = 1670 BINLOG_UNSAFE_AUTOINC_COLUMNS = 1671 BINLOG_UNSAFE_UDF = 1672 @@ -723,7 +629,6 @@ BINLOG_UNSAFE_SYSTEM_FUNCTION = 1674 BINLOG_UNSAFE_NONTRANS_AFTER_TRANS = 1675 MESSAGE_AND_STATEMENT = 1676 -SLAVE_CONVERSION_FAILED = 1677 SLAVE_CANT_CREATE_CONVERSION = 1678 INSIDE_TRANSACTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1679 PATH_LENGTH = 1680 @@ -746,7 +651,6 @@ VALUES_IS_NOT_INT_TYPE_ERROR = 1697 ACCESS_DENIED_NO_PASSWORD_ERROR = 1698 SET_PASSWORD_AUTH_PLUGIN = 1699 -GRANT_PLUGIN_USER_EXISTS = 1700 TRUNCATE_ILLEGAL_FK = 1701 PLUGIN_IS_PERMANENT = 1702 SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MIN = 1703 @@ -773,10 +677,8 @@ BINLOG_UNSAFE_INSERT_TWO_KEYS = 1724 TABLE_IN_FK_CHECK = 1725 UNSUPPORTED_ENGINE = 1726 -UNUSED_1 = 1726 BINLOG_UNSAFE_AUTOINC_NOT_FIRST = 1727 CANNOT_LOAD_FROM_TABLE_V2 = 1728 -LAST_MYSQL_ERROR_MESSAGE = 1728 MASTER_DELAY_VALUE_OUT_OF_RANGE = 1729 ONLY_FD_AND_RBR_EVENTS_ALLOWED_IN_BINLOG_STATEMENT = 1730 PARTITION_EXCHANGE_DIFFERENT_OPTION = 1731 @@ -789,15 +691,11 @@ BINLOG_CACHE_SIZE_GREATER_THAN_MAX = 1738 WARN_INDEX_NOT_APPLICABLE = 1739 PARTITION_EXCHANGE_FOREIGN_KEY = 1740 -NO_SUCH_KEY_VALUE = 1741 RPL_INFO_DATA_TOO_LONG = 1742 -NETWORK_READ_EVENT_CHECKSUM_FAILURE = 1743 -BINLOG_READ_EVENT_CHECKSUM_FAILURE = 1744 BINLOG_STMT_CACHE_SIZE_GREATER_THAN_MAX = 1745 CANT_UPDATE_TABLE_IN_CREATE_TABLE_SELECT = 1746 PARTITION_CLAUSE_ON_NONPARTITIONED = 1747 ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET = 1748 -NO_SUCH_PARTITION__UNUSED = 1749 CHANGE_RPL_INFO_REPOSITORY_FAILURE = 1750 WARNING_NOT_COMPLETE_ROLLBACK_WITH_CREATED_TEMP_TABLE = 1751 WARNING_NOT_COMPLETE_ROLLBACK_WITH_DROPPED_TEMP_TABLE = 1752 @@ -815,24 +713,19 @@ TABLE_HAS_NO_FT = 1764 VARIABLE_NOT_SETTABLE_IN_SF_OR_TRIGGER = 1765 VARIABLE_NOT_SETTABLE_IN_TRANSACTION = 1766 -GTID_NEXT_IS_NOT_IN_GTID_NEXT_LIST = 1767 -CANT_CHANGE_GTID_NEXT_IN_TRANSACTION_WHEN_GTID_NEXT_LIST_IS_NULL = 1768 SET_STATEMENT_CANNOT_INVOKE_FUNCTION = 1769 GTID_NEXT_CANT_BE_AUTOMATIC_IF_GTID_NEXT_LIST_IS_NON_NULL = 1770 -SKIPPING_LOGGED_TRANSACTION = 1771 MALFORMED_GTID_SET_SPECIFICATION = 1772 MALFORMED_GTID_SET_ENCODING = 1773 MALFORMED_GTID_SPECIFICATION = 1774 GNO_EXHAUSTED = 1775 BAD_SLAVE_AUTO_POSITION = 1776 -AUTO_POSITION_REQUIRES_GTID_MODE_ON = 1777 +AUTO_POSITION_REQUIRES_GTID_MODE_NOT_OFF = 1777 CANT_DO_IMPLICIT_COMMIT_IN_TRX_WHEN_GTID_NEXT_IS_SET = 1778 -GTID_MODE_2_OR_3_REQUIRES_ENFORCE_GTID_CONSISTENCY_ON = 1779 -GTID_MODE_REQUIRES_BINLOG = 1780 +GTID_MODE_ON_REQUIRES_ENFORCE_GTID_CONSISTENCY_ON = 1779 CANT_SET_GTID_NEXT_TO_GTID_WHEN_GTID_MODE_IS_OFF = 1781 CANT_SET_GTID_NEXT_TO_ANONYMOUS_WHEN_GTID_MODE_IS_ON = 1782 CANT_SET_GTID_NEXT_LIST_TO_NON_NULL_WHEN_GTID_MODE_IS_OFF = 1783 -FOUND_GTID_EVENT_WHEN_GTID_MODE_IS_OFF = 1784 GTID_UNSAFE_NON_TRANSACTIONAL_TABLE = 1785 GTID_UNSAFE_CREATE_SELECT = 1786 GTID_UNSAFE_CREATE_DROP_TEMPORARY_TABLE_IN_TRANSACTION = 1787 @@ -882,12 +775,10 @@ DUP_INDEX = 1831 FK_COLUMN_CANNOT_CHANGE = 1832 FK_COLUMN_CANNOT_CHANGE_CHILD = 1833 -UNUSED5 = 1834 MALFORMED_PACKET = 1835 READ_ONLY_MODE = 1836 -GTID_NEXT_TYPE_UNDEFINED_GROUP = 1837 +GTID_NEXT_TYPE_UNDEFINED_GTID = 1837 VARIABLE_NOT_SETTABLE_IN_SP = 1838 -CANT_SET_GTID_PURGED_WHEN_GTID_MODE_IS_OFF = 1839 CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY = 1840 CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY = 1841 GTID_PURGED_WAS_CHANGED = 1842 @@ -900,7 +791,6 @@ ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_RENAME = 1849 ALTER_OPERATION_NOT_SUPPORTED_REASON_COLUMN_TYPE = 1850 ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_CHECK = 1851 -UNUSED6 = 1852 ALTER_OPERATION_NOT_SUPPORTED_REASON_NOPK = 1853 ALTER_OPERATION_NOT_SUPPORTED_REASON_AUTOINC = 1854 ALTER_OPERATION_NOT_SUPPORTED_REASON_HIDDEN_FTS = 1855 @@ -913,7 +803,6 @@ MUST_CHANGE_PASSWORD_LOGIN = 1862 ROW_IN_WRONG_PARTITION = 1863 MTS_EVENT_BIGGER_PENDING_JOBS_SIZE_MAX = 1864 -INNODB_NO_FT_USES_PARSER = 1865 BINLOG_LOGICAL_CORRUPTION = 1866 WARN_PURGE_LOG_IN_USE = 1867 WARN_PURGE_LOG_IS_ACTIVE = 1868 @@ -932,129 +821,7 @@ INNODB_FORCED_RECOVERY = 1881 AES_INVALID_IV = 1882 PLUGIN_CANNOT_BE_UNINSTALLED = 1883 -GTID_UNSAFE_BINLOG_SPLITTABLE_STATEMENT_AND_GTID_GROUP = 1884 -FILE_CORRUPT = 1885 -ERROR_ON_MASTER = 1886 -INCONSISTENT_ERROR = 1887 -STORAGE_ENGINE_NOT_LOADED = 1888 -GET_STACKED_DA_WITHOUT_ACTIVE_HANDLER = 1889 -WARN_LEGACY_SYNTAX_CONVERTED = 1890 -BINLOG_UNSAFE_FULLTEXT_PLUGIN = 1891 -CANNOT_DISCARD_TEMPORARY_TABLE = 1892 -FK_DEPTH_EXCEEDED = 1893 -COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE_V2 = 1894 -WARN_TRIGGER_DOESNT_HAVE_CREATED = 1895 -REFERENCED_TRG_DOES_NOT_EXIST = 1896 -EXPLAIN_NOT_SUPPORTED = 1897 -INVALID_FIELD_SIZE = 1898 -MISSING_HA_CREATE_OPTION = 1899 -ENGINE_OUT_OF_MEMORY = 1900 -VCOL_BASED_ON_VCOL = 1900 -PASSWORD_EXPIRE_ANONYMOUS_USER = 1901 -VIRTUAL_COLUMN_FUNCTION_IS_NOT_ALLOWED = 1901 -DATA_CONVERSION_ERROR_FOR_VIRTUAL_COLUMN = 1902 -SLAVE_SQL_THREAD_MUST_STOP = 1902 -NO_FT_MATERIALIZED_SUBQUERY = 1903 -PRIMARY_KEY_BASED_ON_VIRTUAL_COLUMN = 1903 -INNODB_UNDO_LOG_FULL = 1904 -KEY_BASED_ON_GENERATED_VIRTUAL_COLUMN = 1904 -INVALID_ARGUMENT_FOR_LOGARITHM = 1905 -WRONG_FK_OPTION_FOR_VIRTUAL_COLUMN = 1905 -SLAVE_CHANNEL_IO_THREAD_MUST_STOP = 1906 -WARNING_NON_DEFAULT_VALUE_FOR_VIRTUAL_COLUMN = 1906 -UNSUPPORTED_ACTION_ON_VIRTUAL_COLUMN = 1907 -WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO = 1907 -CONST_EXPR_IN_VCOL = 1908 -WARN_ONLY_MASTER_LOG_FILE_NO_POS = 1908 -QUERY_TIMEOUT = 1909 -ROW_EXPR_FOR_VCOL = 1909 -NON_RO_SELECT_DISABLE_TIMER = 1910 -UNSUPPORTED_ENGINE_FOR_VIRTUAL_COLUMNS = 1910 -DUP_LIST_ENTRY = 1911 -UNKNOWN_OPTION = 1911 -BAD_OPTION_VALUE = 1912 -SQL_MODE_NO_EFFECT = 1912 -AGGREGATE_ORDER_FOR_UNION = 1913 -NETWORK_READ_EVENT_CHECKSUM_FAILURE = 1913 -AGGREGATE_ORDER_NON_AGG_QUERY = 1914 -BINLOG_READ_EVENT_CHECKSUM_FAILURE = 1914 -CANT_DO_ONLINE = 1915 -SLAVE_WORKER_STOPPED_PREVIOUS_THD_ERROR = 1915 -DATA_OVERFLOW = 1916 -DONT_SUPPORT_SLAVE_PRESERVE_COMMIT_ORDER = 1916 -DATA_TRUNCATED = 1917 -SERVER_OFFLINE_MODE = 1917 -BAD_DATA = 1918 -GIS_DIFFERENT_SRIDS = 1918 -DYN_COL_WRONG_FORMAT = 1919 -GIS_UNSUPPORTED_ARGUMENT = 1919 -DYN_COL_IMPLEMENTATION_LIMIT = 1920 -GIS_UNKNOWN_ERROR = 1920 -DYN_COL_DATA = 1921 -GIS_UNKNOWN_EXCEPTION = 1921 -DYN_COL_WRONG_CHARSET = 1922 -GIS_INVALID_DATA = 1922 -BOOST_GEOMETRY_EMPTY_INPUT_EXCEPTION = 1923 -ILLEGAL_SUBQUERY_OPTIMIZER_SWITCHES = 1923 -BOOST_GEOMETRY_CENTROID_EXCEPTION = 1924 -QUERY_CACHE_IS_DISABLED = 1924 -BOOST_GEOMETRY_OVERLAY_INVALID_INPUT_EXCEPTION = 1925 -QUERY_CACHE_IS_GLOBALY_DISABLED = 1925 -BOOST_GEOMETRY_TURN_INFO_EXCEPTION = 1926 -VIEW_ORDERBY_IGNORED = 1926 -BOOST_GEOMETRY_SELF_INTERSECTION_POINT_EXCEPTION = 1927 -CONNECTION_KILLED = 1927 -BOOST_GEOMETRY_UNKNOWN_EXCEPTION = 1928 -INTERNAL_ERROR = 1928 -INSIDE_TRANSACTION_PREVENTS_SWITCH_SKIP_REPLICATION = 1929 -STD_BAD_ALLOC_ERROR = 1929 -STD_DOMAIN_ERROR = 1930 -STORED_FUNCTION_PREVENTS_SWITCH_SKIP_REPLICATION = 1930 -QUERY_EXCEEDED_ROWS_EXAMINED_LIMIT = 1931 -STD_LENGTH_ERROR = 1931 -NO_SUCH_TABLE_IN_ENGINE = 1932 -STD_INVALID_ARGUMENT = 1932 -GEOMETRY_SRID_MISMATCH = 1933 -STD_OUT_OF_RANGE_ERROR = 1933 -NO_SUCH_SPATIAL_REF_ID = 1934 -STD_OVERFLOW_ERROR = 1934 -STD_RANGE_ERROR = 1935 -STD_UNDERFLOW_ERROR = 1936 -STD_LOGIC_ERROR = 1937 -STD_RUNTIME_ERROR = 1938 -STD_UNKNOWN_EXCEPTION = 1939 -GIS_DATA_WRONG_ENDIANESS = 1940 -CHANGE_MASTER_PASSWORD_LENGTH = 1941 -USER_LOCK_WRONG_NAME = 1942 -USER_LOCK_DEADLOCK = 1943 -REPLACE_INACCESSIBLE_ROWS = 1944 -ALTER_OPERATION_NOT_SUPPORTED_REASON_GIS = 1945 -ILLEGAL_USER_VAR = 1946 -GTID_MODE_OFF = 1947 -UNSUPPORTED_BY_REPLICATION_THREAD = 1948 -INCORRECT_TYPE = 1949 -FIELD_IN_ORDER_NOT_SELECT = 1950 -AGGREGATE_IN_ORDER_NOT_SELECT = 1951 -INVALID_RPL_WILD_TABLE_FILTER_PATTERN = 1952 -NET_OK_PACKET_TOO_LARGE = 1953 -INVALID_JSON_DATA = 1954 -INVALID_GEOJSON_MISSING_MEMBER = 1955 -INVALID_GEOJSON_WRONG_TYPE = 1956 -INVALID_GEOJSON_UNSPECIFIED = 1957 -DIMENSION_UNSUPPORTED = 1958 -SLAVE_CHANNEL_DOES_NOT_EXIST = 1959 -SLAVE_MULTIPLE_CHANNELS_HOST_PORT = 1960 -SLAVE_CHANNEL_NAME_INVALID_OR_TOO_LONG = 1961 -SLAVE_NEW_CHANNEL_WRONG_REPOSITORY = 1962 -SLAVE_CHANNEL_DELETE = 1963 -SLAVE_MULTIPLE_CHANNELS_CMD = 1964 -SLAVE_MAX_CHANNELS_EXCEEDED = 1965 -SLAVE_CHANNEL_MUST_STOP = 1966 -SLAVE_CHANNEL_NOT_RUNNING = 1967 -SLAVE_CHANNEL_WAS_RUNNING = 1968 -SLAVE_CHANNEL_WAS_NOT_RUNNING = 1969 -SLAVE_CHANNEL_SQL_THREAD_MUST_STOP = 1970 -SLAVE_CHANNEL_SQL_SKIP_COUNTER = 1971 -WRONG_FIELD_WITH_GROUP_V2 = 1972 -MIX_OF_GROUP_FUNC_AND_FIELDS_V2 = 1973 +GTID_UNSAFE_BINLOG_SPLITTABLE_STATEMENT_AND_ASSIGNED_GTID = 1884 +SLAVE_HAS_MORE_GTIDS_THAN_MASTER = 1885 +MISSING_KEY = 1886 ERROR_LAST = 1973 diff --git a/MySQLdb/constants/FIELD_TYPE.py b/src/MySQLdb/constants/FIELD_TYPE.py similarity index 80% rename from MySQLdb/constants/FIELD_TYPE.py rename to src/MySQLdb/constants/FIELD_TYPE.py index 8a57b171..3c4eca9f 100644 --- a/MySQLdb/constants/FIELD_TYPE.py +++ b/src/MySQLdb/constants/FIELD_TYPE.py @@ -2,7 +2,6 @@ These constants represent the various column (field) types that are supported by MySQL. - """ DECIMAL = 0 @@ -19,9 +18,13 @@ TIME = 11 DATETIME = 12 YEAR = 13 -NEWDATE = 14 +# NEWDATE = 14 # Internal to MySQL. VARCHAR = 15 BIT = 16 +# TIMESTAMP2 = 17 +# DATETIME2 = 18 +# TIME2 = 19 +JSON = 245 NEWDECIMAL = 246 ENUM = 247 SET = 248 @@ -34,4 +37,4 @@ GEOMETRY = 255 CHAR = TINY -INTERVAL = ENUM +INTERVAL = ENUM diff --git a/MySQLdb/constants/FLAG.py b/src/MySQLdb/constants/FLAG.py similarity index 100% rename from MySQLdb/constants/FLAG.py rename to src/MySQLdb/constants/FLAG.py diff --git a/src/MySQLdb/constants/__init__.py b/src/MySQLdb/constants/__init__.py new file mode 100644 index 00000000..0372265b --- /dev/null +++ b/src/MySQLdb/constants/__init__.py @@ -0,0 +1 @@ +__all__ = ["CR", "FIELD_TYPE", "CLIENT", "ER", "FLAG"] diff --git a/MySQLdb/converters.py b/src/MySQLdb/converters.py similarity index 59% rename from MySQLdb/converters.py rename to src/MySQLdb/converters.py index c13e4265..d6fdc01c 100644 --- a/MySQLdb/converters.py +++ b/src/MySQLdb/converters.py @@ -30,76 +30,81 @@ (with the copy() method), modify the copies, and then pass them to MySQL.connect(). """ +from decimal import Decimal -from MySQLdb._mysql import string_literal, escape, NULL +from MySQLdb._mysql import string_literal from MySQLdb.constants import FIELD_TYPE, FLAG -from MySQLdb.times import * -from MySQLdb.compat import PY2, long - -NoneType = type(None) +from MySQLdb.times import ( + Date, + DateTimeType, + DateTime2literal, + DateTimeDeltaType, + DateTimeDelta2literal, + DateTime_or_None, + TimeDelta_or_None, + Date_or_None, +) +from MySQLdb._exceptions import ProgrammingError import array +NoneType = type(None) + try: ArrayType = array.ArrayType except AttributeError: ArrayType = array.array -def Bool2Str(s, d): return str(int(s)) +def Bool2Str(s, d): + return b"1" if s else b"0" -def Str2Set(s): - return set([ i for i in s.split(',') if i ]) def Set2Str(s, d): # Only support ascii string. Not tested. - return string_literal(','.join(s), d) + return string_literal(",".join(s)) + def Thing2Str(s, d): """Convert something into a string via str().""" return str(s) -def Unicode2Str(s, d): - """Convert a unicode object to a string using the default encoding. - This is only used as a placeholder for the real function, which - is connection-dependent.""" - return s.encode() def Float2Str(o, d): s = repr(o) - if 'e' not in s: - s += 'e0' + if s in ("inf", "-inf", "nan"): + raise ProgrammingError("%s can not be used with MySQL" % s) + if "e" not in s: + s += "e0" return s + def None2NULL(o, d): """Convert None to NULL.""" - return NULL # duh + return b"NULL" + def Thing2Literal(o, d): """Convert something into a SQL string literal. If using MySQL-3.23 or newer, string_literal() is a method of the _mysql.MYSQL object, and this function will be overridden with that method when the connection is created.""" - return string_literal(o, d) + return string_literal(o) + def Decimal2Literal(o, d): - return format(o, 'f') + return format(o, "f") -def char_array(s): - return array.array('c', s) def array2Str(o, d): return Thing2Literal(o.tostring(), d) -def quote_tuple(t, d): - return "(%s)" % (','.join(escape_sequence(t, d))) # bytes or str regarding to BINARY_FLAG. -_bytes_or_str = [(FLAG.BINARY, bytes)] +_bytes_or_str = ((FLAG.BINARY, bytes), (None, str)) conversions = { int: Thing2Str, - long: Thing2Str, float: Float2Str, NoneType: None2NULL, ArrayType: array2Str, @@ -107,43 +112,28 @@ def quote_tuple(t, d): Date: Thing2Literal, DateTimeType: DateTime2literal, DateTimeDeltaType: DateTimeDelta2literal, - str: Thing2Literal, # default set: Set2Str, - + Decimal: Decimal2Literal, FIELD_TYPE.TINY: int, FIELD_TYPE.SHORT: int, - FIELD_TYPE.LONG: long, + FIELD_TYPE.LONG: int, FIELD_TYPE.FLOAT: float, FIELD_TYPE.DOUBLE: float, - FIELD_TYPE.DECIMAL: float, - FIELD_TYPE.NEWDECIMAL: float, - FIELD_TYPE.LONGLONG: long, + FIELD_TYPE.DECIMAL: Decimal, + FIELD_TYPE.NEWDECIMAL: Decimal, + FIELD_TYPE.LONGLONG: int, FIELD_TYPE.INT24: int, FIELD_TYPE.YEAR: int, - FIELD_TYPE.SET: Str2Set, - FIELD_TYPE.TIMESTAMP: mysql_timestamp_converter, + FIELD_TYPE.TIMESTAMP: DateTime_or_None, FIELD_TYPE.DATETIME: DateTime_or_None, FIELD_TYPE.TIME: TimeDelta_or_None, FIELD_TYPE.DATE: Date_or_None, - - FIELD_TYPE.TINY_BLOB: _bytes_or_str, - FIELD_TYPE.MEDIUM_BLOB: _bytes_or_str, - FIELD_TYPE.LONG_BLOB: _bytes_or_str, - FIELD_TYPE.BLOB: _bytes_or_str, - FIELD_TYPE.STRING: _bytes_or_str, - FIELD_TYPE.VAR_STRING: _bytes_or_str, - FIELD_TYPE.VARCHAR: _bytes_or_str, + FIELD_TYPE.TINY_BLOB: bytes, + FIELD_TYPE.MEDIUM_BLOB: bytes, + FIELD_TYPE.LONG_BLOB: bytes, + FIELD_TYPE.BLOB: bytes, + FIELD_TYPE.STRING: bytes, + FIELD_TYPE.VAR_STRING: bytes, + FIELD_TYPE.VARCHAR: bytes, + FIELD_TYPE.JSON: bytes, } - -if PY2: - conversions[unicode] = Unicode2Str -else: - conversions[bytes] = Thing2Literal - -try: - from decimal import Decimal - conversions[FIELD_TYPE.DECIMAL] = Decimal - conversions[FIELD_TYPE.NEWDECIMAL] = Decimal - conversions[Decimal] = Decimal2Literal -except ImportError: - pass diff --git a/MySQLdb/cursors.py b/src/MySQLdb/cursors.py similarity index 66% rename from MySQLdb/cursors.py rename to src/MySQLdb/cursors.py index 97908249..70fbeea4 100644 --- a/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -3,36 +3,27 @@ This module implements Cursors of various types for MySQLdb. By default, MySQLdb uses the Cursor class. """ -from __future__ import print_function, absolute_import -from functools import partial import re -import sys -from .compat import unicode -from ._mysql_exceptions import ( - Warning, Error, InterfaceError, DataError, - DatabaseError, OperationalError, IntegrityError, InternalError, - NotSupportedError, ProgrammingError) +from ._exceptions import ProgrammingError -PY2 = sys.version_info[0] == 2 -if PY2: - text_type = unicode -else: - text_type = str - - -#: Regular expression for :meth:`Cursor.executemany`. +#: Regular expression for ``Cursor.executemany```. #: executemany only supports simple bulk insert. #: You can use it to load large dataset. RE_INSERT_VALUES = re.compile( - r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)" + - r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" + - r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z", - re.IGNORECASE | re.DOTALL) - - -class BaseCursor(object): + "".join( + [ + r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)", + r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))", + r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z", + ] + ), + re.IGNORECASE | re.DOTALL, +) + + +class BaseCursor: """A base for Cursor classes. Useful attributes: description @@ -49,16 +40,24 @@ class BaseCursor(object): default number of rows fetchmany() will fetch """ - #: Max stetement size which :meth:`executemany` generates. + #: Max statement size which :meth:`executemany` generates. #: #: Max size of allowed statement is max_allowed_packet - packet_header_size. #: Default value of max_allowed_packet is 1048576. - max_stmt_length = 64*1024 - - from ._mysql_exceptions import ( - MySQLError, Warning, Error, InterfaceError, - DatabaseError, DataError, OperationalError, IntegrityError, - InternalError, ProgrammingError, NotSupportedError, + max_stmt_length = 64 * 1024 + + from ._exceptions import ( + MySQLError, + Warning, + Error, + InterfaceError, + DatabaseError, + DataError, + OperationalError, + IntegrityError, + InternalError, + ProgrammingError, + NotSupportedError, ) connection = None @@ -67,23 +66,41 @@ def __init__(self, connection): self.connection = connection self.description = None self.description_flags = None - self.rowcount = -1 + self.rowcount = 0 self.arraysize = 1 self._executed = None + self.lastrowid = None - self.messages = [] self._result = None - self._warnings = None self.rownumber = None self._rows = None + def _discard(self): + self.description = None + self.description_flags = None + # Django uses some member after __exit__. + # So we keep rowcount and lastrowid here. They are cleared in Cursor._query(). + # self.rowcount = 0 + # self.lastrowid = None + self._rows = None + self.rownumber = None + + if self._result: + self._result.discard() + self._result = None + + con = self.connection + if con is None: + return + while con.next_result() == 0: # -1 means no more data. + con.discard_result() + def close(self): """Close the cursor. No further queries will be possible.""" try: if self.connection is None: return - while self.nextset(): - pass + self._discard() finally: self.connection = None self._result = None @@ -95,32 +112,6 @@ def __exit__(self, *exc_info): del exc_info self.close() - def _ensure_bytes(self, x, encoding=None): - if isinstance(x, text_type): - x = x.encode(encoding) - elif isinstance(x, (tuple, list)): - x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x) - return x - - def _escape_args(self, args, conn): - ensure_bytes = partial(self._ensure_bytes, encoding=conn.encoding) - - if isinstance(args, (tuple, list)): - if PY2: - args = tuple(map(ensure_bytes, args)) - return tuple(conn.literal(arg) for arg in args) - elif isinstance(args, dict): - if PY2: - args = dict((ensure_bytes(key), ensure_bytes(val)) for - (key, val) in args.items()) - return dict((key, conn.literal(val)) for (key, val) in args.items()) - else: - # If it's not a dictionary let's try escaping it anyways. - # Worst case it will throw a Value error - if PY2: - args = ensure_bytes(args) - return conn.literal(args) - def _check_executed(self): if not self._executed: raise ProgrammingError("execute() first") @@ -132,7 +123,6 @@ def nextset(self): """ if self._executed: self.fetchall() - del self.messages[:] db = self._get_db() nr = db.next_result() @@ -153,7 +143,6 @@ def _do_get_result(self, db): self.rowcount = db.affected_rows() self.rownumber = 0 self.lastrowid = db.insert_id() - self._warnings = None def _post_get_result(self): pass @@ -182,37 +171,51 @@ def execute(self, query, args=None): Returns integer represents rows affected, if any """ - while self.nextset(): - pass - db = self._get_db() + self._discard() + + mogrified_query = self._mogrify(query, args) - # NOTE: - # Python 2: query should be bytes when executing %. - # All unicode in args should be encoded to bytes on Python 2. - # Python 3: query should be str (unicode) when executing %. - # All bytes in args should be decoded with ascii and surrogateescape on Python 3. - # db.literal(obj) always returns str. + assert isinstance(mogrified_query, (bytes, bytearray)) + res = self._query(mogrified_query) + return res + + def _mogrify(self, query, args=None): + """Return query after binding args.""" + db = self._get_db() - if PY2 and isinstance(query, unicode): + if isinstance(query, str): query = query.encode(db.encoding) if args is not None: if isinstance(args, dict): - args = dict((key, db.literal(item)) for key, item in args.items()) + nargs = {} + for key, item in args.items(): + if isinstance(key, str): + key = key.encode(db.encoding) + nargs[key] = db.literal(item) + args = nargs else: args = tuple(map(db.literal, args)) - if not PY2 and isinstance(query, (bytes, bytearray)): - query = query.decode(db.encoding) try: query = query % args except TypeError as m: raise ProgrammingError(str(m)) - if isinstance(query, unicode): - query = query.encode(db.encoding, 'surrogateescape') + return query - res = self._query(query) - return res + def mogrify(self, query, args=None): + """Return query after binding args. + + query -- string, query to mogrify + args -- optional sequence or mapping, parameters to use with query. + + Note: If args is a sequence, then %s must be used as the + parameter placeholder in the query. If a mapping is used, + %(key)s must be used as the placeholder. + + Returns string representing query that would be executed by the server + """ + return self._mogrify(query, args).decode(self._get_db().encoding) def executemany(self, query, args): # type: (str, list) -> int @@ -226,8 +229,6 @@ def executemany(self, query, args): REPLACE. Otherwise it is equivalent to looping over args with execute(). """ - del self.messages[:] - if not args: return @@ -235,46 +236,41 @@ def executemany(self, query, args): if m: q_prefix = m.group(1) % () q_values = m.group(2).rstrip() - q_postfix = m.group(3) or '' - assert q_values[0] == '(' and q_values[-1] == ')' - return self._do_execute_many(q_prefix, q_values, q_postfix, args, - self.max_stmt_length, - self._get_db().encoding) + q_postfix = m.group(3) or "" + assert q_values[0] == "(" and q_values[-1] == ")" + return self._do_execute_many( + q_prefix, + q_values, + q_postfix, + args, + self.max_stmt_length, + self._get_db().encoding, + ) self.rowcount = sum(self.execute(query, arg) for arg in args) return self.rowcount - def _do_execute_many(self, prefix, values, postfix, args, max_stmt_length, encoding): - conn = self._get_db() - escape = self._escape_args - if isinstance(prefix, text_type): + def _do_execute_many( + self, prefix, values, postfix, args, max_stmt_length, encoding + ): + if isinstance(prefix, str): prefix = prefix.encode(encoding) - if PY2 and isinstance(values, text_type): + if isinstance(values, str): values = values.encode(encoding) - if isinstance(postfix, text_type): + if isinstance(postfix, str): postfix = postfix.encode(encoding) sql = bytearray(prefix) args = iter(args) - v = values % escape(next(args), conn) - if isinstance(v, text_type): - if PY2: - v = v.encode(encoding) - else: - v = v.encode(encoding, 'surrogateescape') + v = self._mogrify(values, next(args)) sql += v rows = 0 for arg in args: - v = values % escape(arg, conn) - if isinstance(v, text_type): - if PY2: - v = v.encode(encoding) - else: - v = v.encode(encoding, 'surrogateescape') + v = self._mogrify(values, arg) if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: rows += self.execute(sql + postfix) sql = bytearray(prefix) else: - sql += b',' + sql += b"," sql += v rows += self.execute(sql + postfix) self.rowcount = rows @@ -308,28 +304,29 @@ def callproc(self, procname, args=()): to advance through all result sets; otherwise you may get disconnected. """ - db = self._get_db() + if isinstance(procname, str): + procname = procname.encode(db.encoding) if args: - fmt = '@_{0}_%d=%s'.format(procname) - q = 'SET %s' % ','.join(fmt % (index, db.literal(arg)) - for index, arg in enumerate(args)) - if isinstance(q, unicode): - q = q.encode(db.encoding, 'surrogateescape') + fmt = b"@_" + procname + b"_%d=%s" + q = b"SET %s" % b",".join( + fmt % (index, db.literal(arg)) for index, arg in enumerate(args) + ) self._query(q) self.nextset() - q = "CALL %s(%s)" % (procname, - ','.join(['@_%s_%d' % (procname, i) - for i in range(len(args))])) - if isinstance(q, unicode): - q = q.encode(db.encoding, 'surrogateescape') + q = b"CALL %s(%s)" % ( + procname, + b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]), + ) self._query(q) return args def _query(self, q): db = self._get_db() self._result = None + self.rowcount = None + self.lastrowid = None db.query(q) self._do_get_result(db) self._post_get_result() @@ -356,7 +353,7 @@ def __iter__(self): NotSupportedError = NotSupportedError -class CursorStoreResultMixIn(object): +class CursorStoreResultMixIn: """This is a MixIn class which causes the entire result set to be stored on the client side, i.e. it uses mysql_store_result(). If the result set can be very large, consider adding a LIMIT clause to your @@ -384,21 +381,21 @@ def fetchmany(self, size=None): than size. If size is not defined, cursor.arraysize is used.""" self._check_executed() end = self.rownumber + (size or self.arraysize) - result = self._rows[self.rownumber:end] + result = self._rows[self.rownumber : end] self.rownumber = min(end, len(self._rows)) return result def fetchall(self): - """Fetchs all available rows from the cursor.""" + """Fetches all available rows from the cursor.""" self._check_executed() if self.rownumber: - result = self._rows[self.rownumber:] + result = self._rows[self.rownumber :] else: result = self._rows self.rownumber = len(self._rows) return result - def scroll(self, value, mode='relative'): + def scroll(self, value, mode="relative"): """Scroll the cursor in the result set to a new position according to mode. @@ -406,9 +403,9 @@ def scroll(self, value, mode='relative'): the current position in the result set, if set to 'absolute', value states an absolute target position.""" self._check_executed() - if mode == 'relative': + if mode == "relative": r = self.rownumber + value - elif mode == 'absolute': + elif mode == "absolute": r = value else: raise ProgrammingError("unknown scroll mode %s" % repr(mode)) @@ -418,11 +415,11 @@ def scroll(self, value, mode='relative'): def __iter__(self): self._check_executed() - result = self.rownumber and self._rows[self.rownumber:] or self._rows + result = self.rownumber and self._rows[self.rownumber :] or self._rows return iter(result) -class CursorUseResultMixIn(object): +class CursorUseResultMixIn: """This is a MixIn class which causes the result set to be stored in the server and sent row-by-row to client side, i.e. it uses @@ -451,7 +448,7 @@ def fetchmany(self, size=None): return r def fetchall(self): - """Fetchs all available rows from the cursor.""" + """Fetches all available rows from the cursor.""" self._check_executed() r = self._fetch_row(0) self.rownumber = self.rownumber + len(r) @@ -469,39 +466,35 @@ def next(self): __next__ = next -class CursorTupleRowsMixIn(object): +class CursorTupleRowsMixIn: """This is a MixIn class that causes all rows to be returned as tuples, which is the standard form required by DB API.""" _fetch_type = 0 -class CursorDictRowsMixIn(object): +class CursorDictRowsMixIn: """This is a MixIn class that causes all rows to be returned as dictionaries. This is a non-standard feature.""" _fetch_type = 1 -class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn, - BaseCursor): +class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn, BaseCursor): """This is the standard Cursor class that returns rows as tuples and stores the result set in the client.""" -class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn, - BaseCursor): - """This is a Cursor class that returns rows as dictionaries and +class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn, BaseCursor): + """This is a Cursor class that returns rows as dictionaries and stores the result set in the client.""" -class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn, - BaseCursor): +class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn, BaseCursor): """This is a Cursor class that returns rows as tuples and stores the result set in the server.""" -class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn, - BaseCursor): +class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn, BaseCursor): """This is a Cursor class that returns rows as dictionaries and stores the result set in the server.""" diff --git a/src/MySQLdb/release.py b/src/MySQLdb/release.py new file mode 100644 index 00000000..234d9958 --- /dev/null +++ b/src/MySQLdb/release.py @@ -0,0 +1,3 @@ +__author__ = "Inada Naoki " +__version__ = "2.2.7" +version_info = (2, 2, 7, "final", 0) diff --git a/MySQLdb/times.py b/src/MySQLdb/times.py similarity index 61% rename from MySQLdb/times.py rename to src/MySQLdb/times.py index 510d1c7c..915d827b 100644 --- a/MySQLdb/times.py +++ b/src/MySQLdb/times.py @@ -16,34 +16,50 @@ DateTimeDeltaType = timedelta DateTimeType = datetime + def DateFromTicks(ticks): """Convert UNIX ticks into a date instance.""" return date(*localtime(ticks)[:3]) + def TimeFromTicks(ticks): """Convert UNIX ticks into a time instance.""" return time(*localtime(ticks)[3:6]) + def TimestampFromTicks(ticks): """Convert UNIX ticks into a datetime instance.""" return datetime(*localtime(ticks)[:6]) + format_TIME = format_DATE = str + def format_TIMEDELTA(v): seconds = int(v.seconds) % 60 minutes = int(v.seconds // 60) % 60 hours = int(v.seconds // 3600) % 24 - return '%d %d:%d:%d' % (v.days, hours, minutes, seconds) + return "%d %d:%d:%d" % (v.days, hours, minutes, seconds) + def format_TIMESTAMP(d): """ :type d: datetime.datetime """ if d.microsecond: - fmt = "{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}" + fmt = " ".join( + [ + "{0.year:04}-{0.month:02}-{0.day:02}", + "{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}", + ] + ) else: - fmt = "{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}" + fmt = " ".join( + [ + "{0.year:04}-{0.month:02}-{0.day:02}", + "{0.hour:02}:{0.minute:02}:{0.second:02}", + ] + ) return fmt.format(d) @@ -64,32 +80,32 @@ def DateTime_or_None(s): return None return datetime( - int(s[:4]), # year - int(s[5:7]), # month - int(s[8:10]), # day + int(s[:4]), # year + int(s[5:7]), # month + int(s[8:10]), # day int(s[11:13] or 0), # hour int(s[14:16] or 0), # minute int(s[17:19] or 0), # second - micros, # microsecond + micros, # microsecond ) except ValueError: return None + def TimeDelta_or_None(s): try: - h, m, s = s.split(':') - if '.' in s: - s, ms = s.split('.') - ms = ms.ljust(6, '0') + h, m, s = s.split(":") + if "." in s: + s, ms = s.split(".") + ms = ms.ljust(6, "0") else: ms = 0 - if h[0] == '-': + if h[0] == "-": negative = True else: negative = False h, m, s, ms = abs(int(h)), int(m), int(s), int(ms) - td = timedelta(hours=h, minutes=m, seconds=s, - microseconds=ms) + td = timedelta(hours=h, minutes=m, seconds=s, microseconds=ms) if negative: return -td else: @@ -98,48 +114,37 @@ def TimeDelta_or_None(s): # unpacking or int/float conversion failed return None + def Time_or_None(s): try: - h, m, s = s.split(':') - if '.' in s: - s, ms = s.split('.') - ms = ms.ljust(6, '0') + h, m, s = s.split(":") + if "." in s: + s, ms = s.split(".") + ms = ms.ljust(6, "0") else: ms = 0 h, m, s, ms = int(h), int(m), int(s), int(ms) - return time(hour=h, minute=m, second=s, - microsecond=ms) + return time(hour=h, minute=m, second=s, microsecond=ms) except ValueError: return None + def Date_or_None(s): try: return date( - int(s[:4]), # year - int(s[5:7]), # month - int(s[8:10]), # day - ) + int(s[:4]), + int(s[5:7]), + int(s[8:10]), + ) # year # month # day except ValueError: return None + def DateTime2literal(d, c): """Format a DateTime object as an ISO timestamp.""" - return string_literal(format_TIMESTAMP(d), c) + return string_literal(format_TIMESTAMP(d)) + def DateTimeDelta2literal(d, c): """Format a DateTimeDelta object as a time.""" - return string_literal(format_TIMEDELTA(d),c) - -def mysql_timestamp_converter(s): - """Convert a MySQL TIMESTAMP to a Timestamp object.""" - # MySQL>4.1 returns TIMESTAMP in the same format as DATETIME - if s[4] == '-': return DateTime_or_None(s) - s = s + "0"*(14-len(s)) # padding - parts = map(int, filter(None, (s[:4],s[4:6],s[6:8], - s[8:10],s[10:12],s[12:14]))) - try: - return Timestamp(*parts) - except (SystemExit, KeyboardInterrupt): - raise # pragma: no cover - except: - return None + return string_literal(format_TIMEDELTA(d)) diff --git a/tests/actions.cnf b/tests/actions.cnf new file mode 100644 index 00000000..d20296d6 --- /dev/null +++ b/tests/actions.cnf @@ -0,0 +1,11 @@ +# To create your own custom version of this file, read +# http://dev.mysql.com/doc/refman/5.1/en/option-files.html +# and set TESTDB in your environment to the name of the file + +[MySQLdb-tests] +host = 127.0.0.1 +port = 3306 +user = root +database = mysqldb_test +password = root +default-character-set = utf8mb4 diff --git a/tests/capabilities.py b/tests/capabilities.py index c341b3c9..1e695e9e 100644 --- a/tests/capabilities.py +++ b/tests/capabilities.py @@ -1,298 +1,314 @@ -#!/usr/bin/env python -O -""" Script to test database capabilities and the DB-API interface - for functionality and memory leaks. - - Adapted from a script by M-A Lemburg. - -""" -from time import time -import array -import unittest -from configdb import connection_factory - -from MySQLdb.compat import unichr - - -class DatabaseTest(unittest.TestCase): - - db_module = None - connect_args = () - connect_kwargs = dict() - create_table_extra = '' - rows = 10 - debug = False - - def setUp(self): - import gc - db = connection_factory(**self.connect_kwargs) - self.connection = db - self.cursor = db.cursor() - self.BLOBUText = u''.join([unichr(i) for i in range(16384)]) - self.BLOBBinary = self.db_module.Binary((u''.join([unichr(i) for i in range(256)] * 16)).encode('latin1')) - - leak_test = True - - def tearDown(self): - if self.leak_test: - import gc - del self.cursor - orphans = gc.collect() - self.failIf(orphans, "%d orphaned objects found after deleting cursor" % orphans) - - del self.connection - orphans = gc.collect() - self.failIf(orphans, "%d orphaned objects found after deleting connection" % orphans) - - def table_exists(self, name): - try: - self.cursor.execute('select * from %s where 1=0' % name) - except: - return False - else: - return True - - def quote_identifier(self, ident): - return '"%s"' % ident - - def new_table_name(self): - i = id(self.cursor) - while True: - name = self.quote_identifier('tb%08x' % i) - if not self.table_exists(name): - return name - i = i + 1 - - def create_table(self, columndefs): - - """ Create a table using a list of column definitions given in - columndefs. - - generator must be a function taking arguments (row_number, - col_number) returning a suitable data object for insertion - into the table. - - """ - self.table = self.new_table_name() - self.cursor.execute('CREATE TABLE %s (%s) %s' % - (self.table, - ',\n'.join(columndefs), - self.create_table_extra)) - - def check_data_integrity(self, columndefs, generator): - # insert - self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] - self.cursor.executemany(insert_statement, data) - self.connection.commit() - # verify - self.cursor.execute('select * from %s' % self.table) - l = self.cursor.fetchall() - self.assertEqual(len(l), self.rows) - try: - for i in range(self.rows): - for j in range(len(columndefs)): - self.assertEqual(l[i][j], generator(i,j)) - finally: - if not self.debug: - self.cursor.execute('drop table %s' % (self.table)) - - def test_transactions(self): - columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') - def generator(row, col): - if col == 0: return row - else: return ('%i' % (row%10))*255 - self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] - self.cursor.executemany(insert_statement, data) - # verify - self.connection.commit() - self.cursor.execute('select * from %s' % self.table) - l = self.cursor.fetchall() - self.assertEqual(len(l), self.rows) - for i in range(self.rows): - for j in range(len(columndefs)): - self.assertEqual(l[i][j], generator(i,j)) - delete_statement = 'delete from %s where col1=%%s' % self.table - self.cursor.execute(delete_statement, (0,)) - self.cursor.execute('select col1 from %s where col1=%s' % \ - (self.table, 0)) - l = self.cursor.fetchall() - self.assertFalse(l, "DELETE didn't work") - self.connection.rollback() - self.cursor.execute('select col1 from %s where col1=%s' % \ - (self.table, 0)) - l = self.cursor.fetchall() - self.assertTrue(len(l) == 1, "ROLLBACK didn't work") - self.cursor.execute('drop table %s' % (self.table)) - - def test_truncation(self): - columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') - def generator(row, col): - if col == 0: return row - else: return ('%i' % (row%10))*((255-self.rows//2)+row) - self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) - - try: - self.cursor.execute(insert_statement, (0, '0'*256)) - except self.connection.DataError: - pass - else: - self.fail("Over-long column did not generate warnings/exception with single insert") - - self.connection.rollback() - - try: - for i in range(self.rows): - data = [] - for j in range(len(columndefs)): - data.append(generator(i,j)) - self.cursor.execute(insert_statement,tuple(data)) - except self.connection.DataError: - pass - else: - self.fail("Over-long columns did not generate warnings/exception with execute()") - - self.connection.rollback() - - try: - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] - self.cursor.executemany(insert_statement, data) - except self.connection.DataError: - pass - else: - self.fail("Over-long columns did not generate warnings/exception with executemany()") - - self.connection.rollback() - self.cursor.execute('drop table %s' % (self.table)) - - def test_CHAR(self): - # Character data - def generator(row,col): - return ('%i' % ((row+col) % 10)) * 255 - self.check_data_integrity( - ('col1 char(255)','col2 char(255)'), - generator) - - def test_INT(self): - # Number data - def generator(row,col): - return row*row - self.check_data_integrity( - ('col1 INT',), - generator) - - def test_DECIMAL(self): - # DECIMAL - from decimal import Decimal - def generator(row,col): - return Decimal("%d.%02d" % (row, col)) - self.check_data_integrity( - ('col1 DECIMAL(5,2)',), - generator) - - val = Decimal('1.11111111111111119E-7') - self.cursor.execute('SELECT %s', (val,)) - result = self.cursor.fetchone()[0] - self.assertEqual(result, val) - self.assertIsInstance(result, Decimal) - - self.cursor.execute('SELECT %s + %s', (Decimal('0.1'), Decimal('0.2'))) - result = self.cursor.fetchone()[0] - self.assertEqual(result, Decimal('0.3')) - self.assertIsInstance(result, Decimal) - - def test_DATE(self): - ticks = time() - def generator(row,col): - return self.db_module.DateFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 DATE',), - generator) - - def test_TIME(self): - ticks = time() - def generator(row,col): - return self.db_module.TimeFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 TIME',), - generator) - - def test_DATETIME(self): - ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 DATETIME',), - generator) - - def test_TIMESTAMP(self): - ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 TIMESTAMP',), - generator) - - def test_fractional_TIMESTAMP(self): - ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313+row*0.7*col/3.0) - self.check_data_integrity( - ('col1 TIMESTAMP',), - generator) - - def test_LONG(self): - def generator(row,col): - if col == 0: - return row - else: - return self.BLOBUText # 'BLOB Text ' * 1024 - self.check_data_integrity( - ('col1 INT','col2 LONG'), - generator) - - def test_TEXT(self): - def generator(row,col): - return self.BLOBUText # 'BLOB Text ' * 1024 - self.check_data_integrity( - ('col2 TEXT',), - generator) - - def test_LONG_BYTE(self): - def generator(row,col): - if col == 0: - return row - else: - return self.BLOBBinary # 'BLOB\000Binary ' * 1024 - self.check_data_integrity( - ('col1 INT','col2 LONG BYTE'), - generator) - - def test_BLOB(self): - def generator(row,col): - if col == 0: - return row - else: - return self.BLOBBinary # 'BLOB\000Binary ' * 1024 - self.check_data_integrity( - ('col1 INT','col2 BLOB'), - generator) - - def test_DOUBLE(self): - for val in (18014398509481982.0, 0.1): - self.cursor.execute('SELECT %s', (val,)); - result = self.cursor.fetchone()[0] - self.assertEqual(result, val) - self.assertIsInstance(result, float) +#!/usr/bin/env python -O +""" Script to test database capabilities and the DB-API interface + for functionality and memory leaks. + + Adapted from a script by M-A Lemburg. + +""" +from time import time +import unittest +from configdb import connection_factory + + +class DatabaseTest(unittest.TestCase): + db_module = None + connect_args = () + connect_kwargs = dict() + create_table_extra = "" + rows = 10 + debug = False + + def setUp(self): + db = connection_factory(**self.connect_kwargs) + self.connection = db + self.cursor = db.cursor() + self.BLOBUText = "".join([chr(i) for i in range(16384)]) + self.BLOBBinary = self.db_module.Binary( + ("".join([chr(i) for i in range(256)] * 16)).encode("latin1") + ) + + leak_test = True + + def tearDown(self): + if self.leak_test: + import gc + + del self.cursor + orphans = gc.collect() + self.assertFalse( + orphans, "%d orphaned objects found after deleting cursor" % orphans + ) + + del self.connection + orphans = gc.collect() + self.assertFalse( + orphans, "%d orphaned objects found after deleting connection" % orphans + ) + + def table_exists(self, name): + try: + self.cursor.execute("select * from %s where 1=0" % name) + except Exception: + return False + else: + return True + + def quote_identifier(self, ident): + return '"%s"' % ident + + def new_table_name(self): + i = id(self.cursor) + while True: + name = self.quote_identifier("tb%08x" % i) + if not self.table_exists(name): + return name + i = i + 1 + + def create_table(self, columndefs): + """Create a table using a list of column definitions given in + columndefs. + + generator must be a function taking arguments (row_number, + col_number) returning a suitable data object for insertion + into the table. + + """ + self.table = self.new_table_name() + self.cursor.execute( + "CREATE TABLE %s (%s) %s" + % (self.table, ",\n".join(columndefs), self.create_table_extra) + ) + + def check_data_integrity(self, columndefs, generator): + # insert + self.create_table(columndefs) + insert_statement = "INSERT INTO {} VALUES ({})".format( + self.table, + ",".join(["%s"] * len(columndefs)), + ) + data = [ + [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) + ] + self.cursor.executemany(insert_statement, data) + self.connection.commit() + # verify + self.cursor.execute("select * from %s" % self.table) + res = self.cursor.fetchall() + self.assertEqual(len(res), self.rows) + try: + for i in range(self.rows): + for j in range(len(columndefs)): + self.assertEqual(res[i][j], generator(i, j)) + finally: + if not self.debug: + self.cursor.execute("drop table %s" % (self.table)) + + def test_transactions(self): + columndefs = ("col1 INT", "col2 VARCHAR(255)") + + def generator(row, col): + if col == 0: + return row + else: + return ("%i" % (row % 10)) * 255 + + self.create_table(columndefs) + insert_statement = "INSERT INTO {} VALUES ({})".format( + self.table, + ",".join(["%s"] * len(columndefs)), + ) + data = [ + [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) + ] + self.cursor.executemany(insert_statement, data) + # verify + self.connection.commit() + self.cursor.execute("select * from %s" % self.table) + res = self.cursor.fetchall() + self.assertEqual(len(res), self.rows) + for i in range(self.rows): + for j in range(len(columndefs)): + self.assertEqual(res[i][j], generator(i, j)) + delete_statement = "delete from %s where col1=%%s" % self.table + self.cursor.execute(delete_statement, (0,)) + self.cursor.execute(f"select col1 from {self.table} where col1=%s", (0,)) + res = self.cursor.fetchall() + self.assertFalse(res, "DELETE didn't work") + self.connection.rollback() + self.cursor.execute(f"select col1 from {self.table} where col1=%s", (0,)) + res = self.cursor.fetchall() + self.assertTrue(len(res) == 1, "ROLLBACK didn't work") + self.cursor.execute("drop table %s" % (self.table)) + + def test_truncation(self): + columndefs = ("col1 INT", "col2 VARCHAR(255)") + + def generator(row, col): + if col == 0: + return row + else: + return ("%i" % (row % 10)) * ((255 - self.rows // 2) + row) + + self.create_table(columndefs) + insert_statement = "INSERT INTO {} VALUES ({})".format( + self.table, + ",".join(["%s"] * len(columndefs)), + ) + + try: + self.cursor.execute(insert_statement, (0, "0" * 256)) + except self.connection.DataError: + pass + else: + self.fail( + "Over-long column did not generate warnings/exception with single insert" # noqa: E501 + ) + + self.connection.rollback() + + try: + for i in range(self.rows): + data = [] + for j in range(len(columndefs)): + data.append(generator(i, j)) + self.cursor.execute(insert_statement, tuple(data)) + except self.connection.DataError: + pass + else: + self.fail( + "Over-long columns did not generate warnings/exception with execute()" # noqa: E501 + ) + + self.connection.rollback() + + try: + data = [ + [generator(i, j) for j in range(len(columndefs))] + for i in range(self.rows) + ] + self.cursor.executemany(insert_statement, data) + except self.connection.DataError: + pass + else: + self.fail( + "Over-long columns did not generate warnings/exception with executemany()" # noqa: E501 + ) + + self.connection.rollback() + self.cursor.execute("drop table %s" % (self.table)) + + def test_CHAR(self): + # Character data + def generator(row, col): + return ("%i" % ((row + col) % 10)) * 255 + + self.check_data_integrity(("col1 char(255)", "col2 char(255)"), generator) + + def test_INT(self): + # Number data + def generator(row, col): + return row * row + + self.check_data_integrity(("col1 INT",), generator) + + def test_DECIMAL(self): + # DECIMAL + from decimal import Decimal + + def generator(row, col): + return Decimal("%d.%02d" % (row, col)) + + self.check_data_integrity(("col1 DECIMAL(5,2)",), generator) + + val = Decimal("1.11111111111111119E-7") + self.cursor.execute("SELECT %s", (val,)) + result = self.cursor.fetchone()[0] + self.assertEqual(result, val) + self.assertIsInstance(result, Decimal) + + self.cursor.execute("SELECT %s + %s", (Decimal("0.1"), Decimal("0.2"))) + result = self.cursor.fetchone()[0] + self.assertEqual(result, Decimal("0.3")) + self.assertIsInstance(result, Decimal) + + def test_DATE(self): + ticks = time() + + def generator(row, col): + return self.db_module.DateFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 DATE",), generator) + + def test_TIME(self): + ticks = time() + + def generator(row, col): + return self.db_module.TimeFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 TIME",), generator) + + def test_DATETIME(self): + ticks = time() + + def generator(row, col): + return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 DATETIME",), generator) + + def test_TIMESTAMP(self): + ticks = time() + + def generator(row, col): + return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 TIMESTAMP",), generator) + + def test_fractional_TIMESTAMP(self): + ticks = time() + + def generator(row, col): + return self.db_module.TimestampFromTicks( + ticks + row * 86400 - col * 1313 + row * 0.7 * col / 3.0 + ) + + self.check_data_integrity(("col1 TIMESTAMP",), generator) + + def test_LONG(self): + def generator(row, col): + if col == 0: + return row + else: + return self.BLOBUText # 'BLOB Text ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 LONG"), generator) + + def test_TEXT(self): + def generator(row, col): + return self.BLOBUText # 'BLOB Text ' * 1024 + + self.check_data_integrity(("col2 TEXT",), generator) + + def test_LONG_BYTE(self): + def generator(row, col): + if col == 0: + return row + else: + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 LONG BYTE"), generator) + + def test_BLOB(self): + def generator(row, col): + if col == 0: + return row + else: + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 BLOB"), generator) + + def test_DOUBLE(self): + for val in (18014398509481982.0, 0.1): + self.cursor.execute("SELECT %s", (val,)) + result = self.cursor.fetchone()[0] + self.assertEqual(result, val) + self.assertIsInstance(result, float) diff --git a/tests/configdb.py b/tests/configdb.py index 307cc3f4..c2949039 100644 --- a/tests/configdb.py +++ b/tests/configdb.py @@ -3,11 +3,11 @@ from os import environ, path tests_path = path.dirname(__file__) -conf_file = environ.get('TESTDB', 'default.cnf') +conf_file = environ.get("TESTDB", "default.cnf") conf_path = path.join(tests_path, conf_file) connect_kwargs = dict( - read_default_file = conf_path, - read_default_group = "MySQLdb-tests", + read_default_file=conf_path, + read_default_group="MySQLdb-tests", ) @@ -19,6 +19,7 @@ def connection_kwargs(kwargs): def connection_factory(**kwargs): import MySQLdb + db_kwargs = connection_kwargs(kwargs) db = MySQLdb.connect(**db_kwargs) return db diff --git a/tests/dbapi20.py b/tests/dbapi20.py index e28d5d1f..be0f6292 100644 --- a/tests/dbapi20.py +++ b/tests/dbapi20.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -''' Python DB API 2.0 driver compliance unit test suite. - +""" Python DB API 2.0 driver compliance unit test suite. + This software is Public Domain and may be used without restrictions. "Now we have booze and barflies entering the discussion, plus rumours of @@ -9,11 +9,11 @@ this is turning out to be a thoroughly unwholesome unit test." -- Ian Bicking -''' +""" -__rcs_id__ = '$Id$' -__version__ = '$Revision$'[11:-2] -__author__ = 'Stuart Bishop ' +__rcs_id__ = "$Id$" +__version__ = "$Revision$"[11:-2] +__author__ = "Stuart Bishop " import unittest import time @@ -56,7 +56,7 @@ # - self.populate is now self._populate(), so if a driver stub # overrides self.ddl1 this change propagates # - VARCHAR columns now have a width, which will hopefully make the -# DDL even more portible (this will be reversed if it causes more problems) +# DDL even more portable (this will be reversed if it causes more problems) # - cursor.rowcount being checked after various execute and fetchXXX methods # - Check for fetchall and fetchmany returning empty lists after results # are exhausted (already checking for empty lists if select retrieved @@ -64,69 +64,70 @@ # - Fix bugs in test_setoutputsize_basic and test_setinputsizes # + class DatabaseAPI20Test(unittest.TestCase): - ''' Test a database self.driver for DB API 2.0 compatibility. - This implementation tests Gadfly, but the TestCase - is structured so that other self.drivers can subclass this - test case to ensure compiliance with the DB-API. It is - expected that this TestCase may be expanded in the future - if ambiguities or edge conditions are discovered. + """Test a database self.driver for DB API 2.0 compatibility. + This implementation tests Gadfly, but the TestCase + is structured so that other self.drivers can subclass this + test case to ensure compiliance with the DB-API. It is + expected that this TestCase may be expanded in the future + if ambiguities or edge conditions are discovered. - The 'Optional Extensions' are not yet being tested. + The 'Optional Extensions' are not yet being tested. - self.drivers should subclass this test, overriding setUp, tearDown, - self.driver, connect_args and connect_kw_args. Class specification - should be as follows: + self.drivers should subclass this test, overriding setUp, tearDown, + self.driver, connect_args and connect_kw_args. Class specification + should be as follows: - import dbapi20 - class mytest(dbapi20.DatabaseAPI20Test): - [...] + import dbapi20 + class mytest(dbapi20.DatabaseAPI20Test): + [...] - Don't 'import DatabaseAPI20Test from dbapi20', or you will - confuse the unit tester - just 'import dbapi20'. - ''' + Don't 'import DatabaseAPI20Test from dbapi20', or you will + confuse the unit tester - just 'import dbapi20'. + """ # The self.driver module. This should be the module where the 'connect' # method is to be found driver = None - connect_args = () # List of arguments to pass to connect - connect_kw_args = {} # Keyword arguments for connect - table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables + connect_args = () # List of arguments to pass to connect + connect_kw_args = {} # Keyword arguments for connect + table_prefix = "dbapi20test_" # If you need to specify a prefix for tables + + ddl1 = "create table %sbooze (name varchar(20))" % table_prefix + ddl2 = "create table %sbarflys (name varchar(20))" % table_prefix + xddl1 = "drop table %sbooze" % table_prefix + xddl2 = "drop table %sbarflys" % table_prefix - ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix - ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix - xddl1 = 'drop table %sbooze' % table_prefix - xddl2 = 'drop table %sbarflys' % table_prefix + lowerfunc = "lower" # Name of stored procedure to convert string->lowercase - lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase - # Some drivers may need to override these helpers, for example adding # a 'commit' after the execute. - def executeDDL1(self,cursor): + def executeDDL1(self, cursor): cursor.execute(self.ddl1) - def executeDDL2(self,cursor): + def executeDDL2(self, cursor): cursor.execute(self.ddl2) def setUp(self): - ''' self.drivers should override this method to perform required setup - if any is necessary, such as creating the database. - ''' + """self.drivers should override this method to perform required setup + if any is necessary, such as creating the database. + """ pass def tearDown(self): - ''' self.drivers should override this method to perform required cleanup - if any is necessary, such as deleting the test database. - The default drops the tables that may be created. - ''' + """self.drivers should override this method to perform required cleanup + if any is necessary, such as deleting the test database. + The default drops the tables that may be created. + """ con = self._connect() try: cur = con.cursor() - for ddl in (self.xddl1,self.xddl2): - try: + for ddl in (self.xddl1, self.xddl2): + try: cur.execute(ddl) con.commit() - except self.driver.Error: + except self.driver.Error: # Assume table didn't exist. Other tests will check if # execute is busted. pass @@ -135,9 +136,7 @@ def tearDown(self): def _connect(self): try: - return self.driver.connect( - *self.connect_args,**self.connect_kw_args - ) + return self.driver.connect(*self.connect_args, **self.connect_kw_args) except AttributeError: self.fail("No connect method found in self.driver module") @@ -150,7 +149,7 @@ def test_apilevel(self): # Must exist apilevel = self.driver.apilevel # Must equal 2.0 - self.assertEqual(apilevel,'2.0') + self.assertEqual(apilevel, "2.0") except AttributeError: self.fail("Driver doesn't define apilevel") @@ -159,7 +158,7 @@ def test_threadsafety(self): # Must exist threadsafety = self.driver.threadsafety # Must be a valid value - self.assertTrue(threadsafety in (0,1,2,3)) + self.assertTrue(threadsafety in (0, 1, 2, 3)) except AttributeError: self.fail("Driver doesn't define threadsafety") @@ -168,38 +167,24 @@ def test_paramstyle(self): # Must exist paramstyle = self.driver.paramstyle # Must be a valid value - self.assertTrue(paramstyle in ( - 'qmark','numeric','named','format','pyformat' - )) + self.assertTrue( + paramstyle in ("qmark", "numeric", "named", "format", "pyformat") + ) except AttributeError: self.fail("Driver doesn't define paramstyle") def test_Exceptions(self): # Make sure required exceptions exist, and are in the # defined hierarchy. - self.assertTrue(issubclass(self.driver.Warning,Exception)) - self.assertTrue(issubclass(self.driver.Error,Exception)) - self.assertTrue( - issubclass(self.driver.InterfaceError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.DatabaseError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.OperationalError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.IntegrityError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.InternalError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.ProgrammingError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.NotSupportedError,self.driver.Error) - ) + self.assertTrue(issubclass(self.driver.Warning, Exception)) + self.assertTrue(issubclass(self.driver.Error, Exception)) + self.assertTrue(issubclass(self.driver.InterfaceError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.DatabaseError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.OperationalError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.IntegrityError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.InternalError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.ProgrammingError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.NotSupportedError, self.driver.Error)) def test_ExceptionsAsConnectionAttributes(self): # OPTIONAL EXTENSION @@ -220,7 +205,6 @@ def test_ExceptionsAsConnectionAttributes(self): self.assertTrue(con.ProgrammingError is drv.ProgrammingError) self.assertTrue(con.NotSupportedError is drv.NotSupportedError) - def test_commit(self): con = self._connect() try: @@ -233,16 +217,16 @@ def test_rollback(self): con = self._connect() # If rollback is defined, it should either work or throw # the documented exception - if hasattr(con,'rollback'): + if hasattr(con, "rollback"): try: con.rollback() except self.driver.NotSupportedError: pass - + def test_cursor(self): con = self._connect() try: - cur = con.cursor() + _ = con.cursor() finally: con.close() @@ -254,14 +238,14 @@ def test_cursor_isolation(self): cur1 = con.cursor() cur2 = con.cursor() self.executeDDL1(cur1) - cur1.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) + cur1.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) cur2.execute("select name from %sbooze" % self.table_prefix) booze = cur2.fetchall() - self.assertEqual(len(booze),1) - self.assertEqual(len(booze[0]),1) - self.assertEqual(booze[0][0],'Victoria Bitter') + self.assertEqual(len(booze), 1) + self.assertEqual(len(booze[0]), 1) + self.assertEqual(booze[0][0], "Victoria Bitter") finally: con.close() @@ -270,31 +254,41 @@ def test_description(self): try: cur = con.cursor() self.executeDDL1(cur) - self.assertEqual(cur.description,None, - 'cursor.description should be none after executing a ' - 'statement that can return no rows (such as DDL)' - ) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(len(cur.description),1, - 'cursor.description describes too many columns' - ) - self.assertEqual(len(cur.description[0]),7, - 'cursor.description[x] tuples must have 7 elements' - ) - self.assertEqual(cur.description[0][0].lower(),'name', - 'cursor.description[x][0] must return column name' - ) - self.assertEqual(cur.description[0][1],self.driver.STRING, - 'cursor.description[x][1] must return column type. Got %r' - % cur.description[0][1] - ) + self.assertEqual( + cur.description, + None, + "cursor.description should be none after executing a " + "statement that can return no rows (such as DDL)", + ) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + len(cur.description), 1, "cursor.description describes too many columns" + ) + self.assertEqual( + len(cur.description[0]), + 7, + "cursor.description[x] tuples must have 7 elements", + ) + self.assertEqual( + cur.description[0][0].lower(), + "name", + "cursor.description[x][0] must return column name", + ) + self.assertEqual( + cur.description[0][1], + self.driver.STRING, + "cursor.description[x][1] must return column type. Got %r" + % cur.description[0][1], + ) # Make sure self.description gets reset self.executeDDL2(cur) - self.assertEqual(cur.description,None, - 'cursor.description not being set to None when executing ' - 'no-result statements (eg. DDL)' - ) + self.assertEqual( + cur.description, + None, + "cursor.description not being set to None when executing " + "no-result statements (eg. DDL)", + ) finally: con.close() @@ -303,47 +297,49 @@ def test_rowcount(self): try: cur = con.cursor() self.executeDDL1(cur) - self.assertEqual(cur.rowcount,-1, - 'cursor.rowcount should be -1 after executing no-result ' - 'statements' - ) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number or rows inserted, or ' - 'set to -1 after executing an insert statement' - ) + self.assertEqual( + cur.rowcount, + -1, + "cursor.rowcount should be -1 after executing no-result " "statements", + ) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number or rows inserted, or " + "set to -1 after executing an insert statement", + ) cur.execute("select name from %sbooze" % self.table_prefix) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number of rows returned, or ' - 'set to -1 after executing a select statement' - ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number of rows returned, or " + "set to -1 after executing a select statement", + ) self.executeDDL2(cur) - self.assertEqual(cur.rowcount,-1, - 'cursor.rowcount not being reset to -1 after executing ' - 'no-result statements' - ) + self.assertEqual( + cur.rowcount, + -1, + "cursor.rowcount not being reset to -1 after executing " + "no-result statements", + ) finally: con.close() - lower_func = 'lower' + lower_func = "lower" + def test_callproc(self): con = self._connect() try: cur = con.cursor() - if self.lower_func and hasattr(cur,'callproc'): - r = cur.callproc(self.lower_func,('FOO',)) - self.assertEqual(len(r),1) - self.assertEqual(r[0],'FOO') + if self.lower_func and hasattr(cur, "callproc"): + r = cur.callproc(self.lower_func, ("FOO",)) + self.assertEqual(len(r), 1) + self.assertEqual(r[0], "FOO") r = cur.fetchall() - self.assertEqual(len(r),1,'callproc produced no result set') - self.assertEqual(len(r[0]),1, - 'callproc produced invalid result set' - ) - self.assertEqual(r[0][0],'foo', - 'callproc produced invalid results' - ) + self.assertEqual(len(r), 1, "callproc produced no result set") + self.assertEqual(len(r[0]), 1, "callproc produced invalid result set") + self.assertEqual(r[0][0], "foo", "callproc produced invalid results") finally: con.close() @@ -356,14 +352,14 @@ def test_close(self): # cursor.execute should raise an Error if called after connection # closed - self.assertRaises(self.driver.Error,self.executeDDL1,cur) + self.assertRaises(self.driver.Error, self.executeDDL1, cur) # connection.commit should raise an Error if called after connection' # closed.' - self.assertRaises(self.driver.Error,con.commit) + self.assertRaises(self.driver.Error, con.commit) # connection.close should raise an Error if called more than once - self.assertRaises(self.driver.Error,con.close) + self.assertRaises(self.driver.Error, con.close) def test_execute(self): con = self._connect() @@ -373,105 +369,99 @@ def test_execute(self): finally: con.close() - def _paraminsert(self,cur): + def _paraminsert(self, cur): self.executeDDL1(cur) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertTrue(cur.rowcount in (-1,1)) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertTrue(cur.rowcount in (-1, 1)) - if self.driver.paramstyle == 'qmark': + if self.driver.paramstyle == "qmark": cur.execute( - 'insert into %sbooze values (?)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'numeric': + "insert into %sbooze values (?)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "numeric": cur.execute( - 'insert into %sbooze values (:1)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'named': + "insert into %sbooze values (:1)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "named": cur.execute( - 'insert into %sbooze values (:beer)' % self.table_prefix, - {'beer':"Cooper's"} - ) - elif self.driver.paramstyle == 'format': + "insert into %sbooze values (:beer)" % self.table_prefix, + {"beer": "Cooper's"}, + ) + elif self.driver.paramstyle == "format": cur.execute( - 'insert into %sbooze values (%%s)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'pyformat': + "insert into %sbooze values (%%s)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "pyformat": cur.execute( - 'insert into %sbooze values (%%(beer)s)' % self.table_prefix, - {'beer':"Cooper's"} - ) + "insert into %sbooze values (%%(beer)s)" % self.table_prefix, + {"beer": "Cooper's"}, + ) else: - self.fail('Invalid paramstyle') - self.assertTrue(cur.rowcount in (-1,1)) + self.fail("Invalid paramstyle") + self.assertTrue(cur.rowcount in (-1, 1)) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) res = cur.fetchall() - self.assertEqual(len(res),2,'cursor.fetchall returned too few rows') - beers = [res[0][0],res[1][0]] + self.assertEqual(len(res), 2, "cursor.fetchall returned too few rows") + beers = [res[0][0], res[1][0]] beers.sort() - self.assertEqual(beers[0],"Cooper's", - 'cursor.fetchall retrieved incorrect data, or data inserted ' - 'incorrectly' - ) - self.assertEqual(beers[1],"Victoria Bitter", - 'cursor.fetchall retrieved incorrect data, or data inserted ' - 'incorrectly' - ) + self.assertEqual( + beers[0], + "Cooper's", + "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly", + ) + self.assertEqual( + beers[1], + "Victoria Bitter", + "cursor.fetchall retrieved incorrect data, or data inserted " "incorrectly", + ) def test_executemany(self): con = self._connect() try: cur = con.cursor() self.executeDDL1(cur) - largs = [ ("Cooper's",) , ("Boag's",) ] - margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ] - if self.driver.paramstyle == 'qmark': + largs = [("Cooper's",), ("Boag's",)] + margs = [{"beer": "Cooper's"}, {"beer": "Boag's"}] + if self.driver.paramstyle == "qmark": cur.executemany( - 'insert into %sbooze values (?)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'numeric': + "insert into %sbooze values (?)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "numeric": cur.executemany( - 'insert into %sbooze values (:1)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'named': + "insert into %sbooze values (:1)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "named": cur.executemany( - 'insert into %sbooze values (:beer)' % self.table_prefix, - margs - ) - elif self.driver.paramstyle == 'format': + "insert into %sbooze values (:beer)" % self.table_prefix, margs + ) + elif self.driver.paramstyle == "format": cur.executemany( - 'insert into %sbooze values (%%s)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'pyformat': + "insert into %sbooze values (%%s)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "pyformat": cur.executemany( - 'insert into %sbooze values (%%(beer)s)' % ( - self.table_prefix - ), - margs - ) - else: - self.fail('Unknown paramstyle') - self.assertTrue(cur.rowcount in (-1,2), - 'insert using cursor.executemany set cursor.rowcount to ' - 'incorrect value %r' % cur.rowcount + "insert into %sbooze values (%%(beer)s)" % (self.table_prefix), + margs, ) - cur.execute('select name from %sbooze' % self.table_prefix) + else: + self.fail("Unknown paramstyle") + self.assertTrue( + cur.rowcount in (-1, 2), + "insert using cursor.executemany set cursor.rowcount to " + "incorrect value %r" % cur.rowcount, + ) + cur.execute("select name from %sbooze" % self.table_prefix) res = cur.fetchall() - self.assertEqual(len(res),2, - 'cursor.fetchall retrieved incorrect number of rows' - ) - beers = [res[0][0],res[1][0]] + self.assertEqual( + len(res), 2, "cursor.fetchall retrieved incorrect number of rows" + ) + beers = [res[0][0], res[1][0]] beers.sort() - self.assertEqual(beers[0],"Boag's",'incorrect data retrieved') - self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved') + self.assertEqual(beers[0], "Boag's", "incorrect data retrieved") + self.assertEqual(beers[1], "Cooper's", "incorrect data retrieved") finally: con.close() @@ -482,59 +472,61 @@ def test_fetchone(self): # cursor.fetchone should raise an Error if called before # executing a select-type query - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) # cursor.fetchone should raise an Error if called after # executing a query that cannot return rows self.executeDDL1(cur) - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if a query retrieves ' - 'no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if a query retrieves " "no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) # cursor.fetchone should raise an Error if called after # executing a query that cannot return rows - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertRaises(self.driver.Error,cur.fetchone) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertRaises(self.driver.Error, cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchone() - self.assertEqual(len(r),1, - 'cursor.fetchone should have retrieved a single row' - ) - self.assertEqual(r[0],'Victoria Bitter', - 'cursor.fetchone retrieved incorrect data' - ) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if no more rows available' - ) - self.assertTrue(cur.rowcount in (-1,1)) + self.assertEqual( + len(r), 1, "cursor.fetchone should have retrieved a single row" + ) + self.assertEqual( + r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data" + ) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if no more rows available", + ) + self.assertTrue(cur.rowcount in (-1, 1)) finally: con.close() samples = [ - 'Carlton Cold', - 'Carlton Draft', - 'Mountain Goat', - 'Redback', - 'Victoria Bitter', - 'XXXX' - ] + "Carlton Cold", + "Carlton Draft", + "Mountain Goat", + "Redback", + "Victoria Bitter", + "XXXX", + ] def _populate(self): - ''' Return a list of sql commands to setup the DB for the fetch - tests. - ''' + """Return a list of sql commands to setup the DB for the fetch + tests. + """ populate = [ - "insert into %sbooze values ('%s')" % (self.table_prefix,s) - for s in self.samples - ] + f"insert into {self.table_prefix}booze values ('{s}')" for s in self.samples + ] return populate def test_fetchmany(self): @@ -543,78 +535,88 @@ def test_fetchmany(self): cur = con.cursor() # cursor.fetchmany should raise an Error if called without - #issuing a query - self.assertRaises(self.driver.Error,cur.fetchmany,4) + # issuing a query + self.assertRaises(self.driver.Error, cur.fetchmany, 4) self.executeDDL1(cur) for sql in self._populate(): cur.execute(sql) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchmany() - self.assertEqual(len(r),1, - 'cursor.fetchmany retrieved incorrect number of rows, ' - 'default of arraysize is one.' - ) - cur.arraysize=10 - r = cur.fetchmany(3) # Should get 3 rows - self.assertEqual(len(r),3, - 'cursor.fetchmany retrieved incorrect number of rows' - ) - r = cur.fetchmany(4) # Should get 2 more - self.assertEqual(len(r),2, - 'cursor.fetchmany retrieved incorrect number of rows' - ) - r = cur.fetchmany(4) # Should be an empty sequence - self.assertEqual(len(r),0, - 'cursor.fetchmany should return an empty sequence after ' - 'results are exhausted' + self.assertEqual( + len(r), + 1, + "cursor.fetchmany retrieved incorrect number of rows, " + "default of arraysize is one.", + ) + cur.arraysize = 10 + r = cur.fetchmany(3) # Should get 3 rows + self.assertEqual( + len(r), 3, "cursor.fetchmany retrieved incorrect number of rows" + ) + r = cur.fetchmany(4) # Should get 2 more + self.assertEqual( + len(r), 2, "cursor.fetchmany retrieved incorrect number of rows" + ) + r = cur.fetchmany(4) # Should be an empty sequence + self.assertEqual( + len(r), + 0, + "cursor.fetchmany should return an empty sequence after " + "results are exhausted", ) - self.assertTrue(cur.rowcount in (-1,6)) + self.assertTrue(cur.rowcount in (-1, 6)) # Same as above, using cursor.arraysize - cur.arraysize=4 - cur.execute('select name from %sbooze' % self.table_prefix) - r = cur.fetchmany() # Should get 4 rows - self.assertEqual(len(r),4, - 'cursor.arraysize not being honoured by fetchmany' - ) - r = cur.fetchmany() # Should get 2 more - self.assertEqual(len(r),2) - r = cur.fetchmany() # Should be an empty sequence - self.assertEqual(len(r),0) - self.assertTrue(cur.rowcount in (-1,6)) - - cur.arraysize=6 - cur.execute('select name from %sbooze' % self.table_prefix) - rows = cur.fetchmany() # Should get all rows - self.assertTrue(cur.rowcount in (-1,6)) - self.assertEqual(len(rows),6) - self.assertEqual(len(rows),6) + cur.arraysize = 4 + cur.execute("select name from %sbooze" % self.table_prefix) + r = cur.fetchmany() # Should get 4 rows + self.assertEqual( + len(r), 4, "cursor.arraysize not being honoured by fetchmany" + ) + r = cur.fetchmany() # Should get 2 more + self.assertEqual(len(r), 2) + r = cur.fetchmany() # Should be an empty sequence + self.assertEqual(len(r), 0) + self.assertTrue(cur.rowcount in (-1, 6)) + + cur.arraysize = 6 + cur.execute("select name from %sbooze" % self.table_prefix) + rows = cur.fetchmany() # Should get all rows + self.assertTrue(cur.rowcount in (-1, 6)) + self.assertEqual(len(rows), 6) + self.assertEqual(len(rows), 6) rows = [r[0] for r in rows] rows.sort() - + # Make sure we get the right data back out - for i in range(0,6): - self.assertEqual(rows[i],self.samples[i], - 'incorrect data retrieved by cursor.fetchmany' - ) - - rows = cur.fetchmany() # Should return an empty list - self.assertEqual(len(rows),0, - 'cursor.fetchmany should return an empty sequence if ' - 'called after the whole result set has been fetched' + for i in range(0, 6): + self.assertEqual( + rows[i], + self.samples[i], + "incorrect data retrieved by cursor.fetchmany", ) - self.assertTrue(cur.rowcount in (-1,6)) + + rows = cur.fetchmany() # Should return an empty list + self.assertEqual( + len(rows), + 0, + "cursor.fetchmany should return an empty sequence if " + "called after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, 6)) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) - r = cur.fetchmany() # Should get empty sequence - self.assertEqual(len(r),0, - 'cursor.fetchmany should return an empty sequence if ' - 'query retrieved no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbarflys" % self.table_prefix) + r = cur.fetchmany() # Should get empty sequence + self.assertEqual( + len(r), + 0, + "cursor.fetchmany should return an empty sequence if " + "query retrieved no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) finally: con.close() @@ -634,40 +636,45 @@ def test_fetchall(self): # cursor.fetchall should raise an Error if called # after executing a statement that cannot return rows - self.assertRaises(self.driver.Error,cur.fetchall) + self.assertRaises(self.driver.Error, cur.fetchall) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,len(self.samples))) - self.assertEqual(len(rows),len(self.samples), - 'cursor.fetchall did not retrieve all rows' - ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) + self.assertEqual( + len(rows), + len(self.samples), + "cursor.fetchall did not retrieve all rows", + ) rows = [r[0] for r in rows] rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'cursor.fetchall retrieved incorrect rows' + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows" ) rows = cur.fetchall() self.assertEqual( - len(rows),0, - 'cursor.fetchall should return an empty list if called ' - 'after the whole result set has been fetched' - ) - self.assertTrue(cur.rowcount in (-1,len(self.samples))) + len(rows), + 0, + "cursor.fetchall should return an empty list if called " + "after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) + cur.execute("select name from %sbarflys" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,0)) - self.assertEqual(len(rows),0, - 'cursor.fetchall should return an empty list if ' - 'a select query returns no rows' - ) - + self.assertTrue(cur.rowcount in (-1, 0)) + self.assertEqual( + len(rows), + 0, + "cursor.fetchall should return an empty list if " + "a select query returns no rows", + ) + finally: con.close() - + def test_mixedfetch(self): con = self._connect() try: @@ -676,91 +683,91 @@ def test_mixedfetch(self): for sql in self._populate(): cur.execute(sql) - cur.execute('select name from %sbooze' % self.table_prefix) - rows1 = cur.fetchone() + cur.execute("select name from %sbooze" % self.table_prefix) + rows1 = cur.fetchone() rows23 = cur.fetchmany(2) - rows4 = cur.fetchone() + rows4 = cur.fetchone() rows56 = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,6)) - self.assertEqual(len(rows23),2, - 'fetchmany returned incorrect number of rows' - ) - self.assertEqual(len(rows56),2, - 'fetchall returned incorrect number of rows' - ) + self.assertTrue(cur.rowcount in (-1, 6)) + self.assertEqual( + len(rows23), 2, "fetchmany returned incorrect number of rows" + ) + self.assertEqual( + len(rows56), 2, "fetchall returned incorrect number of rows" + ) rows = [rows1[0]] - rows.extend([rows23[0][0],rows23[1][0]]) + rows.extend([rows23[0][0], rows23[1][0]]) rows.append(rows4[0]) - rows.extend([rows56[0][0],rows56[1][0]]) + rows.extend([rows56[0][0], rows56[1][0]]) rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'incorrect data retrieved or inserted' - ) + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "incorrect data retrieved or inserted" + ) finally: con.close() - def help_nextset_setUp(self,cur): - ''' Should create a procedure called deleteme - that returns two result sets, first the - number of rows in booze then "name from booze" - ''' - raise NotImplementedError('Helper not implemented') - #sql=""" + def help_nextset_setUp(self, cur): + """Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + """ + raise NotImplementedError("Helper not implemented") + # sql=""" # create procedure deleteme as # begin # select count(*) from booze # select name from booze # end - #""" - #cur.execute(sql) + # """ + # cur.execute(sql) - def help_nextset_tearDown(self,cur): - 'If cleaning up is needed after nextSetTest' - raise NotImplementedError('Helper not implemented') - #cur.execute("drop procedure deleteme") + def help_nextset_tearDown(self, cur): + "If cleaning up is needed after nextSetTest" + raise NotImplementedError("Helper not implemented") + # cur.execute("drop procedure deleteme") def test_nextset(self): con = self._connect() try: cur = con.cursor() - if not hasattr(cur,'nextset'): + if not hasattr(cur, "nextset"): return try: self.executeDDL1(cur) - sql=self._populate() + sql = self._populate() for sql in self._populate(): cur.execute(sql) self.help_nextset_setUp(cur) - cur.callproc('deleteme') - numberofrows=cur.fetchone() - assert numberofrows[0]== len(self.samples) + cur.callproc("deleteme") + numberofrows = cur.fetchone() + assert numberofrows[0] == len(self.samples) assert cur.nextset() - names=cur.fetchall() + names = cur.fetchall() assert len(names) == len(self.samples) - s=cur.nextset() - assert s == None,'No more return sets, should return None' + s = cur.nextset() + assert s is None, "No more return sets, should return None" finally: self.help_nextset_tearDown(cur) finally: con.close() - def test_nextset(self): - raise NotImplementedError('Drivers need to override this test') + def test_nextset(self): # noqa: F811 + raise NotImplementedError("Drivers need to override this test") def test_arraysize(self): # Not much here - rest of the tests for this are in test_fetchmany con = self._connect() try: cur = con.cursor() - self.assertTrue(hasattr(cur,'arraysize'), - 'cursor.arraysize must be defined' - ) + self.assertTrue( + hasattr(cur, "arraysize"), "cursor.arraysize must be defined" + ) finally: con.close() @@ -768,8 +775,8 @@ def test_setinputsizes(self): con = self._connect() try: cur = con.cursor() - cur.setinputsizes( (25,) ) - self._paraminsert(cur) # Make sure cursor still works + cur.setinputsizes((25,)) + self._paraminsert(cur) # Make sure cursor still works finally: con.close() @@ -779,75 +786,74 @@ def test_setoutputsize_basic(self): try: cur = con.cursor() cur.setoutputsize(1000) - cur.setoutputsize(2000,0) - self._paraminsert(cur) # Make sure the cursor still works + cur.setoutputsize(2000, 0) + self._paraminsert(cur) # Make sure the cursor still works finally: con.close() def test_setoutputsize(self): - # Real test for setoutputsize is driver dependant - raise NotImplementedError('Driver need to override this test') + # Real test for setoutputsize is driver dependent + raise NotImplementedError("Driver need to override this test") def test_None(self): con = self._connect() try: cur = con.cursor() self.executeDDL1(cur) - cur.execute('insert into %sbooze values (NULL)' % self.table_prefix) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("insert into %sbooze values (NULL)" % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchall() - self.assertEqual(len(r),1) - self.assertEqual(len(r[0]),1) - self.assertEqual(r[0][0],None,'NULL value not returned as None') + self.assertEqual(len(r), 1) + self.assertEqual(len(r[0]), 1) + self.assertEqual(r[0][0], None, "NULL value not returned as None") finally: con.close() def test_Date(self): - d1 = self.driver.Date(2002,12,25) - d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0))) + d1 = self.driver.Date(2002, 12, 25) # noqa F841 + d2 = self.driver.DateFromTicks( # noqa F841 + time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0)) + ) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(d1),str(d2)) def test_Time(self): - t1 = self.driver.Time(13,45,30) - t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0))) + t1 = self.driver.Time(13, 45, 30) # noqa F841 + t2 = self.driver.TimeFromTicks( # noqa F841 + time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0)) + ) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Timestamp(self): - t1 = self.driver.Timestamp(2002,12,25,13,45,30) - t2 = self.driver.TimestampFromTicks( - time.mktime((2002,12,25,13,45,30,0,0,0)) - ) + t1 = self.driver.Timestamp(2002, 12, 25, 13, 45, 30) # noqa F841 + t2 = self.driver.TimestampFromTicks( # noqa F841 + time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0)) + ) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Binary(self): - b = self.driver.Binary(b'Something') - b = self.driver.Binary(b'') + b = self.driver.Binary(b"Something") + b = self.driver.Binary(b"") # noqa F841 def test_STRING(self): - self.assertTrue(hasattr(self.driver,'STRING'), - 'module.STRING must be defined' - ) + self.assertTrue(hasattr(self.driver, "STRING"), "module.STRING must be defined") def test_BINARY(self): - self.assertTrue(hasattr(self.driver,'BINARY'), - 'module.BINARY must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "BINARY"), "module.BINARY must be defined." + ) def test_NUMBER(self): - self.assertTrue(hasattr(self.driver,'NUMBER'), - 'module.NUMBER must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "NUMBER"), "module.NUMBER must be defined." + ) def test_DATETIME(self): - self.assertTrue(hasattr(self.driver,'DATETIME'), - 'module.DATETIME must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "DATETIME"), "module.DATETIME must be defined." + ) def test_ROWID(self): - self.assertTrue(hasattr(self.driver,'ROWID'), - 'module.ROWID must be defined.' - ) - + self.assertTrue(hasattr(self.driver, "ROWID"), "module.ROWID must be defined.") diff --git a/tests/default.cnf b/tests/default.cnf index 2aeda7cf..1d6c9421 100644 --- a/tests/default.cnf +++ b/tests/default.cnf @@ -2,9 +2,10 @@ # http://dev.mysql.com/doc/refman/5.1/en/option-files.html # and set TESTDB in your environment to the name of the file +# $ docker run -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 --rm --name mysqld mysql:latest [MySQLdb-tests] host = 127.0.0.1 -user = test +user = root database = test #password = -default-character-set = utf8 +default-character-set = utf8mb4 diff --git a/tests/test_MySQLdb_capabilities.py b/tests/test_MySQLdb_capabilities.py index e9b0e2a9..dbff27c2 100644 --- a/tests/test_MySQLdb_capabilities.py +++ b/tests/test_MySQLdb_capabilities.py @@ -1,186 +1,205 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import capabilities -from datetime import timedelta -from contextlib import closing -import unittest -import MySQLdb -from MySQLdb.compat import unicode -from MySQLdb import cursors -from configdb import connection_factory -import warnings - - -warnings.filterwarnings('ignore') - - -class test_MySQLdb(capabilities.DatabaseTest): - - db_module = MySQLdb - connect_args = () - connect_kwargs = dict(use_unicode=True, sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL") - create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" - leak_test = False - - def quote_identifier(self, ident): - return "`%s`" % ident - - def test_TIME(self): - def generator(row,col): - return timedelta(0, row*8000) - self.check_data_integrity( - ('col1 TIME',), - generator) - - def test_TINYINT(self): - # Number data - def generator(row, col): - v = (row*row) % 256 - if v > 127: - v = v-256 - return v - self.check_data_integrity( - ('col1 TINYINT',), - generator) - - def test_stored_procedures(self): - db = self.connection - c = self.cursor - self.create_table(('pos INT', 'tree CHAR(20)')) - c.executemany("INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, - list(enumerate('ash birch cedar Lärche pine'.split()))) - db.commit() - - c.execute(""" - CREATE PROCEDURE test_sp(IN t VARCHAR(255)) - BEGIN - SELECT pos FROM %s WHERE tree = t; - END - """ % self.table) - db.commit() - - c.callproc('test_sp', ('Lärche',)) - rows = c.fetchall() - self.assertEqual(len(rows), 1) - self.assertEqual(rows[0][0], 3) - c.nextset() - - c.execute("DROP PROCEDURE test_sp") - c.execute('drop table %s' % (self.table)) - - def test_small_CHAR(self): - # Character data - def generator(row,col): - i = (row*col+62)%256 - if i == 62: return '' - if i == 63: return None - return chr(i) - self.check_data_integrity( - ('col1 char(1)','col2 char(1)'), - generator) - - def test_BIT(self): - c = self.cursor - try: - c.execute("""create table test_BIT ( - b3 BIT(3), - b7 BIT(10), - b64 BIT(64))""") - - one64 = '1'*64 - c.execute( - "insert into test_BIT (b3, b7, b64)" - " VALUES (b'011', b'1111111111', b'%s')" - % one64) - - c.execute("SELECT b3, b7, b64 FROM test_BIT") - row = c.fetchone() - self.assertEqual(row[0], b'\x03') - self.assertEqual(row[1], b'\x03\xff') - self.assertEqual(row[2], b'\xff'*8) - finally: - c.execute("drop table if exists test_BIT") - - def test_MULTIPOLYGON(self): - c = self.cursor - try: - c.execute("""create table test_MULTIPOLYGON ( - id INTEGER PRIMARY KEY, - border MULTIPOLYGON)""") - - c.execute( - "insert into test_MULTIPOLYGON (id, border)" - " VALUES (1, GeomFromText('MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))'))" - ) - - c.execute("SELECT id, AsText(border) FROM test_MULTIPOLYGON") - row = c.fetchone() - self.assertEqual(row[0], 1) - self.assertEqual(row[1], 'MULTIPOLYGON(((1 1,1 -1,-1 -1,-1 1,1 1)),((1 1,3 1,3 3,1 3,1 1)))') - - c.execute("SELECT id, AsWKB(border) FROM test_MULTIPOLYGON") - row = c.fetchone() - self.assertEqual(row[0], 1) - self.assertNotEqual(len(row[1]), 0) - - c.execute("SELECT id, border FROM test_MULTIPOLYGON") - row = c.fetchone() - self.assertEqual(row[0], 1) - self.assertNotEqual(len(row[1]), 0) - finally: - c.execute("drop table if exists test_MULTIPOLYGON") - - def test_bug_2671682(self): - from MySQLdb.constants import ER - try: - self.cursor.execute("describe some_non_existent_table"); - except self.connection.ProgrammingError as msg: - self.assertTrue(str(ER.NO_SUCH_TABLE) in str(msg)) - - def test_bug_3514287(self): - c = self.cursor - try: - c.execute("""create table bug_3541287 ( - c1 CHAR(10), - t1 TIMESTAMP)""") - c.execute("insert into bug_3541287 (c1,t1) values (%s, NOW())", - ("blah",)) - finally: - c.execute("drop table if exists bug_3541287") - - def test_ping(self): - self.connection.ping() - - def test_reraise_exception(self): - c = self.cursor - try: - c.execute("SELECT x FROM not_existing_table") - except MySQLdb.ProgrammingError as e: - self.assertEqual(e.args[0], 1146) - return - self.fail("Should raise ProgrammingError") - - def test_binary_prefix(self): - # verify prefix behaviour when enabled, disabled and for default (disabled) - for binary_prefix in (True, False, None): - kwargs = self.connect_kwargs.copy() - # needs to be set to can guarantee CHARSET response for normal strings - kwargs['charset'] = 'utf8' - if binary_prefix != None: - kwargs['binary_prefix'] = binary_prefix - - with closing(connection_factory(**kwargs)) as conn: - with closing(conn.cursor()) as c: - c.execute('SELECT CHARSET(%s)', (MySQLdb.Binary(b'raw bytes'),)) - self.assertEqual(c.fetchall()[0][0], 'binary' if binary_prefix else 'utf8') - # normal strings should not get prefix - c.execute('SELECT CHARSET(%s)', ('str',)) - self.assertEqual(c.fetchall()[0][0], 'utf8') - - -if __name__ == '__main__': - if test_MySQLdb.leak_test: - import gc - gc.enable() - gc.set_debug(gc.DEBUG_LEAK) - unittest.main() +#!/usr/bin/env python +import capabilities +from datetime import timedelta +from contextlib import closing +import unittest +import MySQLdb +from configdb import connection_factory +import warnings + + +warnings.filterwarnings("ignore") + + +class test_MySQLdb(capabilities.DatabaseTest): + db_module = MySQLdb + connect_args = () + connect_kwargs = dict( + use_unicode=True, sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL" + ) + create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" + leak_test = False + + def quote_identifier(self, ident): + return "`%s`" % ident + + def test_TIME(self): + def generator(row, col): + return timedelta(0, row * 8000) + + self.check_data_integrity(("col1 TIME",), generator) + + def test_TINYINT(self): + # Number data + def generator(row, col): + v = (row * row) % 256 + if v > 127: + v = v - 256 + return v + + self.check_data_integrity(("col1 TINYINT",), generator) + + def test_stored_procedures(self): + db = self.connection + c = self.cursor + self.create_table(("pos INT", "tree CHAR(20)")) + c.executemany( + "INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, + list(enumerate("ash birch cedar Lärche pine".split())), + ) + db.commit() + + c.execute( + """ + CREATE PROCEDURE test_sp(IN t VARCHAR(255)) + BEGIN + SELECT pos FROM %s WHERE tree = t; + END + """ + % self.table + ) + db.commit() + + c.callproc("test_sp", ("Lärche",)) + rows = c.fetchall() + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], 3) + c.nextset() + + c.execute("DROP PROCEDURE test_sp") + c.execute("drop table %s" % (self.table)) + + def test_small_CHAR(self): + # Character data + def generator(row, col): + i = (row * col + 62) % 256 + if i == 62: + return "" + if i == 63: + return None + return chr(i) + + self.check_data_integrity(("col1 char(1)", "col2 char(1)"), generator) + + def test_BIT(self): + c = self.cursor + try: + c.execute( + """create table test_BIT ( + b3 BIT(3), + b7 BIT(10), + b64 BIT(64))""" + ) + + one64 = "1" * 64 + c.execute( + "insert into test_BIT (b3, b7, b64)" + " VALUES (b'011', b'1111111111', b'%s')" % one64 + ) + + c.execute("SELECT b3, b7, b64 FROM test_BIT") + row = c.fetchone() + self.assertEqual(row[0], b"\x03") + self.assertEqual(row[1], b"\x03\xff") + self.assertEqual(row[2], b"\xff" * 8) + finally: + c.execute("drop table if exists test_BIT") + + def test_MULTIPOLYGON(self): + c = self.cursor + try: + c.execute( + """create table test_MULTIPOLYGON ( + id INTEGER PRIMARY KEY, + border MULTIPOLYGON)""" + ) + + c.execute( + """ +INSERT INTO test_MULTIPOLYGON + (id, border) +VALUES (1, + ST_Geomfromtext( +'MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))')) +""" + ) + + c.execute("SELECT id, ST_AsText(border) FROM test_MULTIPOLYGON") + row = c.fetchone() + self.assertEqual(row[0], 1) + self.assertEqual( + row[1], + "MULTIPOLYGON(((1 1,1 -1,-1 -1,-1 1,1 1)),((1 1,3 1,3 3,1 3,1 1)))", + ) + + c.execute("SELECT id, ST_AsWKB(border) FROM test_MULTIPOLYGON") + row = c.fetchone() + self.assertEqual(row[0], 1) + self.assertNotEqual(len(row[1]), 0) + + c.execute("SELECT id, border FROM test_MULTIPOLYGON") + row = c.fetchone() + self.assertEqual(row[0], 1) + self.assertNotEqual(len(row[1]), 0) + finally: + c.execute("drop table if exists test_MULTIPOLYGON") + + def test_bug_2671682(self): + from MySQLdb.constants import ER + + try: + self.cursor.execute("describe some_non_existent_table") + except self.connection.ProgrammingError as msg: + self.assertTrue(str(ER.NO_SUCH_TABLE) in str(msg)) + + def test_bug_3514287(self): + c = self.cursor + try: + c.execute( + """create table bug_3541287 ( + c1 CHAR(10), + t1 TIMESTAMP)""" + ) + c.execute("insert into bug_3541287 (c1,t1) values (%s, NOW())", ("blah",)) + finally: + c.execute("drop table if exists bug_3541287") + + def test_ping(self): + self.connection.ping() + + def test_reraise_exception(self): + c = self.cursor + try: + c.execute("SELECT x FROM not_existing_table") + except MySQLdb.ProgrammingError as e: + self.assertEqual(e.args[0], 1146) + return + self.fail("Should raise ProgrammingError") + + def test_binary_prefix(self): + # verify prefix behaviour when enabled, disabled and for default (disabled) + for binary_prefix in (True, False, None): + kwargs = self.connect_kwargs.copy() + # needs to be set to can guarantee CHARSET response for normal strings + kwargs["charset"] = "utf8mb4" + if binary_prefix is not None: + kwargs["binary_prefix"] = binary_prefix + + with closing(connection_factory(**kwargs)) as conn: + with closing(conn.cursor()) as c: + c.execute("SELECT CHARSET(%s)", (MySQLdb.Binary(b"raw bytes"),)) + self.assertEqual( + c.fetchall()[0][0], "binary" if binary_prefix else "utf8mb4" + ) + # normal strings should not get prefix + c.execute("SELECT CHARSET(%s)", ("str",)) + self.assertEqual(c.fetchall()[0][0], "utf8mb4") + + +if __name__ == "__main__": + if test_MySQLdb.leak_test: + import gc + + gc.enable() + gc.set_debug(gc.DEBUG_LEAK) + unittest.main() diff --git a/tests/test_MySQLdb_dbapi20.py b/tests/test_MySQLdb_dbapi20.py index 88eaaef8..a0dd92a1 100644 --- a/tests/test_MySQLdb_dbapi20.py +++ b/tests/test_MySQLdb_dbapi20.py @@ -4,23 +4,28 @@ import MySQLdb from configdb import connection_kwargs import warnings + warnings.simplefilter("ignore") class test_MySQLdb(dbapi20.DatabaseAPI20Test): driver = MySQLdb connect_args = () - connect_kw_args = connection_kwargs(dict(sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")) + connect_kw_args = connection_kwargs( + dict(sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL") + ) + + def test_setoutputsize(self): + pass - def test_setoutputsize(self): pass - def test_setoutputsize_basic(self): pass - def test_nextset(self): pass + def test_setoutputsize_basic(self): + pass """The tests on fetchone and fetchall and rowcount bogusly test for an exception if the statement cannot return a result set. MySQL always returns a result set; it's just that some things return empty result sets.""" - + def test_fetchall(self): con = self._connect() try: @@ -36,40 +41,45 @@ def test_fetchall(self): # cursor.fetchall should raise an Error if called # after executing a statement that cannot return rows - #self.assertRaises(self.driver.Error,cur.fetchall) + # self.assertRaises(self.driver.Error,cur.fetchall) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,len(self.samples))) - self.assertEqual(len(rows),len(self.samples), - 'cursor.fetchall did not retrieve all rows' - ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) + self.assertEqual( + len(rows), + len(self.samples), + "cursor.fetchall did not retrieve all rows", + ) rows = [r[0] for r in rows] rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'cursor.fetchall retrieved incorrect rows' + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows" ) rows = cur.fetchall() self.assertEqual( - len(rows),0, - 'cursor.fetchall should return an empty list if called ' - 'after the whole result set has been fetched' - ) - self.assertTrue(cur.rowcount in (-1,len(self.samples))) + len(rows), + 0, + "cursor.fetchall should return an empty list if called " + "after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) + cur.execute("select name from %sbarflys" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,0)) - self.assertEqual(len(rows),0, - 'cursor.fetchall should return an empty list if ' - 'a select query returns no rows' - ) - + self.assertTrue(cur.rowcount in (-1, 0)) + self.assertEqual( + len(rows), + 0, + "cursor.fetchall should return an empty list if " + "a select query returns no rows", + ) + finally: con.close() - + def test_fetchone(self): con = self._connect() try: @@ -77,39 +87,42 @@ def test_fetchone(self): # cursor.fetchone should raise an Error if called before # executing a select-type query - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) # cursor.fetchone should raise an Error if called after # executing a query that cannot return rows self.executeDDL1(cur) -## self.assertRaises(self.driver.Error,cur.fetchone) + # self.assertRaises(self.driver.Error,cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if a query retrieves ' - 'no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if a query retrieves " "no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) # cursor.fetchone should raise an Error if called after # executing a query that cannot return rows - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) -## self.assertRaises(self.driver.Error,cur.fetchone) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + # self.assertRaises(self.driver.Error,cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchone() - self.assertEqual(len(r),1, - 'cursor.fetchone should have retrieved a single row' - ) - self.assertEqual(r[0],'Victoria Bitter', - 'cursor.fetchone retrieved incorrect data' - ) -## self.assertEqual(cur.fetchone(),None, -## 'cursor.fetchone should return None if no more rows available' -## ) - self.assertTrue(cur.rowcount in (-1,1)) + self.assertEqual( + len(r), 1, "cursor.fetchone should have retrieved a single row" + ) + self.assertEqual( + r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data" + ) + # self.assertEqual( + # cur.fetchone(), + # None, + # "cursor.fetchone should return None if no more rows available", + # ) + self.assertTrue(cur.rowcount in (-1, 1)) finally: con.close() @@ -119,87 +132,100 @@ def test_rowcount(self): try: cur = con.cursor() self.executeDDL1(cur) -## self.assertEqual(cur.rowcount,-1, -## 'cursor.rowcount should be -1 after executing no-result ' -## 'statements' -## ) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) -## self.assertTrue(cur.rowcount in (-1,1), -## 'cursor.rowcount should == number or rows inserted, or ' -## 'set to -1 after executing an insert statement' -## ) + # self.assertEqual(cur.rowcount,-1, + # 'cursor.rowcount should be -1 after executing no-result ' + # 'statements' + # ) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + # self.assertTrue(cur.rowcount in (-1,1), + # 'cursor.rowcount should == number or rows inserted, or ' + # 'set to -1 after executing an insert statement' + # ) cur.execute("select name from %sbooze" % self.table_prefix) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number of rows returned, or ' - 'set to -1 after executing a select statement' - ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number of rows returned, or " + "set to -1 after executing a select statement", + ) self.executeDDL2(cur) -## self.assertEqual(cur.rowcount,-1, -## 'cursor.rowcount not being reset to -1 after executing ' -## 'no-result statements' -## ) + # self.assertEqual(cur.rowcount,-1, + # 'cursor.rowcount not being reset to -1 after executing ' + # 'no-result statements' + # ) finally: con.close() def test_callproc(self): - pass # performed in test_MySQL_capabilities - - def help_nextset_setUp(self,cur): - ''' Should create a procedure called deleteme - that returns two result sets, first the - number of rows in booze then "name from booze" - ''' - sql=""" + pass # performed in test_MySQL_capabilities + + def help_nextset_setUp(self, cur): + """ + Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + """ + sql = """ create procedure deleteme() begin select count(*) from %(tp)sbooze; select name from %(tp)sbooze; end - """ % dict(tp=self.table_prefix) + """ % dict( + tp=self.table_prefix + ) cur.execute(sql) - def help_nextset_tearDown(self,cur): - 'If cleaning up is needed after nextSetTest' + def help_nextset_tearDown(self, cur): + "If cleaning up is needed after nextSetTest" cur.execute("drop procedure deleteme") def test_nextset(self): - #from warnings import warn + # from warnings import warn + con = self._connect() try: cur = con.cursor() - if not hasattr(cur, 'nextset'): + if not hasattr(cur, "nextset"): return try: self.executeDDL1(cur) - sql=self._populate() + sql = self._populate() for sql in self._populate(): cur.execute(sql) self.help_nextset_setUp(cur) - cur.callproc('deleteme') - numberofrows=cur.fetchone() - assert numberofrows[0]== len(self.samples) + cur.callproc("deleteme") + numberofrows = cur.fetchone() + assert numberofrows[0] == len(self.samples) assert cur.nextset() - names=cur.fetchall() + names = cur.fetchall() assert len(names) == len(self.samples) - s=cur.nextset() + s = cur.nextset() if s: empty = cur.fetchall() - self.assertEqual(len(empty), 0, - "non-empty result set after other result sets") - #warn("Incompatibility: MySQL returns an empty result set for the CALL itself", - # Warning) - #assert s == None,'No more return sets, should return None' + self.assertEqual( + len(empty), 0, "non-empty result set after other result sets" + ) + # warn( + # ": ".join( + # [ + # "Incompatibility", + # "MySQL returns an empty result set for the CALL itself" + # ] + # ), + # Warning, + # ) + # assert s == None, "No more return sets, should return None" finally: self.help_nextset_tearDown(cur) finally: con.close() - -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_MySQLdb_nonstandard.py b/tests/test_MySQLdb_nonstandard.py index a06d5edf..5e841791 100644 --- a/tests/test_MySQLdb_nonstandard.py +++ b/tests/test_MySQLdb_nonstandard.py @@ -5,6 +5,7 @@ from MySQLdb.constants import FIELD_TYPE from configdb import connection_factory import warnings + warnings.simplefilter("ignore") @@ -25,10 +26,6 @@ def test_set_inequality_membership(self): class TestCoreModule(unittest.TestCase): """Core _mysql module features.""" - def test_NULL(self): - """Should have a NULL constant.""" - self.assertEqual(_mysql.NULL, 'NULL') - def test_version(self): """Version information sanity.""" self.assertTrue(isinstance(_mysql.__version__, str)) @@ -39,14 +36,13 @@ def test_version(self): def test_client_info(self): self.assertTrue(isinstance(_mysql.get_client_info(), str)) - def test_thread_safe(self): - self.assertTrue(isinstance(_mysql.thread_safe(), int)) - def test_escape_string(self): - self.assertEqual(_mysql.escape_string(b'foo"bar'), - b'foo\\"bar', "escape byte string") - self.assertEqual(_mysql.escape_string(u'foo"bar'), - b'foo\\"bar', "escape unicode string") + self.assertEqual( + _mysql.escape_string(b'foo"bar'), b'foo\\"bar', "escape byte string" + ) + self.assertEqual( + _mysql.escape_string('foo"bar'), b'foo\\"bar', "escape unicode string" + ) class CoreAPI(unittest.TestCase): @@ -60,46 +56,91 @@ def tearDown(self): def test_thread_id(self): tid = self.conn.thread_id() - self.assertTrue(isinstance(tid, int), - "thread_id didn't return an int.") + self.assertTrue(isinstance(tid, int), "thread_id didn't return an int.") - self.assertRaises(TypeError, self.conn.thread_id, ('evil',), - "thread_id shouldn't accept arguments.") + self.assertRaises( + TypeError, + self.conn.thread_id, + ("evil",), + "thread_id shouldn't accept arguments.", + ) def test_affected_rows(self): - self.assertEqual(self.conn.affected_rows(), 0, - "Should return 0 before we do anything.") - + self.assertEqual( + self.conn.affected_rows(), 0, "Should return 0 before we do anything." + ) - #def test_debug(self): - ## FIXME Only actually tests if you lack SUPER - #self.assertRaises(MySQLdb.OperationalError, - #self.conn.dump_debug_info) + # def test_debug(self): + # (FIXME) Only actually tests if you lack SUPER + # self.assertRaises(MySQLdb.OperationalError, + # self.conn.dump_debug_info) def test_charset_name(self): - self.assertTrue(isinstance(self.conn.character_set_name(), str), - "Should return a string.") + self.assertTrue( + isinstance(self.conn.character_set_name(), str), "Should return a string." + ) def test_host_info(self): - self.assertTrue(isinstance(self.conn.get_host_info(), str), - "Should return a string.") + self.assertTrue( + isinstance(self.conn.get_host_info(), str), "Should return a string." + ) def test_proto_info(self): - self.assertTrue(isinstance(self.conn.get_proto_info(), int), - "Should return an int.") + self.assertTrue( + isinstance(self.conn.get_proto_info(), int), "Should return an int." + ) def test_server_info(self): - self.assertTrue(isinstance(self.conn.get_server_info(), str), - "Should return an str.") + self.assertTrue( + isinstance(self.conn.get_server_info(), str), "Should return a string." + ) def test_client_flag(self): conn = connection_factory( - use_unicode=True, - client_flag=MySQLdb.constants.CLIENT.FOUND_ROWS) + use_unicode=True, client_flag=MySQLdb.constants.CLIENT.FOUND_ROWS + ) - self.assertIsInstance(conn.client_flag, (int, MySQLdb.compat.long)) + self.assertIsInstance(conn.client_flag, int) self.assertTrue(conn.client_flag & MySQLdb.constants.CLIENT.FOUND_ROWS) - with self.assertRaises(TypeError if MySQLdb.compat.PY2 else AttributeError): + with self.assertRaises(AttributeError): conn.client_flag = 0 conn.close() + + def test_fileno(self): + self.assertGreaterEqual(self.conn.fileno(), 0) + + def test_context_manager(self): + with connection_factory() as conn: + self.assertFalse(conn.closed) + self.assertTrue(conn.closed) + + +class TestCollation(unittest.TestCase): + """Test charset and collation connection options.""" + + def setUp(self): + # Initialize a connection with a non-default character set and + # collation. + self.conn = connection_factory( + charset="utf8mb4", + collation="utf8mb4_esperanto_ci", + ) + + def tearDown(self): + self.conn.close() + + def test_charset_collation(self): + c = self.conn.cursor() + c.execute( + """ + SHOW VARIABLES WHERE + Variable_Name="character_set_connection" OR + Variable_Name="collation_connection"; + """ + ) + row = c.fetchall() + charset = row[0][1] + collation = row[1][1] + self.assertEqual(charset, "utf8mb4") + self.assertEqual(collation, "utf8mb4_esperanto_ci") diff --git a/tests/test_MySQLdb_times.py b/tests/test_MySQLdb_times.py index 8000f645..2081b1ac 100644 --- a/tests/test_MySQLdb_times.py +++ b/tests/test_MySQLdb_times.py @@ -1,121 +1,146 @@ -import mock -import unittest -from time import gmtime from datetime import time, date, datetime, timedelta +from time import gmtime +import unittest +from unittest import mock +import warnings from MySQLdb import times -import warnings + warnings.simplefilter("ignore") class TestX_or_None(unittest.TestCase): def test_date_or_none(self): - assert times.Date_or_None('1969-01-01') == date(1969, 1, 1) - assert times.Date_or_None('2015-01-01') == date(2015, 1, 1) - assert times.Date_or_None('2015-12-13') == date(2015, 12, 13) + assert times.Date_or_None("1969-01-01") == date(1969, 1, 1) + assert times.Date_or_None("2015-01-01") == date(2015, 1, 1) + assert times.Date_or_None("2015-12-13") == date(2015, 12, 13) - assert times.Date_or_None('') is None - assert times.Date_or_None('fail') is None - assert times.Date_or_None('2015-12') is None - assert times.Date_or_None('2015-12-40') is None - assert times.Date_or_None('0000-00-00') is None + assert times.Date_or_None("") is None + assert times.Date_or_None("fail") is None + assert times.Date_or_None("2015-12") is None + assert times.Date_or_None("2015-12-40") is None + assert times.Date_or_None("0000-00-00") is None def test_time_or_none(self): - assert times.Time_or_None('00:00:00') == time(0, 0) - assert times.Time_or_None('01:02:03') == time(1, 2, 3) - assert times.Time_or_None('01:02:03.123456') == time(1, 2, 3, 123456) + assert times.Time_or_None("00:00:00") == time(0, 0) + assert times.Time_or_None("01:02:03") == time(1, 2, 3) + assert times.Time_or_None("01:02:03.123456") == time(1, 2, 3, 123456) - assert times.Time_or_None('') is None - assert times.Time_or_None('fail') is None - assert times.Time_or_None('24:00:00') is None - assert times.Time_or_None('01:02:03.123456789') is None + assert times.Time_or_None("") is None + assert times.Time_or_None("fail") is None + assert times.Time_or_None("24:00:00") is None + assert times.Time_or_None("01:02:03.123456789") is None def test_datetime_or_none(self): - assert times.DateTime_or_None('1000-01-01') == date(1000, 1, 1) - assert times.DateTime_or_None('2015-12-13') == date(2015, 12, 13) - assert times.DateTime_or_None('2015-12-13 01:02') == datetime(2015, 12, 13, 1, 2) - assert times.DateTime_or_None('2015-12-13T01:02') == datetime(2015, 12, 13, 1, 2) - assert times.DateTime_or_None('2015-12-13 01:02:03') == datetime(2015, 12, 13, 1, 2, 3) - assert times.DateTime_or_None('2015-12-13T01:02:03') == datetime(2015, 12, 13, 1, 2, 3) - assert times.DateTime_or_None('2015-12-13 01:02:03.123') == datetime(2015, 12, 13, 1, 2, 3, 123000) - assert times.DateTime_or_None('2015-12-13 01:02:03.000123') == datetime(2015, 12, 13, 1, 2, 3, 123) - assert times.DateTime_or_None('2015-12-13 01:02:03.123456') == datetime(2015, 12, 13, 1, 2, 3, 123456) - assert times.DateTime_or_None('2015-12-13T01:02:03.123456') == datetime(2015, 12, 13, 1, 2, 3, 123456) - - assert times.DateTime_or_None('') is None - assert times.DateTime_or_None('fail') is None - assert times.DateTime_or_None('0000-00-00 00:00:00') is None - assert times.DateTime_or_None('0000-00-00 00:00:00.000000') is None - assert times.DateTime_or_None('2015-12-13T01:02:03.123456789') is None + assert times.DateTime_or_None("1000-01-01") == date(1000, 1, 1) + assert times.DateTime_or_None("2015-12-13") == date(2015, 12, 13) + assert times.DateTime_or_None("2015-12-13 01:02") == datetime( + 2015, 12, 13, 1, 2 + ) + assert times.DateTime_or_None("2015-12-13T01:02") == datetime( + 2015, 12, 13, 1, 2 + ) + assert times.DateTime_or_None("2015-12-13 01:02:03") == datetime( + 2015, 12, 13, 1, 2, 3 + ) + assert times.DateTime_or_None("2015-12-13T01:02:03") == datetime( + 2015, 12, 13, 1, 2, 3 + ) + assert times.DateTime_or_None("2015-12-13 01:02:03.123") == datetime( + 2015, 12, 13, 1, 2, 3, 123000 + ) + assert times.DateTime_or_None("2015-12-13 01:02:03.000123") == datetime( + 2015, 12, 13, 1, 2, 3, 123 + ) + assert times.DateTime_or_None("2015-12-13 01:02:03.123456") == datetime( + 2015, 12, 13, 1, 2, 3, 123456 + ) + assert times.DateTime_or_None("2015-12-13T01:02:03.123456") == datetime( + 2015, 12, 13, 1, 2, 3, 123456 + ) + + assert times.DateTime_or_None("") is None + assert times.DateTime_or_None("fail") is None + assert times.DateTime_or_None("0000-00-00 00:00:00") is None + assert times.DateTime_or_None("0000-00-00 00:00:00.000000") is None + assert times.DateTime_or_None("2015-12-13T01:02:03.123456789") is None def test_timedelta_or_none(self): - assert times.TimeDelta_or_None('-1:0:0') == timedelta(0, -3600) - assert times.TimeDelta_or_None('1:0:0') == timedelta(0, 3600) - assert times.TimeDelta_or_None('12:55:30') == timedelta(0, 46530) - assert times.TimeDelta_or_None('12:55:30.123456') == timedelta(0, 46530, 123456) - assert times.TimeDelta_or_None('12:55:30.123456789') == timedelta(0, 46653, 456789) - assert times.TimeDelta_or_None('12:55:30.123456789123456') == timedelta(1429, 37719, 123456) - - assert times.TimeDelta_or_None('') is None - assert times.TimeDelta_or_None('0') is None - assert times.TimeDelta_or_None('fail') is None + assert times.TimeDelta_or_None("-1:0:0") == timedelta(0, -3600) + assert times.TimeDelta_or_None("1:0:0") == timedelta(0, 3600) + assert times.TimeDelta_or_None("12:55:30") == timedelta(0, 46530) + assert times.TimeDelta_or_None("12:55:30.123456") == timedelta(0, 46530, 123456) + assert times.TimeDelta_or_None("12:55:30.123456789") == timedelta( + 0, 46653, 456789 + ) + assert times.TimeDelta_or_None("12:55:30.123456789123456") == timedelta( + 1429, 37719, 123456 + ) + + assert times.TimeDelta_or_None("") is None + assert times.TimeDelta_or_None("0") is None + assert times.TimeDelta_or_None("fail") is None class TestTicks(unittest.TestCase): - @mock.patch('MySQLdb.times.localtime', side_effect=gmtime) + @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) def test_date_from_ticks(self, mock): assert times.DateFromTicks(0) == date(1970, 1, 1) assert times.DateFromTicks(1430000000) == date(2015, 4, 25) - @mock.patch('MySQLdb.times.localtime', side_effect=gmtime) + @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) def test_time_from_ticks(self, mock): assert times.TimeFromTicks(0) == time(0, 0, 0) assert times.TimeFromTicks(1431100000) == time(15, 46, 40) assert times.TimeFromTicks(1431100000.123) == time(15, 46, 40) - @mock.patch('MySQLdb.times.localtime', side_effect=gmtime) + @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) def test_timestamp_from_ticks(self, mock): assert times.TimestampFromTicks(0) == datetime(1970, 1, 1, 0, 0, 0) assert times.TimestampFromTicks(1430000000) == datetime(2015, 4, 25, 22, 13, 20) - assert times.TimestampFromTicks(1430000000.123) == datetime(2015, 4, 25, 22, 13, 20) - - -class TestTimestampConverter(unittest.TestCase): - def test_mysql_timestamp_converter(self): - assert times.mysql_timestamp_converter('2015-12-13') == date(2015, 12, 13) - assert times.mysql_timestamp_converter('2038-01-19 03:14:07') == datetime(2038, 1, 19, 3, 14, 7) - - assert times.mysql_timestamp_converter('2015121310') == datetime(2015, 12, 13, 10, 0) - assert times.mysql_timestamp_converter('20151213101112') == datetime(2015, 12, 13, 10, 11, 12) - - assert times.mysql_timestamp_converter('20151313') is None - assert times.mysql_timestamp_converter('2015-13-13') is None + assert times.TimestampFromTicks(1430000000.123) == datetime( + 2015, 4, 25, 22, 13, 20 + ) class TestToLiteral(unittest.TestCase): def test_datetime_to_literal(self): - assert times.DateTime2literal(datetime(2015, 12, 13), '') == b"'2015-12-13 00:00:00'" - assert times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13), '') == b"'2015-12-13 11:12:13'" - assert times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13, 123456), '') == b"'2015-12-13 11:12:13.123456'" + self.assertEqual( + times.DateTime2literal(datetime(2015, 12, 13), ""), b"'2015-12-13 00:00:00'" + ) + self.assertEqual( + times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13), ""), + b"'2015-12-13 11:12:13'", + ) + self.assertEqual( + times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13, 123456), ""), + b"'2015-12-13 11:12:13.123456'", + ) def test_datetimedelta_to_literal(self): d = datetime(2015, 12, 13, 1, 2, 3) - datetime(2015, 12, 13, 1, 2, 2) - assert times.DateTimeDelta2literal(d, '') == b"'0 0:0:1'" + assert times.DateTimeDelta2literal(d, "") == b"'0 0:0:1'" class TestFormat(unittest.TestCase): def test_format_timedelta(self): d = datetime(2015, 1, 1) - datetime(2015, 1, 1) - assert times.format_TIMEDELTA(d) == '0 0:0:0' + assert times.format_TIMEDELTA(d) == "0 0:0:0" d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 8, 9, 10) - assert times.format_TIMEDELTA(d) == '0 2:2:2' + assert times.format_TIMEDELTA(d) == "0 2:2:2" d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 11, 12, 13) - assert times.format_TIMEDELTA(d) == '-1 22:58:59' + assert times.format_TIMEDELTA(d) == "-1 22:58:59" def test_format_timestamp(self): - assert times.format_TIMESTAMP(datetime(2015, 2, 3)) == '2015-02-03 00:00:00' - assert times.format_TIMESTAMP(datetime(2015, 2, 3, 17, 18, 19)) == '2015-02-03 17:18:19' - assert times.format_TIMESTAMP(datetime(15, 2, 3, 17, 18, 19)) == '0015-02-03 17:18:19' + assert times.format_TIMESTAMP(datetime(2015, 2, 3)) == "2015-02-03 00:00:00" + self.assertEqual( + times.format_TIMESTAMP(datetime(2015, 2, 3, 17, 18, 19)), + "2015-02-03 17:18:19", + ) + self.assertEqual( + times.format_TIMESTAMP(datetime(15, 2, 3, 17, 18, 19)), + "0015-02-03 17:18:19", + ) diff --git a/tests/test_connection.py b/tests/test_connection.py new file mode 100644 index 00000000..960de572 --- /dev/null +++ b/tests/test_connection.py @@ -0,0 +1,26 @@ +import pytest + +from MySQLdb._exceptions import ProgrammingError + +from configdb import connection_factory + + +def test_multi_statements_default_true(): + conn = connection_factory() + cursor = conn.cursor() + + cursor.execute("select 17; select 2") + rows = cursor.fetchall() + assert rows == ((17,),) + + +def test_multi_statements_false(): + conn = connection_factory(multi_statements=False) + cursor = conn.cursor() + + with pytest.raises(ProgrammingError): + cursor.execute("select 17; select 2") + + cursor.execute("select 17") + rows = cursor.fetchall() + assert rows == ((17,),) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 7919b561..1d2c3655 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -6,6 +6,7 @@ _conns = [] _tables = [] + def connect(**kwargs): conn = connection_factory(**kwargs) _conns.append(conn) @@ -17,7 +18,7 @@ def teardown_function(function): c = _conns[0] cur = c.cursor() for t in _tables: - cur.execute("DROP TABLE %s" % (t,)) + cur.execute(f"DROP TABLE {t}") cur.close() del _tables[:] @@ -33,46 +34,212 @@ def test_executemany(): cursor.execute("create table test (data varchar(10))") _tables.append("test") - m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%s, %s)") - assert m is not None, 'error parse %s' - assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?' - - m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)") - assert m is not None, 'error parse %(name)s' - assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?' - - m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)") - assert m is not None, 'error parse %(id_name)s' - assert m.group(3) == '', 'group 3 not blank, bug in RE_INSERT_VALUES?' - - m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update") - assert m is not None, 'error parse %(id_name)s' - assert m.group(3) == ' ON duplicate update', 'group 3 not ON duplicate update, bug in RE_INSERT_VALUES?' + m = MySQLdb.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%s, %s)" + ) + assert m is not None, "error parse %s" + assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" + + m = MySQLdb.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)" + ) + assert m is not None, "error parse %(name)s" + assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" + + m = MySQLdb.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)" + ) + assert m is not None, "error parse %(id_name)s" + assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" + + m = MySQLdb.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update" + ) + assert m is not None, "error parse %(id_name)s" + assert ( + m.group(3) == " ON duplicate update" + ), "group 3 not ON duplicate update, bug in RE_INSERT_VALUES?" # https://github.com/PyMySQL/mysqlclient-python/issues/178 - m = MySQLdb.cursors.RE_INSERT_VALUES.match("INSERT INTO bloup(foo, bar)VALUES(%s, %s)") + m = MySQLdb.cursors.RE_INSERT_VALUES.match( + "INSERT INTO bloup(foo, bar)VALUES(%s, %s)" + ) assert m is not None - # cursor._executed myst bee "insert into test (data) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)" + # cursor._executed myst bee + # """ + # insert into test (data) + # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9) + # """ # list args - data = range(10) + data = [(i,) for i in range(10)] cursor.executemany("insert into test (data) values (%s)", data) - assert cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %s not in one query' + assert cursor._executed.endswith( + b",(7),(8),(9)" + ), "execute many with %s not in one query" # dict args - data_dict = [{'data': i} for i in range(10)] + data_dict = [{"data": i} for i in range(10)] cursor.executemany("insert into test (data) values (%(data)s)", data_dict) - assert cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %(data)s not in one query' + assert cursor._executed.endswith( + b",(7),(8),(9)" + ), "execute many with %(data)s not in one query" # %% in column set - cursor.execute("""\ + cursor.execute( + """\ CREATE TABLE percent_test ( `A%` INTEGER, - `B%` INTEGER)""") + `B%` INTEGER)""" + ) try: q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)" assert MySQLdb.cursors.RE_INSERT_VALUES.match(q) is not None cursor.executemany(q, [(3, 4), (5, 6)]) - assert cursor._executed.endswith(b"(3, 4),(5, 6)"), "executemany with %% not in one query" + assert cursor._executed.endswith( + b"(3, 4),(5, 6)" + ), "executemany with %% not in one query" finally: cursor.execute("DROP TABLE IF EXISTS percent_test") + + +def test_pyparam(): + conn = connect() + cursor = conn.cursor() + + cursor.execute("SELECT %(a)s, %(b)s", {"a": 1, "b": 2}) + assert cursor._executed == b"SELECT 1, 2" + cursor.execute(b"SELECT %(a)s, %(b)s", {b"a": 3, b"b": 4}) + assert cursor._executed == b"SELECT 3, 4" + + +def test_dictcursor(): + conn = connect() + cursor = conn.cursor(MySQLdb.cursors.DictCursor) + + cursor.execute("CREATE TABLE t1 (a int, b int, c int)") + _tables.append("t1") + cursor.execute("INSERT INTO t1 (a,b,c) VALUES (1,1,47), (2,2,47)") + + cursor.execute("CREATE TABLE t2 (b int, c int)") + _tables.append("t2") + cursor.execute("INSERT INTO t2 (b,c) VALUES (1,1), (2,2)") + + cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b") + rows = cursor.fetchall() + + assert len(rows) == 2 + assert rows[0] == {"a": 1, "b": 1, "c": 47, "t2.b": 1, "t2.c": 1} + assert rows[1] == {"a": 2, "b": 2, "c": 47, "t2.b": 2, "t2.c": 2} + + names1 = sorted(rows[0]) + names2 = sorted(rows[1]) + for a, b in zip(names1, names2): + assert a is b + + # Old fetchtype + cursor._fetch_type = 2 + cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b") + rows = cursor.fetchall() + + assert len(rows) == 2 + assert rows[0] == {"t1.a": 1, "t1.b": 1, "t1.c": 47, "t2.b": 1, "t2.c": 1} + assert rows[1] == {"t1.a": 2, "t1.b": 2, "t1.c": 47, "t2.b": 2, "t2.c": 2} + + names1 = sorted(rows[0]) + names2 = sorted(rows[1]) + for a, b in zip(names1, names2): + assert a is b + + +def test_mogrify_without_args(): + conn = connect() + cursor = conn.cursor() + + query = "SELECT VERSION()" + mogrified_query = cursor.mogrify(query) + cursor.execute(query) + + assert mogrified_query == query + assert mogrified_query == cursor._executed.decode() + + +def test_mogrify_with_tuple_args(): + conn = connect() + cursor = conn.cursor() + + query_with_args = "SELECT %s, %s", (1, 2) + mogrified_query = cursor.mogrify(*query_with_args) + cursor.execute(*query_with_args) + + assert mogrified_query == "SELECT 1, 2" + assert mogrified_query == cursor._executed.decode() + + +def test_mogrify_with_dict_args(): + conn = connect() + cursor = conn.cursor() + + query_with_args = "SELECT %(a)s, %(b)s", {"a": 1, "b": 2} + mogrified_query = cursor.mogrify(*query_with_args) + cursor.execute(*query_with_args) + + assert mogrified_query == "SELECT 1, 2" + assert mogrified_query == cursor._executed.decode() + + +# Test that cursor can be used without reading whole resultset. +@pytest.mark.parametrize("Cursor", [MySQLdb.cursors.Cursor, MySQLdb.cursors.SSCursor]) +def test_cursor_discard_result(Cursor): + conn = connect() + cursor = conn.cursor(Cursor) + + cursor.execute( + """\ +CREATE TABLE test_cursor_discard_result ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + data VARCHAR(100) +)""" + ) + _tables.append("test_cursor_discard_result") + + cursor.executemany( + "INSERT INTO test_cursor_discard_result (id, data) VALUES (%s, %s)", + [(i, f"row {i}") for i in range(1, 101)], + ) + + cursor.execute( + """\ +SELECT * FROM test_cursor_discard_result WHERE id <= 10; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 11 AND 20; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 21 AND 30; +""" + ) + cursor.nextset() + assert cursor.fetchone() == (11, "row 11") + + cursor.execute( + "SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 31 AND 40" + ) + assert cursor.fetchone() == (31, "row 31") + + +def test_binary_prefix(): + # https://github.com/PyMySQL/mysqlclient/issues/494 + conn = connect(binary_prefix=True) + cursor = conn.cursor() + + cursor.execute("DROP TABLE IF EXISTS test_binary_prefix") + cursor.execute( + """\ +CREATE TABLE test_binary_prefix ( + id INTEGER NOT NULL AUTO_INCREMENT, + json JSON NOT NULL, + PRIMARY KEY (id) +) CHARSET=utf8mb4""" + ) + + cursor.executemany( + "INSERT INTO test_binary_prefix (id, json) VALUES (%(id)s, %(json)s)", + ({"id": 1, "json": "{}"}, {"id": 2, "json": "{}"}), + ) diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 00000000..fae28e81 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,56 @@ +import pytest +import MySQLdb.cursors +from configdb import connection_factory + + +_conns = [] +_tables = [] + + +def connect(**kwargs): + conn = connection_factory(**kwargs) + _conns.append(conn) + return conn + + +def teardown_function(function): + if _tables: + c = _conns[0] + cur = c.cursor() + for t in _tables: + cur.execute(f"DROP TABLE {t}") + cur.close() + del _tables[:] + + for c in _conns: + c.close() + del _conns[:] + + +def test_null(): + """Inserting NULL into non NULLABLE column""" + # https://github.com/PyMySQL/mysqlclient/issues/535 + table_name = "test_null" + conn = connect() + cursor = conn.cursor() + + cursor.execute(f"create table {table_name} (c1 int primary key)") + _tables.append(table_name) + + with pytest.raises(MySQLdb.IntegrityError): + cursor.execute(f"insert into {table_name} values (null)") + + +def test_duplicated_pk(): + """Inserting row with duplicated PK""" + # https://github.com/PyMySQL/mysqlclient/issues/535 + table_name = "test_duplicated_pk" + conn = connect() + cursor = conn.cursor() + + cursor.execute(f"create table {table_name} (c1 int primary key)") + _tables.append(table_name) + + cursor.execute(f"insert into {table_name} values (1)") + with pytest.raises(MySQLdb.IntegrityError): + cursor.execute(f"insert into {table_name} values (1)") diff --git a/tests/travis.cnf b/tests/travis.cnf index 05ff8039..5fd6f847 100644 --- a/tests/travis.cnf +++ b/tests/travis.cnf @@ -7,5 +7,4 @@ host = 127.0.0.1 port = 3306 user = root database = mysqldb_test -#password = travis default-character-set = utf8mb4