├── .github ├── DISCUSSION_TEMPLATE │ ├── build-issue.yml │ └── issue-report.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── lint.yaml │ ├── tests.yaml │ └── windows.yaml ├── .gitignore ├── .readthedocs.yaml ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── ci ├── django-requirements.txt └── test_mysql.py ├── codecov.yml ├── doc ├── FAQ.rst ├── Makefile ├── MySQLdb.constants.rst ├── MySQLdb.rst ├── conf.py ├── index.rst ├── make.bat ├── requirements.txt └── user_guide.rst ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── setup.py ├── site.cfg ├── src └── MySQLdb │ ├── __init__.py │ ├── _exceptions.py │ ├── _mysql.c │ ├── connections.py │ ├── constants │ ├── CLIENT.py │ ├── CR.py │ ├── ER.py │ ├── FIELD_TYPE.py │ ├── FLAG.py │ └── __init__.py │ ├── converters.py │ ├── cursors.py │ ├── release.py │ └── times.py └── tests ├── actions.cnf ├── capabilities.py ├── configdb.py ├── dbapi20.py ├── default.cnf ├── test_MySQLdb_capabilities.py ├── test_MySQLdb_dbapi20.py ├── test_MySQLdb_nonstandard.py ├── test_MySQLdb_times.py ├── test__mysql.py ├── test_connection.py ├── test_cursor.py ├── test_errors.py └── travis.cnf /.github/DISCUSSION_TEMPLATE/build-issue.yml: -------------------------------------------------------------------------------- 1 | title: "[Build] " 2 | body: 3 | - type: input 4 | id: os 5 | attributes: 6 | label: What OS and which version do you use? 7 | description: | 8 | e.g. 9 | - Windows 11 10 | - macOS 13.4 11 | - Ubuntu 22.04 12 | 13 | - type: textarea 14 | id: libmysqlclient 15 | attributes: 16 | label: How did you installed mysql client library? 17 | description: | 18 | e.g. 19 | - `apt-get install libmysqlclient-dev` 20 | - `brew install mysql-client` 21 | - `brew install mysql` 22 | render: bash 23 | 24 | - type: textarea 25 | id: pkgconfig-output 26 | attributes: 27 | label: Output from `pkg-config --cflags --libs mysqlclient` 28 | description: If you are using mariadbclient, run `pkg-config --cflags --libs mariadb` instead. 29 | render: bash 30 | 31 | - type: input 32 | id: mysqlclient-install 33 | attributes: 34 | label: How did you tried to install mysqlclient? 35 | description: | 36 | e.g. 37 | - `pip install mysqlclient` 38 | - `poetry add mysqlclient` 39 | 40 | - type: textarea 41 | id: mysqlclient-error 42 | attributes: 43 | label: Output of building mysqlclient 44 | description: not only error message. full log from start installing mysqlclient. 45 | render: bash 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/issue-report.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: markdown 3 | attributes: 4 | value: | 5 | Failed to buid? [Use this form](https://github.com/PyMySQL/mysqlclient/discussions/new?category=build-issue). 6 | 7 | We don't use this issue tracker to help users. 8 | Please use this tracker only when you are sure about it is an issue of this software. 9 | 10 | If you had trouble, please ask it on some user community. 11 | 12 | - [Python Discord](https://www.pythondiscord.com/) 13 | For general Python questions, including developing application using MySQL. 14 | 15 | - [MySQL Community Slack](https://lefred.be/mysql-community-on-slack/) 16 | For general MySQL questions. 17 | 18 | - [mysqlclient Discuss](https://github.com/PyMySQL/mysqlclient/discussions) 19 | For mysqlclient specific topics. 20 | 21 | - type: textarea 22 | id: describe 23 | attributes: 24 | label: Describe the bug 25 | description: "A **clear and concise** description of what the bug is." 26 | 27 | - type: textarea 28 | id: environments 29 | attributes: 30 | label: Environment 31 | description: | 32 | - Server and version (e.g. MySQL 8.0.33, MariaDB 10.11.4) 33 | - OS (e.g. Windows 11, Ubuntu 22.04, macOS 13.4.1) 34 | - Python version 35 | 36 | - type: input 37 | id: libmysqlclient 38 | attributes: 39 | label: How did you install libmysqlclient libraries? 40 | description: | 41 | e.g. brew install mysql-cleint, brew install mariadb, apt-get install libmysqlclient-dev 42 | 43 | - type: input 44 | id: mysqlclient-version 45 | attributes: 46 | label: What version of mysqlclient do you use? 47 | 48 | - type: markdown 49 | attributes: 50 | value: | 51 | ## Complete step to reproduce. 52 | # 53 | Do not expect maintainer complement any piece of code, schema, and data need to reproduce. 54 | You need to provide **COMPLETE** step to reproduce. 55 | 56 | It is very recommended to use Docker to start MySQL server. 57 | Maintainer can not use your Database to reproduce your issue. 58 | 59 | **If you write only little code snippet, maintainer may close your issue 60 | without any comment.** 61 | 62 | - type: textarea 63 | id: reproduce-docker 64 | attributes: 65 | label: Docker command to start MySQL server 66 | render: bash 67 | description: e.g. `docker run -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 --rm --name mysql mysql:8.0` 68 | 69 | - type: textarea 70 | id: reproduce-code 71 | attributes: 72 | label: Minimum but complete code to reproduce 73 | render: python 74 | value: | 75 | # Write Python code here. 76 | import MySQLdb 77 | 78 | conn = MySQLdb.connect(host='127.0.0.1', port=3306, user='root') 79 | ... 80 | 81 | - type: textarea 82 | id: reproduce-schema 83 | attributes: 84 | label: Schema and initial data required to reproduce. 85 | render: sql 86 | value: | 87 | -- Write SQL here. 88 | -- e.g. CREATE TABLE ... 89 | 90 | - type: textarea 91 | id: reproduce-other 92 | attributes: 93 | label: Commands, and any other step required to reproduce your issue. 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Failed to build 3 | about: Ask help for build error. 4 | url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=build-issue" 5 | 6 | - name: Report issue 7 | about: Found bug? 8 | url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=issue-report" 9 | 10 | - name: Ask question 11 | about: Ask other questions. 12 | url: "https://github.com/PyMySQL/mysqlclient/discussions/new?category=q-a" 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - run: pipx install ruff 14 | - run: ruff check src/ 15 | - run: ruff format src/ 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | env: 12 | PIP_NO_PYTHON_VERSION_WARNING: 1 13 | PIP_DISABLE_PIP_VERSION_CHECK: 1 14 | strategy: 15 | matrix: 16 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 17 | include: 18 | - python-version: "3.12" 19 | mariadb: 1 20 | steps: 21 | - if: ${{ matrix.mariadb }} 22 | name: Start MariaDB 23 | # https://github.com/actions/runner-images/blob/9d9b3a110dfc98100cdd09cb2c957b9a768e2979/images/linux/scripts/installers/mysql.sh#L10-L13 24 | run: | 25 | docker pull mariadb:10.11 26 | docker run -d -e MARIADB_ROOT_PASSWORD=root -p 3306:3306 --rm --name mariadb mariadb:10.11 27 | sudo apt-get -y install libmariadb-dev 28 | mysql --version 29 | mysql -uroot -proot -h127.0.0.1 -e "CREATE DATABASE mysqldb_test" 30 | 31 | - if: ${{ !matrix.mariadb }} 32 | name: Start MySQL 33 | run: | 34 | sudo systemctl start mysql.service 35 | mysql --version 36 | mysql -uroot -proot -e "CREATE DATABASE mysqldb_test" 37 | 38 | - uses: actions/checkout@v4 39 | 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v5 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | cache: "pip" 45 | cache-dependency-path: "requirements.txt" 46 | allow-prereleases: true 47 | 48 | - name: Install mysqlclient 49 | run: | 50 | pip install -v . 51 | 52 | - name: Install test dependencies 53 | run: | 54 | pip install -r requirements.txt 55 | 56 | - name: Run tests 57 | env: 58 | TESTDB: actions.cnf 59 | run: | 60 | pytest --cov=MySQLdb tests 61 | 62 | - uses: codecov/codecov-action@v5 63 | 64 | django-test: 65 | name: "Run Django LTS test suite" 66 | needs: test 67 | runs-on: ubuntu-latest 68 | env: 69 | PIP_NO_PYTHON_VERSION_WARNING: 1 70 | PIP_DISABLE_PIP_VERSION_CHECK: 1 71 | DJANGO_VERSION: "4.2.16" 72 | steps: 73 | - name: Start MySQL 74 | run: | 75 | sudo systemctl start mysql.service 76 | mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql 77 | mysql -uroot -proot -e "set global innodb_flush_log_at_trx_commit=0;" 78 | mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" 79 | mysql -uroot -proot -e "CREATE DATABASE django_default; CREATE DATABASE django_other;" 80 | 81 | - uses: actions/checkout@v4 82 | 83 | - name: Set up Python 84 | uses: actions/setup-python@v5 85 | with: 86 | python-version: "3.12" 87 | cache: "pip" 88 | cache-dependency-path: "ci/django-requirements.txt" 89 | 90 | - name: Install mysqlclient 91 | run: | 92 | #pip install -r requirements.txt 93 | #pip install mysqlclient # Use stable version 94 | pip install . 95 | 96 | - name: Setup Django 97 | run: | 98 | sudo apt-get install libmemcached-dev 99 | wget https://github.com/django/django/archive/${DJANGO_VERSION}.tar.gz 100 | tar xf ${DJANGO_VERSION}.tar.gz 101 | cp ci/test_mysql.py django-${DJANGO_VERSION}/tests/ 102 | cd django-${DJANGO_VERSION} 103 | pip install . -r tests/requirements/py3.txt 104 | 105 | - name: Run Django test 106 | run: | 107 | cd django-${DJANGO_VERSION}/tests/ 108 | PYTHONPATH=.. python3 ./runtests.py --settings=test_mysql 109 | -------------------------------------------------------------------------------- /.github/workflows/windows.yaml: -------------------------------------------------------------------------------- 1 | name: Build windows wheels 2 | 3 | on: 4 | push: 5 | branches: ["main", "ci"] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | env: 13 | CONNECTOR_VERSION: "3.4.1" 14 | steps: 15 | - name: Cache Connector 16 | id: cache-connector 17 | uses: actions/cache@v4 18 | with: 19 | path: c:/mariadb-connector 20 | key: mariadb-connector-c-${{ env.CONNECTOR_VERSION }}-win-2 21 | 22 | - name: Download and Unzip Connector 23 | if: steps.cache-connector.outputs.cache-hit != 'true' 24 | shell: bash 25 | run: | 26 | curl -LO "https://downloads.mariadb.com/Connectors/c/connector-c-${CONNECTOR_VERSION}/mariadb-connector-c-${CONNECTOR_VERSION}-src.zip" 27 | unzip "mariadb-connector-c-${CONNECTOR_VERSION}-src.zip" -d c:/ 28 | mv "c:/mariadb-connector-c-${CONNECTOR_VERSION}-src" c:/mariadb-connector-src 29 | 30 | - name: make build directory 31 | if: steps.cache-connector.outputs.cache-hit != 'true' 32 | shell: cmd 33 | working-directory: c:/mariadb-connector-src 34 | run: | 35 | mkdir build 36 | 37 | - name: cmake 38 | if: steps.cache-connector.outputs.cache-hit != 'true' 39 | shell: cmd 40 | working-directory: c:/mariadb-connector-src/build 41 | run: | 42 | 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 43 | 44 | - name: cmake build 45 | if: steps.cache-connector.outputs.cache-hit != 'true' 46 | shell: cmd 47 | working-directory: c:/mariadb-connector-src/build 48 | run: | 49 | cmake --build . -j 8 --config Release 50 | 51 | - name: cmake install 52 | if: steps.cache-connector.outputs.cache-hit != 'true' 53 | shell: cmd 54 | working-directory: c:/mariadb-connector-src/build 55 | run: | 56 | cmake -DCMAKE_INSTALL_PREFIX=c:/mariadb-connector -DCMAKE_INSTALL_COMPONENT=Development -DCMAKE_BUILD_TYPE=Release -P cmake_install.cmake 57 | 58 | - name: Checkout mysqlclient 59 | uses: actions/checkout@v4 60 | with: 61 | path: mysqlclient 62 | 63 | - name: Site Config 64 | shell: bash 65 | working-directory: mysqlclient 66 | run: | 67 | pwd 68 | find . 69 | cat <site.cfg 70 | [options] 71 | static = True 72 | connector = C:/mariadb-connector 73 | EOF 74 | cat site.cfg 75 | 76 | - uses: actions/setup-python@v5 77 | - name: Install cibuildwheel 78 | run: python -m pip install cibuildwheel 79 | - name: Build wheels 80 | working-directory: mysqlclient 81 | env: 82 | CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" 83 | CIBW_ARCHS: "AMD64" 84 | CIBW_TEST_COMMAND: 'python -c "import MySQLdb; print(MySQLdb.version_info)" ' 85 | run: "python -m cibuildwheel --prerelease-pythons --output-dir dist" 86 | 87 | - name: Build sdist 88 | working-directory: mysqlclient 89 | run: | 90 | python -m pip install build 91 | python -m build -s -o dist 92 | 93 | - name: Upload Wheel 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: win-wheels 97 | path: mysqlclient/dist/*.* 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.o 4 | *.so 5 | *.pyd 6 | *.gz 7 | *.zip 8 | *.egg 9 | *.egg-info/ 10 | .tox/ 11 | build/ 12 | dist/ 13 | .coverage 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: doc/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.13" 10 | 11 | apt_packages: 12 | - default-libmysqlclient-dev 13 | - build-essential 14 | 15 | python: 16 | install: 17 | - requirements: doc/requirements.txt 18 | 19 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | What's new in 2.2.7 3 | ====================== 4 | 5 | Release: 2025-01-10 6 | 7 | * Add ``user``, ``host``, ``database``, and ``db`` attributes to ``Connection``. 8 | opentelemetry-instrumentation-(dbapi|mysqlclient) use them. (#753) 9 | 10 | ====================== 11 | What's new in 2.2.6 12 | ====================== 13 | 14 | Release: 2024-11-12 15 | 16 | * MariaDB Connector/C 3.4 and MairaDB 11.4 enabled SSL and CA verification by default. 17 | It affected 2.2.5 windows wheel. This release disables SSL and CA verification by default. (#731) 18 | 19 | * Add ``server_public_key_path`` option. It is needed to connect MySQL server with 20 | ``sha256_password`` or ``caching_sha2_password`` authentication plugin without 21 | secure connection. (#744) 22 | 23 | ====================== 24 | What's new in 2.2.5 25 | ====================== 26 | 27 | Release: 2024-10-20 28 | 29 | * (Windows wheel) Update MariaDB Connector/C to 3.4.1. #726 30 | * (Windows wheel) Build wheels for Python 3.13. #726 31 | 32 | ====================== 33 | What's new in 2.2.4 34 | ====================== 35 | 36 | Release: 2024-02-09 37 | 38 | * Support ``ssl=True`` in ``connect()``. (#700) 39 | This makes better compatibility with PyMySQL and mysqlclient==2.2.1 40 | with libmariadb. See #698 for detail. 41 | 42 | 43 | ====================== 44 | What's new in 2.2.3 45 | ====================== 46 | 47 | Release: 2024-02-04 48 | 49 | * Fix ``Connection.kill()`` method that broken in 2.2.2. (#689) 50 | 51 | 52 | ====================== 53 | What's new in 2.2.2 54 | ====================== 55 | 56 | Release: 2024-02-04 57 | 58 | * Support building with MySQL 8.3 (#688). 59 | * Deprecate ``db.shutdown()`` and ``db.kill()`` methods in docstring. 60 | This is because ``mysql_shutdown()`` and ``mysql_kill()`` were removed in MySQL 8.3. 61 | They will emit DeprecationWarning in the future but not for now. 62 | 63 | 64 | ====================== 65 | What's new in 2.2.1 66 | ====================== 67 | 68 | Release: 2023-12-13 69 | 70 | * ``Connection.ping()`` avoid using ``MYSQL_OPT_RECONNECT`` option until 71 | ``reconnect=True`` is specified. MySQL 8.0.33 start showing warning 72 | when the option is used. (#664) 73 | * Windows: Update MariaDB Connector/C to 3.3.8. (#665) 74 | * Windows: Build wheels for Python 3.12 (#644) 75 | 76 | 77 | ====================== 78 | What's new in 2.2.0 79 | ====================== 80 | 81 | Release: 2023-06-22 82 | 83 | * Use ``pkg-config`` instead of ``mysql_config`` (#586) 84 | * Raise ProgrammingError on -inf (#557) 85 | * Raise IntegrityError for ER_BAD_NULL. (#579) 86 | * Windows: Use MariaDB Connector/C 3.3.4 (#585) 87 | * Use pkg-config instead of mysql_config (#586) 88 | * Add collation option (#564) 89 | * Drop Python 3.7 support (#593) 90 | * Use pyproject.toml for build (#598) 91 | * Add Cursor.mogrify (#477) 92 | * Partial support of ssl_mode option with mariadbclient (#475) 93 | * Discard remaining results without creating Python objects (#601) 94 | * Fix executemany with binary prefix (#605) 95 | 96 | ====================== 97 | What's new in 2.1.1 98 | ====================== 99 | 100 | Release: 2022-06-22 101 | 102 | * Fix qualname of exception classes. (#522) 103 | * Fix range check in ``MySQLdb._mysql.result.fetch_row()``. Invalid ``how`` argument caused SEGV. (#538) 104 | * Fix docstring of ``_mysql.connect``. (#540) 105 | * Windows: Binary wheels are updated. (#541) 106 | * Use MariaDB Connector/C 3.3.1. 107 | * Use cibuildwheel to build wheels. 108 | * Python 3.8-3.11 109 | 110 | ====================== 111 | What's new in 2.1.0 112 | ====================== 113 | 114 | Release: 2021-11-17 115 | 116 | * Add ``multistatement=True`` option. You can disable multi statement. (#500). 117 | * Remove unnecessary bytes encoder which is remained for Django 1.11 118 | compatibility (#490). 119 | * Deprecate ``passwd`` and ``db`` keyword. Use ``password`` and ``database`` 120 | instead. (#488). 121 | * Windows: Binary wheels are built with MariaDB Connector/C 3.2.4. (#508) 122 | * ``set_character_set()`` sends ``SET NAMES`` query always. This means 123 | all new connections send it too. This solves compatibility issues 124 | when server and client library are different version. (#509) 125 | * Remove ``escape()`` and ``escape_string()`` from ``MySQLdb`` package. 126 | (#511) 127 | * Add Python 3.10 support and drop Python 3.5 support. 128 | 129 | ====================== 130 | What's new in 2.0.3 131 | ====================== 132 | 133 | Release: 2021-01-01 134 | 135 | * Add ``-std=c99`` option to cflags by default for ancient compilers that doesn't 136 | accept C99 by default. 137 | * You can customize cflags and ldflags by setting ``MYSQLCLIENT_CFLAGS`` and 138 | ``MYSQLCLIENT_LDFLAGS``. It overrides ``mysql_config``. 139 | 140 | ====================== 141 | What's new in 2.0.2 142 | ====================== 143 | 144 | Release: 2020-12-10 145 | 146 | * Windows: Update MariaDB Connector/C to 3.1.11. 147 | * Optimize fetching many rows with DictCursor. 148 | 149 | ====================== 150 | What's new in 2.0.1 151 | ====================== 152 | 153 | Release: 2020-07-03 154 | 155 | * Fixed multithread safety issue in fetching row. 156 | * Removed obsolete members from Cursor. (e.g. `messages`, `_warnings`, `_last_executed`) 157 | 158 | ====================== 159 | What's new in 2.0.0 160 | ====================== 161 | 162 | Release: 2020-07-02 163 | 164 | * Dropped Python 2 support 165 | * Dropped Django 1.11 support 166 | * Add context manager interface to Connection which closes the connection on ``__exit__``. 167 | * Add ``ssl_mode`` option. 168 | 169 | 170 | ====================== 171 | What's new in 1.4.6 172 | ====================== 173 | 174 | Release: 2019-11-21 175 | 176 | * The ``cp1252`` encoding is used when charset is "latin1". (#390) 177 | 178 | ====================== 179 | What's new in 1.4.5 180 | ====================== 181 | 182 | Release: 2019-11-06 183 | 184 | * The ``auth_plugin`` option is added. (#389) 185 | 186 | 187 | ====================== 188 | What's new in 1.4.4 189 | ====================== 190 | 191 | Release: 2019-08-12 192 | 193 | * ``charset`` option is passed to ``mysql_options(mysql, MYSQL_SET_CHARSET_NAME, charset)`` 194 | before ``mysql_real_connect`` is called. 195 | This avoid extra ``SET NAMES `` query when creating connection. 196 | 197 | 198 | ====================== 199 | What's new in 1.4.3 200 | ====================== 201 | 202 | Release: 2019-08-09 203 | 204 | * ``--static`` build supports ``libmariadbclient.a`` 205 | * Try ``mariadb_config`` when ``mysql_config`` is not found 206 | * Fixed warning happened in Python 3.8 (#359) 207 | * Fixed ``from MySQLdb import *``, while I don't recommend it. (#369) 208 | * Fixed SEGV ``MySQLdb.escape_string("1")`` when libmariadb is used and 209 | no connection is created. (#367) 210 | * Fixed many circular references are created in ``Cursor.executemany()``. (#375) 211 | 212 | 213 | ====================== 214 | What's new in 1.4.2 215 | ====================== 216 | 217 | Release: 2019-02-08 218 | 219 | * Fix Django 1.11 compatibility. (#327) 220 | mysqlclient 1.5 will not support Django 1.11. It is not because 221 | mysqlclient will break backward compatibility, but Django used 222 | unsupported APIs and Django 1.11 don't fix bugs including 223 | compatibility issues. 224 | 225 | ====================== 226 | What's new in 1.4.1 227 | ====================== 228 | 229 | Release: 2019-01-19 230 | 231 | * Fix dict parameter support (#323, regression of 1.4.0) 232 | 233 | ====================== 234 | What's new in 1.4.0 235 | ====================== 236 | 237 | Release: 2019-01-18 238 | 239 | * Dropped Python 3.4 support. 240 | 241 | * Removed ``threadsafe`` and ``embedded`` build options. 242 | 243 | * Remove some deprecated cursor classes and methods. 244 | 245 | * ``_mysql`` and ``_mysql_exceptions`` modules are moved under 246 | ``MySQLdb`` package. (#293) 247 | 248 | * Remove ``errorhandler`` from Connection and Cursor classes. 249 | 250 | * Remove context manager API from Connection. It was for transaction. 251 | New context manager API for closing connection will be added in future version. 252 | 253 | * Remove ``waiter`` option from Connection. 254 | 255 | * Remove ``escape_sequence``, and ``escape_dict`` methods from Connection class. 256 | 257 | * Remove automatic MySQL warning checking. 258 | 259 | * Drop support for MySQL Connector/C with MySQL<5.1.12. 260 | 261 | * Remove ``_mysql.NULL`` constant. 262 | 263 | * Remove ``_mysql.thread_safe()`` function. 264 | 265 | * Support non-ASCII field name with non-UTF-8 connection encoding. (#210) 266 | 267 | * Optimize decoding speed of string and integer types. 268 | 269 | * Remove ``MySQLdb.constants.REFRESH`` module. 270 | 271 | * Remove support for old datetime format for MySQL < 4.1. 272 | 273 | * Fix wrong errno is raised when ``mysql_real_connect`` is failed. (#316) 274 | 275 | 276 | ====================== 277 | What's new in 1.3.14 278 | ====================== 279 | 280 | Release: 2018-12-04 281 | 282 | * Support static linking of MariaDB Connector/C (#265) 283 | 284 | * Better converter for Decimal and Float (#267, #268, #273, #286) 285 | 286 | * Add ``Connection._get_native_connection`` for XTA project (#269) 287 | 288 | * Fix SEGV on MariaDB Connector/C when some methods of ``Connection`` 289 | objects are called after ``Connection.close()`` is called. (#270, #272, #276) 290 | See https://jira.mariadb.org/browse/CONC-289 291 | 292 | * Fix ``Connection.client_flag`` (#266) 293 | 294 | * Fix SSCursor may raise same exception twice (#282) 295 | 296 | * This removed ``Cursor._last_executed`` which was duplicate of ``Cursor._executed``. 297 | Both members are private. So this type of changes are not documented in changelog 298 | generally. But Django used the private member for ``last_executed_query`` implementation. 299 | If you use the method the method directly or indirectly, this version will break 300 | your application. See https://code.djangoproject.com/ticket/30013 301 | 302 | * ``waiter`` option is now deprecated. (#285) 303 | 304 | * Fixed SSL support is not detected when built with MySQL < 5.1 (#291) 305 | 306 | 307 | ====================== 308 | What's new in 1.3.13 309 | ====================== 310 | 311 | Support build with MySQL 8 312 | 313 | Fix decoding tiny/medium/long blobs (#215) 314 | 315 | Remove broken row_seek() and row_tell() APIs (#220) 316 | 317 | Reduce callproc roundtrip time (#223) 318 | 319 | 320 | ====================== 321 | What's new in 1.3.12 322 | ====================== 323 | 324 | Fix tuple argument again (#201) 325 | 326 | InterfaceError is raised when Connection.query() is called for closed connection (#202) 327 | 328 | ====================== 329 | What's new in 1.3.11 330 | ====================== 331 | 332 | Support MariaDB 10.2 client library (#197, #177, #200) 333 | 334 | Add NEWDECIMAL to the NUMBER DBAPISet (#167) 335 | 336 | Allow bulk insert which no space around `VALUES` (#179) 337 | 338 | Fix leak of `connection->converter`. (#182) 339 | 340 | Support error `numbers > CR_MAX_ERROR` (#188) 341 | 342 | Fix tuple argument support (#145) 343 | 344 | 345 | ====================== 346 | What's new in 1.3.10 347 | ====================== 348 | 349 | Added `binary_prefix` option (disabled by default) to support 350 | `_binary` prefix again. (#134) 351 | 352 | Fix SEGV of `_mysql.result()` when argument's type is unexpected. (#138) 353 | 354 | Deprecate context interface of Connection object. (#149) 355 | 356 | Don't use workaround of `bytes.decode('ascii', 'surrogateescape')` on Python 3.6+. (#150) 357 | 358 | 359 | ===================== 360 | What's new in 1.3.9 361 | ===================== 362 | 363 | Revert adding `_binary` prefix for bytes/bytearray parameter. It broke backward compatibility. 364 | 365 | Fix Windows compile error on MSVC. 366 | 367 | 368 | ===================== 369 | What's new in 1.3.8 370 | ===================== 371 | 372 | Update error constants (#113) 373 | 374 | Use `_binary` prefix for bytes/bytearray parameters (#106) 375 | 376 | Use mysql_real_escape_string_quote() if exists (#109) 377 | 378 | Better Warning propagation (#101) 379 | 380 | Fix conversion error when mysql_affected_rows returns -1 381 | 382 | Fix Cursor.callproc may raise TypeError (#90, #91) 383 | 384 | connect() supports the 'database' and 'password' keyword arguments. 385 | 386 | Fix accessing dangling pointer when using ssl (#78) 387 | 388 | Accept %% in Cursor.executemany (#83) 389 | 390 | Fix warning that caused TypeError on Python 3 (#68) 391 | 392 | ===================== 393 | What's new in 1.3.7 394 | ===================== 395 | 396 | Support link args other than '-L' and '-l' from mysql_config. 397 | 398 | Missing value for column without default value cause IntegrityError. (#33) 399 | 400 | Support BIT type. (#38) 401 | 402 | More tests for date and time columns. (#41) 403 | 404 | Fix calling .execute() method for closed cursor cause TypeError. (#37) 405 | 406 | Improve performance to parse date. (#43) 407 | 408 | Support geometry types (#49) 409 | 410 | Fix warning while multi statement cause ProgrammingError. (#48) 411 | 412 | 413 | ===================== 414 | What's new in 1.3.6 415 | ===================== 416 | 417 | Fix escape_string() doesn't work. 418 | 419 | Remove `Cursor.__del__` to fix uncollectable circular reference on Python 3.3. 420 | 421 | Add context manager support to `Cursor`. It automatically closes cursor on `__exit__`. 422 | 423 | .. code-block:: 424 | 425 | with conn.cursor() as cur: 426 | cur.execute("SELECT 1+1") 427 | print(cur.fetchone()) 428 | # cur is now closed 429 | 430 | 431 | ===================== 432 | What's new in 1.3.5 433 | ===================== 434 | 435 | Fix TINYBLOB, MEDIUMBLOB and LONGBLOB are treated as string and decoded 436 | to unicode or cause UnicodeError. 437 | 438 | Fix aware datetime is formatted with timezone offset (e.g. "+0900"). 439 | 440 | 441 | ===================== 442 | What's new in 1.3.4 443 | ===================== 444 | 445 | * Remove compiler warnings. 446 | * Fix compile error when using libmariadbclient. 447 | * Fix GIL deadlock while dealloc. 448 | 449 | ===================== 450 | What's new in 1.3.3 451 | ===================== 452 | 453 | * Fix exception reraising doesn't work. 454 | 455 | ===================== 456 | What's new in 1.3.2 457 | ===================== 458 | 459 | * Add send_query() and read_query_result() method to low level connection. 460 | * Add waiter option. 461 | 462 | 463 | ===================== 464 | What's new in 1.3.1 465 | ===================== 466 | 467 | This is a first fork of MySQL-python. 468 | Now named "mysqlclient" 469 | 470 | * Support Python 3 471 | * Add autocommit option 472 | * Support microsecond in datetime field. 473 | 474 | 475 | ===================== 476 | What's new in 1.2.4 477 | ===================== 478 | 479 | final 480 | ===== 481 | 482 | No changes. 483 | 484 | 485 | rc 1 486 | ==== 487 | 488 | Fixed a dangling reference to the old types module. 489 | 490 | 491 | beta 5 492 | ====== 493 | 494 | Another internal fix for handling remapped character sets. 495 | 496 | `_mysql.c` was broken for the case where read_timeout was *not* available. (Issue #6) 497 | 498 | Documentation was converted to sphinx but there is a lot of cleanup left to do. 499 | 500 | 501 | beta 4 502 | ====== 503 | 504 | Added support for the MySQL read_timeout option. Contributed by 505 | Jean Schurger (jean@schurger.org). 506 | 507 | Added a workaround so that the MySQL character set utf8mb4 works with Python; utf8 is substituted 508 | on the Python side. 509 | 510 | 511 | beta 3 512 | ====== 513 | 514 | Unified test database configuration, and set up CI testing with Travis. 515 | 516 | Applied several patches from André Malo (ndparker@users.sf.net) which fix some issues 517 | with exception handling and reference counting and TEXT/BLOB conversion. 518 | 519 | 520 | beta 2 521 | ====== 522 | 523 | Reverted an accidental change in the exception format. (issue #1) 524 | 525 | Reverted some raise statements so that they will continue to work with Python < 2.6 526 | 527 | 528 | beta 1 529 | ====== 530 | 531 | A lot of work has been done towards Python 3 compatibility, and avoiding warnings with Python 2.7. 532 | This includes import changes, converting dict.has_kay(k) to k in dict, updating some test suite methods, etc. 533 | 534 | Due to the difficulties of supporting Python 3 and Python < 2.7, 1.2.4 will support Python 2.4 though 2.7. 535 | 1.3.0 will support Python 3 and Python 2.7 and 2.6. 536 | 537 | MySQLdb-2.0 is instead going to become moist-1.0. See https://github.com/farcepest/moist 538 | 539 | The Windows build has been simplified, and I plan to correct pre-built i386 packages built 540 | against the python.org Python-2.7 package and MySQL Connector/C-6.0. Contact me if you 541 | need ia64 packages. 542 | 543 | The connection's cursorclass (if not default) was being lost on reconnect. 544 | 545 | Newer versions of MySQL don't use OpenSSL and therefore don't have HAVE_SSL defined, but they do have 546 | a different SSL library. Fixed this so SSL support would be enabled in this case. 547 | 548 | The regex that looked for SQL INSERT statement and VALUES in cursor.executemany() was made case-insensitive 549 | again. 550 | 551 | 552 | ===================== 553 | What's new in 1.2.3 554 | ===================== 555 | 556 | ez_setup.py has been update to include various fixes that affect the build. 557 | 558 | Better Python version and dependency detection as well as eliminate exception 559 | warnings under Python 2.6. 560 | 561 | Eliminated memory leaks related to Unicode and failed connections. 562 | 563 | Corrected connection .escape() functionality. 564 | 565 | Miscellaneous cleanups and and expanded testing suite to ensure ongoing release 566 | quality. 567 | 568 | ===================== 569 | What's new in 1.2.2 570 | ===================== 571 | 572 | The build system has been completely redone and should now build 573 | on Windows without any patching; uses setuptools. 574 | 575 | Added compatibility for Python 2.5, including support for with statement. 576 | 577 | connection.ping() now takes an optional boolean argument which can 578 | enable (or disable) automatic reconnection. 579 | 580 | Support returning SET columns as Python sets was removed due to an 581 | API bug in MySQL; corresponding test removed. 582 | 583 | Added a test for single-character CHAR columns. 584 | 585 | BLOB columns are now returned as Python strings instead of byte arrays. 586 | 587 | BINARY character columns are always returned as Python strings, and not 588 | unicode. 589 | 590 | Fixed a bug introduced in 1.2.1 where the new SHOW WARNINGS support broke 591 | SSCursor. 592 | 593 | Only encode the query (convert to a string) when it is a unicode instance; 594 | re-encoding encoded strings would break things. 595 | 596 | Make a deep copy of conv when connecting, since it can be modified. 597 | 598 | Added support for new VARCHAR and BIT column types. 599 | 600 | DBAPISet objects were broken, but nobody noticed. 601 | 602 | 603 | ======================== 604 | What's new in 1.2.1_p2 605 | ======================== 606 | 607 | There are some minor build fixes which probably only affect MySQL 608 | older than 4.0. 609 | 610 | If you had MySQL older than 4.1, the new charset and sql_mode 611 | parameters didn't work right. In fact, it was impossible to create 612 | a connection due to the charset problem. 613 | 614 | If you are using MySQL-4.1 or newer, there is no practical difference 615 | between 1.2.1 and 1.2.1_p2, and you don't need to upgrade. 616 | 617 | 618 | ===================== 619 | What's new in 1.2.1 620 | ===================== 621 | 622 | Switched to Subversion. Was going to do this for 1.3, but a 623 | SourceForge CVS outage has forced the issue. 624 | 625 | Mapped a lot of new 4.1 and 5.0 error codes to Python exceptions 626 | 627 | Added an API call for mysql_set_character_set(charset) (MySQL > 5.0.7) 628 | 629 | Added an API call for mysql_get_character_set_info() (MySQL > 5.0.10) 630 | 631 | Revamped the build system. Edit site.cfg if necessary (probably not 632 | in most cases) 633 | 634 | Python-2.3 is now the minimum version. 635 | 636 | Dropped support for mx.Datetime and stringtimes; always uses Python 637 | datetime module now. 638 | 639 | Improved unit tests 640 | 641 | New connect() options: 642 | * charset: sets character set, implies use_unicode 643 | * sql_mode: sets SQL mode (i.e. ANSI, etc.; see MySQL docs) 644 | 645 | When using MySQL-4.1 or newer, enables MULTI_STATEMENTS 646 | 647 | When using MySQL-5.0 or newer, enables MULTI_RESULTS 648 | 649 | When using MySQL-4.1 or newer, more detailed warning messages 650 | are produced 651 | 652 | SET columns returned as Python Set types; you can pass a Set as 653 | a parameter to cursor.execute(). 654 | 655 | Support for the new MySQL-5.0 DECIMAL implementation 656 | 657 | Support for Python Decimal type 658 | 659 | Some use of weak references internally. Cursors no longer leak 660 | if you don't close them. Connections still do, unfortunately. 661 | 662 | ursor.fetchXXXDict() methods raise DeprecationWarning 663 | 664 | cursor.begin() is making a brief reappearence. 665 | 666 | cursor.callproc() now works, with some limitations. 667 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include doc *.rst 2 | recursive-include tests *.py 3 | include doc/conf.py 4 | include HISTORY.rst 5 | include README.md 6 | include LICENSE 7 | include site.cfg 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | python setup.py build_ext -if 4 | 5 | .PHONY: doc 6 | doc: 7 | pip install . 8 | pip install sphinx 9 | cd doc && make html 10 | 11 | .PHONY: clean 12 | clean: 13 | find . -name '*.pyc' -delete 14 | find . -name '__pycache__' -delete 15 | rm -rf build 16 | 17 | .PHONY: check 18 | check: 19 | ruff *.py src ci 20 | black *.py src ci 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysqlclient 2 | 3 | This project is a fork of [MySQLdb1](https://github.com/farcepest/MySQLdb1). 4 | This project adds Python 3 support and fixed many bugs. 5 | 6 | * PyPI: https://pypi.org/project/mysqlclient/ 7 | * GitHub: https://github.com/PyMySQL/mysqlclient 8 | 9 | 10 | ## Support 11 | 12 | **Do Not use Github Issue Tracker to ask help. OSS Maintainer is not free tech support** 13 | 14 | When your question looks relating to Python rather than MySQL/MariaDB: 15 | 16 | * Python mailing list [python-list](https://mail.python.org/mailman/listinfo/python-list) 17 | * Slack [pythondev.slack.com](https://pyslackers.com/web/slack) 18 | 19 | Or when you have question about MySQL/MariaDB: 20 | 21 | * [MySQL Support](https://dev.mysql.com/support/) 22 | * [Getting Help With MariaDB](https://mariadb.com/kb/en/getting-help-with-mariadb/) 23 | 24 | 25 | ## Install 26 | 27 | ### Windows 28 | 29 | Building mysqlclient on Windows is very hard. 30 | But there are some binary wheels you can install easily. 31 | 32 | If binary wheels do not exist for your version of Python, it may be possible to 33 | build from source, but if this does not work, **do not come asking for support.** 34 | To build from source, download the 35 | [MariaDB C Connector](https://mariadb.com/downloads/#connectors) and install 36 | it. It must be installed in the default location 37 | (usually "C:\Program Files\MariaDB\MariaDB Connector C" or 38 | "C:\Program Files (x86)\MariaDB\MariaDB Connector C" for 32-bit). If you 39 | build the connector yourself or install it in a different location, set the 40 | environment variable `MYSQLCLIENT_CONNECTOR` before installing. Once you have 41 | the connector installed and an appropriate version of Visual Studio for your 42 | version of Python: 43 | 44 | ``` 45 | $ pip install mysqlclient 46 | ``` 47 | 48 | ### macOS (Homebrew) 49 | 50 | Install MySQL and mysqlclient: 51 | 52 | ```bash 53 | $ # Assume you are activating Python 3 venv 54 | $ brew install mysql pkg-config 55 | $ pip install mysqlclient 56 | ``` 57 | 58 | If you don't want to install MySQL server, you can use mysql-client instead: 59 | 60 | ```bash 61 | $ # Assume you are activating Python 3 venv 62 | $ brew install mysql-client pkg-config 63 | $ export PKG_CONFIG_PATH="$(brew --prefix)/opt/mysql-client/lib/pkgconfig" 64 | $ pip install mysqlclient 65 | ``` 66 | 67 | ### Linux 68 | 69 | **Note that this is a basic step. I can not support complete step for build for all 70 | environment. If you can see some error, you should fix it by yourself, or ask for 71 | support in some user forum. Don't file a issue on the issue tracker.** 72 | 73 | You may need to install the Python 3 and MySQL development headers and libraries like so: 74 | 75 | * `$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pkg-config` # Debian / Ubuntu 76 | * `% sudo yum install python3-devel mysql-devel pkgconfig` # Red Hat / CentOS 77 | 78 | Then you can install mysqlclient via pip now: 79 | 80 | ``` 81 | $ pip install mysqlclient 82 | ``` 83 | 84 | ### Customize build (POSIX) 85 | 86 | mysqlclient uses `pkg-config --cflags --ldflags mysqlclient` by default for finding 87 | compiler/linker flags. 88 | 89 | You can use `MYSQLCLIENT_CFLAGS` and `MYSQLCLIENT_LDFLAGS` environment 90 | variables to customize compiler/linker options. 91 | 92 | ```bash 93 | $ export MYSQLCLIENT_CFLAGS=`pkg-config mysqlclient --cflags` 94 | $ export MYSQLCLIENT_LDFLAGS=`pkg-config mysqlclient --libs` 95 | $ pip install mysqlclient 96 | ``` 97 | 98 | ### Documentation 99 | 100 | Documentation is hosted on [Read The Docs](https://mysqlclient.readthedocs.io/) 101 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. -------------------------------------------------------------------------------- /ci/django-requirements.txt: -------------------------------------------------------------------------------- 1 | # django-3.2.19/tests/requirements/py3.txt 2 | aiosmtpd 3 | asgiref >= 3.3.2 4 | argon2-cffi >= 16.1.0 5 | backports.zoneinfo; python_version < '3.9' 6 | bcrypt 7 | docutils 8 | geoip2 9 | jinja2 >= 2.9.2 10 | numpy 11 | Pillow >= 6.2.0 12 | # pylibmc/libmemcached can't be built on Windows. 13 | pylibmc; sys.platform != 'win32' 14 | pymemcache >= 3.4.0 15 | # RemovedInDjango41Warning. 16 | python-memcached >= 1.59 17 | pytz 18 | pywatchman; sys.platform != 'win32' 19 | PyYAML 20 | selenium 21 | sqlparse >= 0.2.2 22 | tblib >= 1.5.0 23 | tzdata 24 | colorama; sys.platform == 'win32' 25 | -------------------------------------------------------------------------------- /ci/test_mysql.py: -------------------------------------------------------------------------------- 1 | # This is an example test settings file for use with the Django test suite. 2 | # 3 | # The 'sqlite3' backend requires only the ENGINE setting (an in- 4 | # memory database will be used). All other backends will require a 5 | # NAME and potentially authentication information. See the 6 | # following section in the docs for more information: 7 | # 8 | # https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/ 9 | # 10 | # The different databases that Django supports behave differently in certain 11 | # situations, so it is recommended to run the test suite against as many 12 | # database backends as possible. You may want to create a separate settings 13 | # file for each of the backends you test against. 14 | 15 | DATABASES = { 16 | "default": { 17 | "ENGINE": "django.db.backends.mysql", 18 | "NAME": "django_default", 19 | "HOST": "127.0.0.1", 20 | "USER": "scott", 21 | "PASSWORD": "tiger", 22 | "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, 23 | }, 24 | "other": { 25 | "ENGINE": "django.db.backends.mysql", 26 | "NAME": "django_other", 27 | "HOST": "127.0.0.1", 28 | "USER": "scott", 29 | "PASSWORD": "tiger", 30 | "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, 31 | }, 32 | } 33 | 34 | SECRET_KEY = "django_tests_secret_key" 35 | 36 | # Use a fast hasher to speed up tests. 37 | PASSWORD_HASHERS = [ 38 | "django.contrib.auth.hashers.MD5PasswordHasher", 39 | ] 40 | 41 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 42 | 43 | USE_TZ = False 44 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "src/MySQLdb/constants/*" 3 | -------------------------------------------------------------------------------- /doc/FAQ.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | MySQLdb Frequently Asked Questions 3 | ==================================== 4 | 5 | .. contents:: 6 | .. 7 | 8 | 9 | Build Errors 10 | ------------ 11 | 12 | mysql.h: No such file or directory 13 | 14 | This almost always mean you don't have development packages 15 | installed. On some systems, C headers for various things (like MySQL) 16 | are distributed as a separate package. You'll need to figure out 17 | what that is and install it, but often the name ends with -devel. 18 | 19 | Another possibility: Some older versions of mysql_config behave oddly 20 | and may throw quotes around some of the path names, which confused 21 | MySQLdb-1.2.0. 1.2.1 works around these problems. If you see things 22 | like -I'/usr/local/include/mysql' in your compile command, that's 23 | probably the issue, but it shouldn't happen any more. 24 | 25 | 26 | ImportError 27 | ----------- 28 | 29 | ImportError: No module named _mysql 30 | 31 | If you see this, it's likely you did some wrong when installing 32 | MySQLdb; re-read (or read) README. _mysql is the low-level C module 33 | that interfaces with the MySQL client library. 34 | 35 | Various versions of MySQLdb in the past have had build issues on 36 | "weird" platforms; "weird" in this case means "not Linux", though 37 | generally there aren't problems on Unix/POSIX platforms, including 38 | BSDs and Mac OS X. Windows has been more problematic, in part because 39 | there is no `mysql_config` available in the Windows installation of 40 | MySQL. 1.2.1 solves most, if not all, of these problems, but you will 41 | still have to edit a configuration file so that the setup knows where 42 | to find MySQL and what libraries to include. 43 | 44 | 45 | ImportError: libmysqlclient_r.so.14: cannot open shared object file: No such file or directory 46 | 47 | The number after .so may vary, but this means you have a version of 48 | MySQLdb compiled against one version of MySQL, and are now trying to 49 | run it against a different version. The shared library version tends 50 | to change between major releases. 51 | 52 | Solution: Rebuilt MySQLdb, or get the matching version of MySQL. 53 | 54 | Another thing that can cause this: The MySQL libraries may not be on 55 | your system path. 56 | 57 | Solutions: 58 | 59 | * set the LD_LIBRARY_PATH environment variable so that it includes 60 | the path to the MySQL libraries. 61 | 62 | * set static=True in site.cfg for static linking 63 | 64 | * reconfigure your system so that the MySQL libraries are on the 65 | default loader path. In Linux, you edit /etc/ld.so.conf and run 66 | ldconfig. For Solaris, see `Linker and Libraries Guide 67 | `_. 68 | 69 | 70 | ImportError: ld.so.1: python: fatal: libmtmalloc.so.1: DF_1_NOOPEN tagged object may not be dlopen()'ed 71 | 72 | This is a weird one from Solaris. What does it mean? I have no idea. 73 | However, things like this can happen if there is some sort of a compiler 74 | or environment mismatch between Python and MySQL. For example, on some 75 | commercial systems, you might have some code compiled with their own 76 | compiler, and other things compiled with GCC. They don't always mesh 77 | together. One way to encounter this is by getting binary packages from 78 | different vendors. 79 | 80 | Solution: Rebuild Python or MySQL (or maybe both) from source. 81 | 82 | ImportError: dlopen(./_mysql.so, 2): Symbol not found: _sprintf$LDBLStub 83 | Referenced from: ./_mysql.so 84 | Expected in: dynamic lookup 85 | 86 | This is one from Mac OS X. It seems to have been a compiler mismatch, 87 | but this time between two different versions of GCC. It seems nearly 88 | every major release of GCC changes the ABI in some why, so linking 89 | code compiled with GCC-3.3 and GCC-4.0, for example, can be 90 | problematic. 91 | 92 | 93 | My data disappeared! (or won't go away!) 94 | ---------------------------------------- 95 | 96 | Starting with 1.2.0, MySQLdb disables autocommit by default, as 97 | required by the DB-API standard (`PEP-249`_). If you are using InnoDB 98 | tables or some other type of transactional table type, you'll need 99 | to do connection.commit() before closing the connection, or else 100 | none of your changes will be written to the database. 101 | 102 | Conversely, you can also use connection.rollback() to throw away 103 | any changes you've made since the last commit. 104 | 105 | Important note: Some SQL statements -- specifically DDL statements 106 | like CREATE TABLE -- are non-transactional, so they can't be 107 | rolled back, and they cause pending transactions to commit. 108 | 109 | 110 | Other Errors 111 | ------------ 112 | 113 | OperationalError: (1251, 'Client does not support authentication protocol requested by server; consider upgrading MySQL client') 114 | 115 | This means your server and client libraries are not the same version. 116 | More specifically, it probably means you have a 4.1 or newer server 117 | and 4.0 or older client. You can either upgrade the client side, or 118 | try some of the workarounds in `Password Hashing as of MySQL 4.1 119 | `_. 120 | 121 | 122 | Other Resources 123 | --------------- 124 | 125 | * Help forum. Please search before posting. 126 | 127 | * `Google `_ 128 | 129 | * READ README! 130 | 131 | * Read the User's Guide 132 | 133 | * Read `PEP-249`_ 134 | 135 | .. _`PEP-249`: https://www.python.org/dev/peps/pep-0249/ 136 | 137 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MySQLdb.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MySQLdb.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/MySQLdb" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MySQLdb" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/MySQLdb.constants.rst: -------------------------------------------------------------------------------- 1 | constants Package 2 | ================= 3 | 4 | :mod:`constants` Package 5 | ------------------------ 6 | 7 | .. automodule:: MySQLdb.constants 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`CLIENT` Module 13 | -------------------- 14 | 15 | .. automodule:: MySQLdb.constants.CLIENT 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`CR` Module 21 | ---------------- 22 | 23 | .. automodule:: MySQLdb.constants.CR 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`ER` Module 29 | ---------------- 30 | 31 | .. automodule:: MySQLdb.constants.ER 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`FIELD_TYPE` Module 37 | ------------------------ 38 | 39 | .. automodule:: MySQLdb.constants.FIELD_TYPE 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`FLAG` Module 45 | ------------------ 46 | 47 | .. automodule:: MySQLdb.constants.FLAG 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | -------------------------------------------------------------------------------- /doc/MySQLdb.rst: -------------------------------------------------------------------------------- 1 | MySQLdb Package 2 | =============== 3 | 4 | :mod:`MySQLdb` Package 5 | ---------------------- 6 | 7 | .. automodule:: MySQLdb 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`MySQLdb.connections` Module 13 | --------------------------------- 14 | 15 | .. automodule:: MySQLdb.connections 16 | :members: Connection 17 | :undoc-members: 18 | 19 | :mod:`MySQLdb.converters` Module 20 | -------------------------------- 21 | 22 | .. automodule:: MySQLdb.converters 23 | :members: 24 | :undoc-members: 25 | 26 | :mod:`MySQLdb.cursors` Module 27 | ----------------------------- 28 | 29 | .. automodule:: MySQLdb.cursors 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | :mod:`MySQLdb.times` Module 35 | --------------------------- 36 | 37 | .. automodule:: MySQLdb.times 38 | :members: 39 | :undoc-members: 40 | 41 | :mod:`MySQLdb._mysql` Module 42 | ---------------------------- 43 | 44 | .. automodule:: MySQLdb._mysql 45 | :members: 46 | :undoc-members: 47 | 48 | :mod:`MySQLdb._exceptions` Module 49 | --------------------------------- 50 | 51 | .. automodule:: MySQLdb._exceptions 52 | :members: 53 | :undoc-members: 54 | 55 | 56 | Subpackages 57 | ----------- 58 | 59 | .. toctree:: 60 | 61 | MySQLdb.constants 62 | 63 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # MySQLdb documentation build configuration file, created by 3 | # sphinx-quickstart on Sun Oct 07 19:36:17 2012. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | # skip flake8 and black for this file 14 | # flake8: noqa 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath("..")) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | nitpick_ignore = [ 26 | ("py:class", "datetime.date"), 27 | ("py:class", "datetime.time"), 28 | ("py:class", "datetime.datetime"), 29 | ] 30 | 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # needs_sphinx = "1.0" 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be extensions 36 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 37 | extensions = ["sphinx.ext.autodoc"] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = ".rst" 44 | 45 | # The encoding of source files. 46 | # source_encoding = "utf-8-sig" 47 | 48 | # The master toctree document. 49 | master_doc = "index" 50 | 51 | # General information about the project. 52 | project = "mysqlclient" 53 | copyright = "2023, Inada Naoki" 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = "1.2" 61 | # The full version, including alpha/beta/rc tags. 62 | release = "1.2.4b4" 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | # today = "" 71 | # Else, today_fmt is used as the format for a strftime call. 72 | # today_fmt = "%B %d, %Y" 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ["_build"] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all documents. 79 | # default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | # add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | # add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | # show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = "sphinx" 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | # modindex_common_prefix = [] 97 | 98 | 99 | # -- Options for HTML output --------------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = "sphinx_rtd_theme" 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | # html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | # html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | # html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | # html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | # html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ["_static"] 133 | 134 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 135 | # using the given strftime format. 136 | # html_last_updated_fmt = "%b %d, %Y" 137 | 138 | # If true, SmartyPants will be used to convert quotes and dashes to 139 | # typographically correct entities. 140 | # html_use_smartypants = True 141 | 142 | # Custom sidebar templates, maps document names to template names. 143 | # html_sidebars = {} 144 | 145 | # Additional templates that should be rendered to pages, maps page names to 146 | # template names. 147 | # html_additional_pages = {} 148 | 149 | # If false, no module index is generated. 150 | # html_domain_indices = True 151 | 152 | # If false, no index is generated. 153 | # html_use_index = True 154 | 155 | # If true, the index is split into individual pages for each letter. 156 | # html_split_index = False 157 | 158 | # If true, links to the reST sources are added to the pages. 159 | # html_show_sourcelink = True 160 | 161 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 162 | # html_show_sphinx = True 163 | 164 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 165 | # html_show_copyright = True 166 | 167 | # If true, an OpenSearch description file will be output, and all pages will 168 | # contain a tag referring to it. The value of this option must be the 169 | # base URL from which the finished HTML is served. 170 | # html_use_opensearch = "" 171 | 172 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 173 | # html_file_suffix = None 174 | 175 | # Output file base name for HTML help builder. 176 | htmlhelp_basename = "MySQLdbdoc" 177 | 178 | 179 | # -- Options for LaTeX output -------------------------------------------------- 180 | 181 | latex_elements = { 182 | # The paper size ('letterpaper' or 'a4paper'). 183 | #'papersize': 'letterpaper', 184 | # The font size ('10pt', '11pt' or '12pt'). 185 | #'pointsize': '10pt', 186 | # Additional stuff for the LaTeX preamble. 187 | #'preamble': '', 188 | } 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, author, documentclass [howto/manual]). 192 | latex_documents = [ 193 | ("index", "MySQLdb.tex", "MySQLdb Documentation", "Andy Dustman", "manual"), 194 | ] 195 | 196 | # The name of an image file (relative to this directory) to place at the top of 197 | # the title page. 198 | # latex_logo = None 199 | 200 | # For "manual" documents, if this is true, then toplevel headings are parts, 201 | # not chapters. 202 | # latex_use_parts = False 203 | 204 | # If true, show page references after internal links. 205 | # latex_show_pagerefs = False 206 | 207 | # If true, show URL addresses after external links. 208 | # latex_show_urls = False 209 | 210 | # Documents to append as an appendix to all manuals. 211 | # latex_appendices = [] 212 | 213 | # If false, no module index is generated. 214 | # latex_domain_indices = True 215 | 216 | 217 | # -- Options for manual page output -------------------------------------------- 218 | 219 | # One entry per manual page. List of tuples 220 | # (source start file, name, description, authors, manual section). 221 | man_pages = [("index", "mysqldb", "MySQLdb Documentation", ["Andy Dustman"], 1)] 222 | 223 | # If true, show URL addresses after external links. 224 | # man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ( 234 | "index", 235 | "MySQLdb", 236 | "MySQLdb Documentation", 237 | "Andy Dustman", 238 | "MySQLdb", 239 | "One line description of project.", 240 | "Miscellaneous", 241 | ), 242 | ] 243 | 244 | # Documents to append as an appendix to all manuals. 245 | # texinfo_appendices = [] 246 | 247 | # If false, no module index is generated. 248 | # texinfo_domain_indices = True 249 | 250 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 251 | # texinfo_show_urls = 'footnote' 252 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to MySQLdb's documentation! 2 | =================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | 9 | user_guide 10 | MySQLdb 11 | FAQ 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | 20 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\MySQLdb.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\MySQLdb.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx~=8.0 2 | sphinx-rtd-theme~=3.0.0 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mysqlclient" 3 | description = "Python interface to MySQL" 4 | readme = "README.md" 5 | requires-python = ">=3.8" 6 | authors = [ 7 | {name = "Inada Naoki", email = "songofacandy@gmail.com"} 8 | ] 9 | license = {text = "GNU General Public License v2 or later (GPLv2+)"} 10 | keywords = ["MySQL"] 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "Environment :: Other Environment", 14 | "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", 15 | "Operating System :: MacOS :: MacOS X", 16 | "Operating System :: Microsoft :: Windows :: Windows NT/2000", 17 | "Operating System :: OS Independent", 18 | "Operating System :: POSIX", 19 | "Operating System :: POSIX :: Linux", 20 | "Operating System :: Unix", 21 | "Programming Language :: C", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Topic :: Database", 31 | "Topic :: Database :: Database Engines/Servers", 32 | ] 33 | dynamic = ["version"] 34 | 35 | [project.urls] 36 | Project = "https://github.com/PyMySQL/mysqlclient" 37 | Documentation = "https://mysqlclient.readthedocs.io/" 38 | 39 | [build-system] 40 | requires = ["setuptools>=61"] 41 | build-backend = "setuptools.build_meta" 42 | 43 | [tool.setuptools] 44 | package-dir = {"" = "src"} 45 | 46 | [tool.setuptools.packages.find] 47 | namespaces = false 48 | where = ["src"] 49 | include = ["MySQLdb*"] 50 | 51 | [tool.setuptools.dynamic] 52 | version = {attr = "MySQLdb.release.__version__"} 53 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is for GitHub Action 2 | coverage 3 | pytest 4 | pytest-cov 5 | tblib 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | import setuptools 7 | from configparser import ConfigParser 8 | 9 | 10 | release_info = {} 11 | with open("src/MySQLdb/release.py", encoding="utf-8") as f: 12 | exec(f.read(), None, release_info) 13 | 14 | 15 | def find_package_name(): 16 | """Get available pkg-config package name""" 17 | # Ubuntu uses mariadb.pc, but CentOS uses libmariadb.pc 18 | packages = ["mysqlclient", "mariadb", "libmariadb", "perconaserverclient"] 19 | for pkg in packages: 20 | try: 21 | cmd = f"pkg-config --exists {pkg}" 22 | print(f"Trying {cmd}") 23 | subprocess.check_call(cmd, shell=True) 24 | except subprocess.CalledProcessError as err: 25 | print(err) 26 | else: 27 | return pkg 28 | raise Exception( 29 | "Can not find valid pkg-config name.\n" 30 | "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 31 | ) 32 | 33 | 34 | def get_config_posix(options=None): 35 | # allow a command-line option to override the base config file to permit 36 | # a static build to be created via requirements.txt 37 | # TODO: find a better way for 38 | static = False 39 | if "--static" in sys.argv: 40 | static = True 41 | sys.argv.remove("--static") 42 | 43 | ldflags = os.environ.get("MYSQLCLIENT_LDFLAGS") 44 | cflags = os.environ.get("MYSQLCLIENT_CFLAGS") 45 | 46 | pkg_name = None 47 | static_opt = " --static" if static else "" 48 | if not (cflags and ldflags): 49 | pkg_name = find_package_name() 50 | if not cflags: 51 | cflags = subprocess.check_output( 52 | f"pkg-config{static_opt} --cflags {pkg_name}", encoding="utf-8", shell=True 53 | ) 54 | if not ldflags: 55 | ldflags = subprocess.check_output( 56 | f"pkg-config{static_opt} --libs {pkg_name}", encoding="utf-8", shell=True 57 | ) 58 | 59 | cflags = cflags.split() 60 | for f in cflags: 61 | if f.startswith("-std="): 62 | break 63 | else: 64 | cflags += ["-std=c99"] 65 | 66 | ldflags = ldflags.split() 67 | 68 | define_macros = [ 69 | ("version_info", release_info["version_info"]), 70 | ("__version__", release_info["__version__"]), 71 | ] 72 | 73 | ext_options = dict( 74 | extra_compile_args=cflags, 75 | extra_link_args=ldflags, 76 | define_macros=define_macros, 77 | ) 78 | # newer versions of gcc require libstdc++ if doing a static build 79 | if static: 80 | ext_options["language"] = "c++" 81 | 82 | return ext_options 83 | 84 | 85 | def get_config_win32(options): 86 | client = "mariadbclient" 87 | connector = os.environ.get("MYSQLCLIENT_CONNECTOR", options.get("connector")) 88 | if not connector: 89 | connector = os.path.join( 90 | os.environ["ProgramFiles"], "MariaDB", "MariaDB Connector C" 91 | ) 92 | 93 | extra_objects = [] 94 | 95 | library_dirs = [ 96 | os.path.join(connector, "lib", "mariadb"), 97 | os.path.join(connector, "lib"), 98 | ] 99 | libraries = [ 100 | "kernel32", 101 | "advapi32", 102 | "wsock32", 103 | "shlwapi", 104 | "Ws2_32", 105 | "crypt32", 106 | "secur32", 107 | "bcrypt", 108 | client, 109 | ] 110 | include_dirs = [ 111 | os.path.join(connector, "include", "mariadb"), 112 | os.path.join(connector, "include", "mysql"), 113 | os.path.join(connector, "include"), 114 | ] 115 | 116 | extra_link_args = ["/MANIFEST"] 117 | 118 | define_macros = [ 119 | ("version_info", release_info["version_info"]), 120 | ("__version__", release_info["__version__"]), 121 | ] 122 | 123 | ext_options = dict( 124 | library_dirs=library_dirs, 125 | libraries=libraries, 126 | extra_link_args=extra_link_args, 127 | include_dirs=include_dirs, 128 | extra_objects=extra_objects, 129 | define_macros=define_macros, 130 | ) 131 | return ext_options 132 | 133 | 134 | def enabled(options, option): 135 | value = options[option] 136 | s = value.lower() 137 | if s in ("yes", "true", "1", "y"): 138 | return True 139 | elif s in ("no", "false", "0", "n"): 140 | return False 141 | else: 142 | raise ValueError(f"Unknown value {value} for option {option}") 143 | 144 | 145 | def get_options(): 146 | config = ConfigParser() 147 | config.read(["site.cfg"]) 148 | options = dict(config.items("options")) 149 | options["static"] = enabled(options, "static") 150 | return options 151 | 152 | 153 | if sys.platform == "win32": 154 | ext_options = get_config_win32(get_options()) 155 | else: 156 | ext_options = get_config_posix(get_options()) 157 | 158 | print("# Options for building extension module:") 159 | for k, v in ext_options.items(): 160 | print(f" {k}: {v}") 161 | 162 | ext_modules = [ 163 | setuptools.Extension( 164 | "MySQLdb._mysql", 165 | sources=["src/MySQLdb/_mysql.c"], 166 | **ext_options, 167 | ) 168 | ] 169 | setuptools.setup(ext_modules=ext_modules) 170 | -------------------------------------------------------------------------------- /site.cfg: -------------------------------------------------------------------------------- 1 | [options] 2 | # static: link against a static library 3 | static = False 4 | 5 | # http://stackoverflow.com/questions/1972259/mysql-python-install-problem-using-virtualenv-windows-pip 6 | # Windows connector libs for MySQL. You need a 32-bit connector for your 32-bit Python build. 7 | connector = 8 | -------------------------------------------------------------------------------- /src/MySQLdb/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MySQLdb - A DB API v2.0 compatible interface to MySQL. 3 | 4 | This package is a wrapper around _mysql, which mostly implements the 5 | MySQL C API. 6 | 7 | connect() -- connects to server 8 | 9 | See the C API specification and the MySQL documentation for more info 10 | on other items. 11 | 12 | For information on how MySQLdb handles type conversion, see the 13 | MySQLdb.converters module. 14 | """ 15 | 16 | from .release import version_info 17 | from . import _mysql 18 | 19 | if version_info != _mysql.version_info: 20 | raise ImportError( 21 | f"this is MySQLdb version {version_info}, " 22 | f"but _mysql is version {_mysql.version_info!r}\n" 23 | f"_mysql: {_mysql.__file__!r}" 24 | ) 25 | 26 | 27 | from ._mysql import ( 28 | NotSupportedError, 29 | OperationalError, 30 | get_client_info, 31 | ProgrammingError, 32 | Error, 33 | InterfaceError, 34 | debug, 35 | IntegrityError, 36 | string_literal, 37 | MySQLError, 38 | DataError, 39 | DatabaseError, 40 | InternalError, 41 | Warning, 42 | ) 43 | from MySQLdb.constants import FIELD_TYPE 44 | from MySQLdb.times import ( 45 | Date, 46 | Time, 47 | Timestamp, 48 | DateFromTicks, 49 | TimeFromTicks, 50 | TimestampFromTicks, 51 | ) 52 | 53 | threadsafety = 1 54 | apilevel = "2.0" 55 | paramstyle = "format" 56 | 57 | 58 | class DBAPISet(frozenset): 59 | """A special type of set for which A == x is true if A is a 60 | DBAPISet and x is a member of that set.""" 61 | 62 | def __eq__(self, other): 63 | if isinstance(other, DBAPISet): 64 | return not self.difference(other) 65 | return other in self 66 | 67 | 68 | STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING]) 69 | BINARY = DBAPISet( 70 | [ 71 | FIELD_TYPE.BLOB, 72 | FIELD_TYPE.LONG_BLOB, 73 | FIELD_TYPE.MEDIUM_BLOB, 74 | FIELD_TYPE.TINY_BLOB, 75 | ] 76 | ) 77 | NUMBER = DBAPISet( 78 | [ 79 | FIELD_TYPE.DECIMAL, 80 | FIELD_TYPE.DOUBLE, 81 | FIELD_TYPE.FLOAT, 82 | FIELD_TYPE.INT24, 83 | FIELD_TYPE.LONG, 84 | FIELD_TYPE.LONGLONG, 85 | FIELD_TYPE.TINY, 86 | FIELD_TYPE.YEAR, 87 | FIELD_TYPE.NEWDECIMAL, 88 | ] 89 | ) 90 | DATE = DBAPISet([FIELD_TYPE.DATE]) 91 | TIME = DBAPISet([FIELD_TYPE.TIME]) 92 | TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME]) 93 | DATETIME = TIMESTAMP 94 | ROWID = DBAPISet() 95 | 96 | 97 | def test_DBAPISet_set_equality(): 98 | assert STRING == STRING 99 | 100 | 101 | def test_DBAPISet_set_inequality(): 102 | assert STRING != NUMBER 103 | 104 | 105 | def test_DBAPISet_set_equality_membership(): 106 | assert FIELD_TYPE.VAR_STRING == STRING 107 | 108 | 109 | def test_DBAPISet_set_inequality_membership(): 110 | assert FIELD_TYPE.DATE != STRING 111 | 112 | 113 | def Binary(x): 114 | return bytes(x) 115 | 116 | 117 | def Connect(*args, **kwargs): 118 | """Factory function for connections.Connection.""" 119 | from MySQLdb.connections import Connection 120 | 121 | return Connection(*args, **kwargs) 122 | 123 | 124 | connect = Connection = Connect 125 | 126 | __all__ = [ 127 | "BINARY", 128 | "Binary", 129 | "Connect", 130 | "Connection", 131 | "DATE", 132 | "Date", 133 | "Time", 134 | "Timestamp", 135 | "DateFromTicks", 136 | "TimeFromTicks", 137 | "TimestampFromTicks", 138 | "DataError", 139 | "DatabaseError", 140 | "Error", 141 | "FIELD_TYPE", 142 | "IntegrityError", 143 | "InterfaceError", 144 | "InternalError", 145 | "MySQLError", 146 | "NUMBER", 147 | "NotSupportedError", 148 | "DBAPISet", 149 | "OperationalError", 150 | "ProgrammingError", 151 | "ROWID", 152 | "STRING", 153 | "TIME", 154 | "TIMESTAMP", 155 | "Warning", 156 | "apilevel", 157 | "connect", 158 | "connections", 159 | "constants", 160 | "converters", 161 | "cursors", 162 | "debug", 163 | "get_client_info", 164 | "paramstyle", 165 | "string_literal", 166 | "threadsafety", 167 | "version_info", 168 | ] 169 | -------------------------------------------------------------------------------- /src/MySQLdb/_exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes for _mysql and MySQLdb. 2 | 3 | These classes are dictated by the DB API v2.0: 4 | 5 | https://www.python.org/dev/peps/pep-0249/ 6 | """ 7 | 8 | 9 | class MySQLError(Exception): 10 | """Exception related to operation with MySQL.""" 11 | 12 | __module__ = "MySQLdb" 13 | 14 | 15 | class Warning(Warning, MySQLError): 16 | """Exception raised for important warnings like data truncations 17 | while inserting, etc.""" 18 | 19 | __module__ = "MySQLdb" 20 | 21 | 22 | class Error(MySQLError): 23 | """Exception that is the base class of all other error exceptions 24 | (not Warning).""" 25 | 26 | __module__ = "MySQLdb" 27 | 28 | 29 | class InterfaceError(Error): 30 | """Exception raised for errors that are related to the database 31 | interface rather than the database itself.""" 32 | 33 | __module__ = "MySQLdb" 34 | 35 | 36 | class DatabaseError(Error): 37 | """Exception raised for errors that are related to the 38 | database.""" 39 | 40 | __module__ = "MySQLdb" 41 | 42 | 43 | class DataError(DatabaseError): 44 | """Exception raised for errors that are due to problems with the 45 | processed data like division by zero, numeric value out of range, 46 | etc.""" 47 | 48 | __module__ = "MySQLdb" 49 | 50 | 51 | class OperationalError(DatabaseError): 52 | """Exception raised for errors that are related to the database's 53 | operation and not necessarily under the control of the programmer, 54 | e.g. an unexpected disconnect occurs, the data source name is not 55 | found, a transaction could not be processed, a memory allocation 56 | error occurred during processing, etc.""" 57 | 58 | __module__ = "MySQLdb" 59 | 60 | 61 | class IntegrityError(DatabaseError): 62 | """Exception raised when the relational integrity of the database 63 | is affected, e.g. a foreign key check fails, duplicate key, 64 | etc.""" 65 | 66 | __module__ = "MySQLdb" 67 | 68 | 69 | class InternalError(DatabaseError): 70 | """Exception raised when the database encounters an internal 71 | error, e.g. the cursor is not valid anymore, the transaction is 72 | out of sync, etc.""" 73 | 74 | __module__ = "MySQLdb" 75 | 76 | 77 | class ProgrammingError(DatabaseError): 78 | """Exception raised for programming errors, e.g. table not found 79 | or already exists, syntax error in the SQL statement, wrong number 80 | of parameters specified, etc.""" 81 | 82 | __module__ = "MySQLdb" 83 | 84 | 85 | class NotSupportedError(DatabaseError): 86 | """Exception raised in case a method or database API was used 87 | which is not supported by the database, e.g. requesting a 88 | .rollback() on a connection that does not support transaction or 89 | has transactions turned off.""" 90 | 91 | __module__ = "MySQLdb" 92 | -------------------------------------------------------------------------------- /src/MySQLdb/connections.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements connections for MySQLdb. Presently there is 3 | only one class: Connection. Others are unlikely. However, you might 4 | want to make your own subclasses. In most cases, you will probably 5 | override Connection.default_cursor with a non-standard Cursor class. 6 | """ 7 | import re 8 | 9 | from . import cursors, _mysql 10 | from ._exceptions import ( 11 | Warning, 12 | Error, 13 | InterfaceError, 14 | DataError, 15 | DatabaseError, 16 | OperationalError, 17 | IntegrityError, 18 | InternalError, 19 | NotSupportedError, 20 | ProgrammingError, 21 | ) 22 | 23 | # Mapping from MySQL charset name to Python codec name 24 | _charset_to_encoding = { 25 | "utf8mb4": "utf8", 26 | "utf8mb3": "utf8", 27 | "latin1": "cp1252", 28 | "koi8r": "koi8_r", 29 | "koi8u": "koi8_u", 30 | } 31 | 32 | re_numeric_part = re.compile(r"^(\d+)") 33 | 34 | 35 | def numeric_part(s): 36 | """Returns the leading numeric part of a string. 37 | 38 | >>> numeric_part("20-alpha") 39 | 20 40 | >>> numeric_part("foo") 41 | >>> numeric_part("16b") 42 | 16 43 | """ 44 | 45 | m = re_numeric_part.match(s) 46 | if m: 47 | return int(m.group(1)) 48 | return None 49 | 50 | 51 | class Connection(_mysql.connection): 52 | """MySQL Database Connection Object""" 53 | 54 | default_cursor = cursors.Cursor 55 | 56 | def __init__(self, *args, **kwargs): 57 | """ 58 | Create a connection to the database. It is strongly recommended 59 | that you only use keyword parameters. Consult the MySQL C API 60 | documentation for more information. 61 | 62 | :param str host: host to connect 63 | :param str user: user to connect as 64 | :param str password: password to use 65 | :param str passwd: alias of password (deprecated) 66 | :param str database: database to use 67 | :param str db: alias of database (deprecated) 68 | :param int port: TCP/IP port to connect to 69 | :param str unix_socket: location of unix_socket to use 70 | :param dict conv: conversion dictionary, see MySQLdb.converters 71 | :param int connect_timeout: 72 | number of seconds to wait before the connection attempt fails. 73 | 74 | :param bool compress: if set, compression is enabled 75 | :param str named_pipe: if set, a named pipe is used to connect (Windows only) 76 | :param str init_command: 77 | command which is run once the connection is created 78 | 79 | :param str read_default_file: 80 | file from which default client values are read 81 | 82 | :param str read_default_group: 83 | configuration group to use from the default file 84 | 85 | :param type cursorclass: 86 | class object, used to create cursors (keyword only) 87 | 88 | :param bool use_unicode: 89 | If True, text-like columns are returned as unicode objects 90 | using the connection's character set. Otherwise, text-like 91 | columns are returned as bytes. Unicode objects will always 92 | be encoded to the connection's character set regardless of 93 | this setting. 94 | Default to True. 95 | 96 | :param str charset: 97 | If supplied, the connection character set will be changed 98 | to this character set. 99 | 100 | :param str collation: 101 | If ``charset`` and ``collation`` are both supplied, the 102 | character set and collation for the current connection 103 | will be set. 104 | 105 | If omitted, empty string, or None, the default collation 106 | for the ``charset`` is implied. 107 | 108 | :param str auth_plugin: 109 | If supplied, the connection default authentication plugin will be 110 | changed to this value. Example values: 111 | `mysql_native_password` or `caching_sha2_password` 112 | 113 | :param str sql_mode: 114 | If supplied, the session SQL mode will be changed to this 115 | setting. 116 | For more details and legal values, see the MySQL documentation. 117 | 118 | :param int client_flag: 119 | flags to use or 0 (see MySQL docs or constants/CLIENTS.py) 120 | 121 | :param bool multi_statements: 122 | If True, enable multi statements for clients >= 4.1. 123 | Defaults to True. 124 | 125 | :param str ssl_mode: 126 | specify the security settings for connection to the server; 127 | see the MySQL documentation for more details 128 | (mysql_option(), MYSQL_OPT_SSL_MODE). 129 | Only one of 'DISABLED', 'PREFERRED', 'REQUIRED', 130 | 'VERIFY_CA', 'VERIFY_IDENTITY' can be specified. 131 | 132 | :param dict ssl: 133 | dictionary or mapping contains SSL connection parameters; 134 | see the MySQL documentation for more details 135 | (mysql_ssl_set()). If this is set, and the client does not 136 | support SSL, NotSupportedError will be raised. 137 | Since mysqlclient 2.2.4, ssl=True is alias of ssl_mode=REQUIRED 138 | for better compatibility with PyMySQL and MariaDB. 139 | 140 | :param str server_public_key_path: 141 | specify the path to a file RSA public key file for caching_sha2_password. 142 | See https://dev.mysql.com/doc/refman/9.0/en/caching-sha2-pluggable-authentication.html 143 | 144 | :param bool local_infile: 145 | sets ``MYSQL_OPT_LOCAL_INFILE`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; zero disables; 146 | 147 | :param str local_infile_dir: 148 | sets ``MYSQL_OPT_LOAD_DATA_LOCAL_DIR`` in ``mysql_options()`` enabling LOAD LOCAL INFILE from any path; 149 | if ``local_infile`` is set to ``True`` then this is ignored; 150 | 151 | supported for mysql version >= 8.0.21 152 | 153 | :param bool autocommit: 154 | If False (default), autocommit is disabled. 155 | If True, autocommit is enabled. 156 | If None, autocommit isn't set and server default is used. 157 | 158 | :param bool binary_prefix: 159 | If set, the '_binary' prefix will be used for raw byte query 160 | arguments (e.g. Binary). This is disabled by default. 161 | 162 | There are a number of undocumented, non-standard methods. See the 163 | documentation for the MySQL C API for some hints on what they do. 164 | """ 165 | from MySQLdb.constants import CLIENT, FIELD_TYPE 166 | from MySQLdb.converters import conversions, _bytes_or_str 167 | 168 | kwargs2 = kwargs.copy() 169 | 170 | if "db" in kwargs2: 171 | kwargs2["database"] = kwargs2.pop("db") 172 | if "passwd" in kwargs2: 173 | kwargs2["password"] = kwargs2.pop("passwd") 174 | 175 | if "conv" in kwargs: 176 | conv = kwargs["conv"] 177 | else: 178 | conv = conversions 179 | 180 | conv2 = {} 181 | for k, v in conv.items(): 182 | if isinstance(k, int) and isinstance(v, list): 183 | conv2[k] = v[:] 184 | else: 185 | conv2[k] = v 186 | kwargs2["conv"] = conv2 187 | 188 | cursorclass = kwargs2.pop("cursorclass", self.default_cursor) 189 | charset = kwargs2.get("charset", "") 190 | collation = kwargs2.pop("collation", "") 191 | use_unicode = kwargs2.pop("use_unicode", True) 192 | sql_mode = kwargs2.pop("sql_mode", "") 193 | self._binary_prefix = kwargs2.pop("binary_prefix", False) 194 | 195 | client_flag = kwargs.get("client_flag", 0) 196 | client_flag |= CLIENT.MULTI_RESULTS 197 | multi_statements = kwargs2.pop("multi_statements", True) 198 | if multi_statements: 199 | client_flag |= CLIENT.MULTI_STATEMENTS 200 | kwargs2["client_flag"] = client_flag 201 | 202 | # PEP-249 requires autocommit to be initially off 203 | autocommit = kwargs2.pop("autocommit", False) 204 | 205 | self._set_attributes(*args, **kwargs2) 206 | super().__init__(*args, **kwargs2) 207 | 208 | self.cursorclass = cursorclass 209 | self.encoders = { 210 | k: v 211 | for k, v in conv.items() 212 | if type(k) is not int # noqa: E721 213 | } 214 | self._server_version = tuple( 215 | [numeric_part(n) for n in self.get_server_info().split(".")[:2]] 216 | ) 217 | self.encoding = "ascii" # overridden in set_character_set() 218 | 219 | if not charset: 220 | charset = self.character_set_name() 221 | self.set_character_set(charset, collation) 222 | 223 | if sql_mode: 224 | self.set_sql_mode(sql_mode) 225 | 226 | if use_unicode: 227 | for t in ( 228 | FIELD_TYPE.STRING, 229 | FIELD_TYPE.VAR_STRING, 230 | FIELD_TYPE.VARCHAR, 231 | FIELD_TYPE.TINY_BLOB, 232 | FIELD_TYPE.MEDIUM_BLOB, 233 | FIELD_TYPE.LONG_BLOB, 234 | FIELD_TYPE.BLOB, 235 | ): 236 | self.converter[t] = _bytes_or_str 237 | # Unlike other string/blob types, JSON is always text. 238 | # MySQL may return JSON with charset==binary. 239 | self.converter[FIELD_TYPE.JSON] = str 240 | 241 | self._transactional = self.server_capabilities & CLIENT.TRANSACTIONS 242 | if self._transactional: 243 | if autocommit is not None: 244 | self.autocommit(autocommit) 245 | self.messages = [] 246 | 247 | def _set_attributes(self, host=None, user=None, password=None, database="", port=3306, 248 | unix_socket=None, **kwargs): 249 | """set some attributes for otel""" 250 | if unix_socket and not host: 251 | host = "localhost" 252 | # support opentelemetry-instrumentation-dbapi 253 | self.host = host 254 | # _mysql.Connection provides self.port 255 | self.user = user 256 | self.database = database 257 | # otel-inst-mysqlclient uses db instead of database. 258 | self.db = database 259 | # NOTE: We have not supported semantic conventions yet. 260 | # https://opentelemetry.io/docs/specs/semconv/database/sql/ 261 | 262 | def __enter__(self): 263 | return self 264 | 265 | def __exit__(self, exc_type, exc_value, traceback): 266 | self.close() 267 | 268 | def autocommit(self, on): 269 | on = bool(on) 270 | if self.get_autocommit() != on: 271 | _mysql.connection.autocommit(self, on) 272 | 273 | def cursor(self, cursorclass=None): 274 | """ 275 | Create a cursor on which queries may be performed. The 276 | optional cursorclass parameter is used to create the 277 | Cursor. By default, self.cursorclass=cursors.Cursor is 278 | used. 279 | """ 280 | return (cursorclass or self.cursorclass)(self) 281 | 282 | def query(self, query): 283 | # Since _mysql releases GIL while querying, we need immutable buffer. 284 | if isinstance(query, bytearray): 285 | query = bytes(query) 286 | _mysql.connection.query(self, query) 287 | 288 | def _bytes_literal(self, bs): 289 | assert isinstance(bs, (bytes, bytearray)) 290 | x = self.string_literal(bs) # x is escaped and quoted bytes 291 | if self._binary_prefix: 292 | return b"_binary" + x 293 | return x 294 | 295 | def _tuple_literal(self, t): 296 | return b"(%s)" % (b",".join(map(self.literal, t))) 297 | 298 | def literal(self, o): 299 | """If o is a single object, returns an SQL literal as a string. 300 | If o is a non-string sequence, the items of the sequence are 301 | converted and returned as a sequence. 302 | 303 | Non-standard. For internal use; do not use this in your 304 | applications. 305 | """ 306 | if isinstance(o, str): 307 | s = self.string_literal(o.encode(self.encoding)) 308 | elif isinstance(o, bytearray): 309 | s = self._bytes_literal(o) 310 | elif isinstance(o, bytes): 311 | s = self._bytes_literal(o) 312 | elif isinstance(o, (tuple, list)): 313 | s = self._tuple_literal(o) 314 | else: 315 | s = self.escape(o, self.encoders) 316 | if isinstance(s, str): 317 | s = s.encode(self.encoding) 318 | assert isinstance(s, bytes) 319 | return s 320 | 321 | def begin(self): 322 | """Explicitly begin a connection. 323 | 324 | This method is not used when autocommit=False (default). 325 | """ 326 | self.query(b"BEGIN") 327 | 328 | def set_character_set(self, charset, collation=None): 329 | """Set the connection character set to charset.""" 330 | super().set_character_set(charset) 331 | self.encoding = _charset_to_encoding.get(charset, charset) 332 | if collation: 333 | self.query(f"SET NAMES {charset} COLLATE {collation}") 334 | self.store_result() 335 | 336 | def set_sql_mode(self, sql_mode): 337 | """Set the connection sql_mode. See MySQL documentation for 338 | legal values.""" 339 | if self._server_version < (4, 1): 340 | raise NotSupportedError("server is too old to set sql_mode") 341 | self.query("SET SESSION sql_mode='%s'" % sql_mode) 342 | self.store_result() 343 | 344 | def show_warnings(self): 345 | """Return detailed information about warnings as a 346 | sequence of tuples of (Level, Code, Message). This 347 | is only supported in MySQL-4.1 and up. If your server 348 | is an earlier version, an empty sequence is returned.""" 349 | if self._server_version < (4, 1): 350 | return () 351 | self.query("SHOW WARNINGS") 352 | r = self.store_result() 353 | warnings = r.fetch_row(0) 354 | return warnings 355 | 356 | Warning = Warning 357 | Error = Error 358 | InterfaceError = InterfaceError 359 | DatabaseError = DatabaseError 360 | DataError = DataError 361 | OperationalError = OperationalError 362 | IntegrityError = IntegrityError 363 | InternalError = InternalError 364 | ProgrammingError = ProgrammingError 365 | NotSupportedError = NotSupportedError 366 | 367 | 368 | # vim: colorcolumn=100 369 | -------------------------------------------------------------------------------- /src/MySQLdb/constants/CLIENT.py: -------------------------------------------------------------------------------- 1 | """MySQL CLIENT constants 2 | 3 | These constants are used when creating the connection. Use bitwise-OR 4 | (|) to combine options together, and pass them as the client_flags 5 | parameter to MySQLdb.Connection. For more information on these flags, 6 | see the MySQL C API documentation for mysql_real_connect(). 7 | 8 | """ 9 | 10 | LONG_PASSWORD = 1 11 | FOUND_ROWS = 2 12 | LONG_FLAG = 4 13 | CONNECT_WITH_DB = 8 14 | NO_SCHEMA = 16 15 | COMPRESS = 32 16 | ODBC = 64 17 | LOCAL_FILES = 128 18 | IGNORE_SPACE = 256 19 | CHANGE_USER = 512 20 | INTERACTIVE = 1024 21 | SSL = 2048 22 | IGNORE_SIGPIPE = 4096 23 | TRANSACTIONS = 8192 # mysql_com.h was WRONG prior to 3.23.35 24 | RESERVED = 16384 25 | SECURE_CONNECTION = 32768 26 | MULTI_STATEMENTS = 65536 27 | MULTI_RESULTS = 131072 28 | -------------------------------------------------------------------------------- /src/MySQLdb/constants/CR.py: -------------------------------------------------------------------------------- 1 | """MySQL Connection Errors 2 | 3 | Nearly all of these raise OperationalError. COMMANDS_OUT_OF_SYNC 4 | raises ProgrammingError. 5 | 6 | """ 7 | 8 | if __name__ == "__main__": 9 | """ 10 | Usage: python CR.py [/path/to/mysql/errmsg.h ...] >> CR.py 11 | """ 12 | import fileinput 13 | import re 14 | 15 | data = {} 16 | error_last = None 17 | for line in fileinput.input(): 18 | line = re.sub(r"/\*.*?\*/", "", line) 19 | m = re.match(r"^\s*#define\s+CR_([A-Z0-9_]+)\s+(\d+)(\s.*|$)", line) 20 | if m: 21 | name = m.group(1) 22 | value = int(m.group(2)) 23 | if name == "ERROR_LAST": 24 | if error_last is None or error_last < value: 25 | error_last = value 26 | continue 27 | if value not in data: 28 | data[value] = set() 29 | data[value].add(name) 30 | for value, names in sorted(data.items()): 31 | for name in sorted(names): 32 | print(f"{name} = {value}") 33 | if error_last is not None: 34 | print("ERROR_LAST = %s" % error_last) 35 | 36 | 37 | ERROR_FIRST = 2000 38 | MIN_ERROR = 2000 39 | UNKNOWN_ERROR = 2000 40 | SOCKET_CREATE_ERROR = 2001 41 | CONNECTION_ERROR = 2002 42 | CONN_HOST_ERROR = 2003 43 | IPSOCK_ERROR = 2004 44 | UNKNOWN_HOST = 2005 45 | SERVER_GONE_ERROR = 2006 46 | VERSION_ERROR = 2007 47 | OUT_OF_MEMORY = 2008 48 | WRONG_HOST_INFO = 2009 49 | LOCALHOST_CONNECTION = 2010 50 | TCP_CONNECTION = 2011 51 | SERVER_HANDSHAKE_ERR = 2012 52 | SERVER_LOST = 2013 53 | COMMANDS_OUT_OF_SYNC = 2014 54 | NAMEDPIPE_CONNECTION = 2015 55 | NAMEDPIPEWAIT_ERROR = 2016 56 | NAMEDPIPEOPEN_ERROR = 2017 57 | NAMEDPIPESETSTATE_ERROR = 2018 58 | CANT_READ_CHARSET = 2019 59 | NET_PACKET_TOO_LARGE = 2020 60 | EMBEDDED_CONNECTION = 2021 61 | PROBE_SLAVE_STATUS = 2022 62 | PROBE_SLAVE_HOSTS = 2023 63 | PROBE_SLAVE_CONNECT = 2024 64 | PROBE_MASTER_CONNECT = 2025 65 | SSL_CONNECTION_ERROR = 2026 66 | MALFORMED_PACKET = 2027 67 | WRONG_LICENSE = 2028 68 | NULL_POINTER = 2029 69 | NO_PREPARE_STMT = 2030 70 | PARAMS_NOT_BOUND = 2031 71 | DATA_TRUNCATED = 2032 72 | NO_PARAMETERS_EXISTS = 2033 73 | INVALID_PARAMETER_NO = 2034 74 | INVALID_BUFFER_USE = 2035 75 | UNSUPPORTED_PARAM_TYPE = 2036 76 | SHARED_MEMORY_CONNECTION = 2037 77 | SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038 78 | SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039 79 | SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040 80 | SHARED_MEMORY_CONNECT_MAP_ERROR = 2041 81 | SHARED_MEMORY_FILE_MAP_ERROR = 2042 82 | SHARED_MEMORY_MAP_ERROR = 2043 83 | SHARED_MEMORY_EVENT_ERROR = 2044 84 | SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045 85 | SHARED_MEMORY_CONNECT_SET_ERROR = 2046 86 | CONN_UNKNOW_PROTOCOL = 2047 87 | INVALID_CONN_HANDLE = 2048 88 | UNUSED_1 = 2049 89 | FETCH_CANCELED = 2050 90 | NO_DATA = 2051 91 | NO_STMT_METADATA = 2052 92 | NO_RESULT_SET = 2053 93 | NOT_IMPLEMENTED = 2054 94 | SERVER_LOST_EXTENDED = 2055 95 | STMT_CLOSED = 2056 96 | NEW_STMT_METADATA = 2057 97 | ALREADY_CONNECTED = 2058 98 | AUTH_PLUGIN_CANNOT_LOAD = 2059 99 | DUPLICATE_CONNECTION_ATTR = 2060 100 | AUTH_PLUGIN_ERR = 2061 101 | INSECURE_API_ERR = 2062 102 | FILE_NAME_TOO_LONG = 2063 103 | SSL_FIPS_MODE_ERR = 2064 104 | MAX_ERROR = 2999 105 | ERROR_LAST = 2064 106 | -------------------------------------------------------------------------------- /src/MySQLdb/constants/FIELD_TYPE.py: -------------------------------------------------------------------------------- 1 | """MySQL FIELD_TYPE Constants 2 | 3 | These constants represent the various column (field) types that are 4 | supported by MySQL. 5 | """ 6 | 7 | DECIMAL = 0 8 | TINY = 1 9 | SHORT = 2 10 | LONG = 3 11 | FLOAT = 4 12 | DOUBLE = 5 13 | NULL = 6 14 | TIMESTAMP = 7 15 | LONGLONG = 8 16 | INT24 = 9 17 | DATE = 10 18 | TIME = 11 19 | DATETIME = 12 20 | YEAR = 13 21 | # NEWDATE = 14 # Internal to MySQL. 22 | VARCHAR = 15 23 | BIT = 16 24 | # TIMESTAMP2 = 17 25 | # DATETIME2 = 18 26 | # TIME2 = 19 27 | JSON = 245 28 | NEWDECIMAL = 246 29 | ENUM = 247 30 | SET = 248 31 | TINY_BLOB = 249 32 | MEDIUM_BLOB = 250 33 | LONG_BLOB = 251 34 | BLOB = 252 35 | VAR_STRING = 253 36 | STRING = 254 37 | GEOMETRY = 255 38 | 39 | CHAR = TINY 40 | INTERVAL = ENUM 41 | -------------------------------------------------------------------------------- /src/MySQLdb/constants/FLAG.py: -------------------------------------------------------------------------------- 1 | """MySQL FLAG Constants 2 | 3 | These flags are used along with the FIELD_TYPE to indicate various 4 | properties of columns in a result set. 5 | 6 | """ 7 | 8 | NOT_NULL = 1 9 | PRI_KEY = 2 10 | UNIQUE_KEY = 4 11 | MULTIPLE_KEY = 8 12 | BLOB = 16 13 | UNSIGNED = 32 14 | ZEROFILL = 64 15 | BINARY = 128 16 | ENUM = 256 17 | AUTO_INCREMENT = 512 18 | TIMESTAMP = 1024 19 | SET = 2048 20 | NUM = 32768 21 | PART_KEY = 16384 22 | GROUP = 32768 23 | UNIQUE = 65536 24 | -------------------------------------------------------------------------------- /src/MySQLdb/constants/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["CR", "FIELD_TYPE", "CLIENT", "ER", "FLAG"] 2 | -------------------------------------------------------------------------------- /src/MySQLdb/converters.py: -------------------------------------------------------------------------------- 1 | """MySQLdb type conversion module 2 | 3 | This module handles all the type conversions for MySQL. If the default 4 | type conversions aren't what you need, you can make your own. The 5 | dictionary conversions maps some kind of type to a conversion function 6 | which returns the corresponding value: 7 | 8 | Key: FIELD_TYPE.* (from MySQLdb.constants) 9 | 10 | Conversion function: 11 | 12 | Arguments: string 13 | 14 | Returns: Python object 15 | 16 | Key: Python type object (from types) or class 17 | 18 | Conversion function: 19 | 20 | Arguments: Python object of indicated type or class AND 21 | conversion dictionary 22 | 23 | Returns: SQL literal value 24 | 25 | Notes: Most conversion functions can ignore the dictionary, but 26 | it is a required parameter. It is necessary for converting 27 | things like sequences and instances. 28 | 29 | Don't modify conversions if you can avoid it. Instead, make copies 30 | (with the copy() method), modify the copies, and then pass them to 31 | MySQL.connect(). 32 | """ 33 | from decimal import Decimal 34 | 35 | from MySQLdb._mysql import string_literal 36 | from MySQLdb.constants import FIELD_TYPE, FLAG 37 | from MySQLdb.times import ( 38 | Date, 39 | DateTimeType, 40 | DateTime2literal, 41 | DateTimeDeltaType, 42 | DateTimeDelta2literal, 43 | DateTime_or_None, 44 | TimeDelta_or_None, 45 | Date_or_None, 46 | ) 47 | from MySQLdb._exceptions import ProgrammingError 48 | 49 | import array 50 | 51 | NoneType = type(None) 52 | 53 | try: 54 | ArrayType = array.ArrayType 55 | except AttributeError: 56 | ArrayType = array.array 57 | 58 | 59 | def Bool2Str(s, d): 60 | return b"1" if s else b"0" 61 | 62 | 63 | def Set2Str(s, d): 64 | # Only support ascii string. Not tested. 65 | return string_literal(",".join(s)) 66 | 67 | 68 | def Thing2Str(s, d): 69 | """Convert something into a string via str().""" 70 | return str(s) 71 | 72 | 73 | def Float2Str(o, d): 74 | s = repr(o) 75 | if s in ("inf", "-inf", "nan"): 76 | raise ProgrammingError("%s can not be used with MySQL" % s) 77 | if "e" not in s: 78 | s += "e0" 79 | return s 80 | 81 | 82 | def None2NULL(o, d): 83 | """Convert None to NULL.""" 84 | return b"NULL" 85 | 86 | 87 | def Thing2Literal(o, d): 88 | """Convert something into a SQL string literal. If using 89 | MySQL-3.23 or newer, string_literal() is a method of the 90 | _mysql.MYSQL object, and this function will be overridden with 91 | that method when the connection is created.""" 92 | return string_literal(o) 93 | 94 | 95 | def Decimal2Literal(o, d): 96 | return format(o, "f") 97 | 98 | 99 | def array2Str(o, d): 100 | return Thing2Literal(o.tostring(), d) 101 | 102 | 103 | # bytes or str regarding to BINARY_FLAG. 104 | _bytes_or_str = ((FLAG.BINARY, bytes), (None, str)) 105 | 106 | conversions = { 107 | int: Thing2Str, 108 | float: Float2Str, 109 | NoneType: None2NULL, 110 | ArrayType: array2Str, 111 | bool: Bool2Str, 112 | Date: Thing2Literal, 113 | DateTimeType: DateTime2literal, 114 | DateTimeDeltaType: DateTimeDelta2literal, 115 | set: Set2Str, 116 | Decimal: Decimal2Literal, 117 | FIELD_TYPE.TINY: int, 118 | FIELD_TYPE.SHORT: int, 119 | FIELD_TYPE.LONG: int, 120 | FIELD_TYPE.FLOAT: float, 121 | FIELD_TYPE.DOUBLE: float, 122 | FIELD_TYPE.DECIMAL: Decimal, 123 | FIELD_TYPE.NEWDECIMAL: Decimal, 124 | FIELD_TYPE.LONGLONG: int, 125 | FIELD_TYPE.INT24: int, 126 | FIELD_TYPE.YEAR: int, 127 | FIELD_TYPE.TIMESTAMP: DateTime_or_None, 128 | FIELD_TYPE.DATETIME: DateTime_or_None, 129 | FIELD_TYPE.TIME: TimeDelta_or_None, 130 | FIELD_TYPE.DATE: Date_or_None, 131 | FIELD_TYPE.TINY_BLOB: bytes, 132 | FIELD_TYPE.MEDIUM_BLOB: bytes, 133 | FIELD_TYPE.LONG_BLOB: bytes, 134 | FIELD_TYPE.BLOB: bytes, 135 | FIELD_TYPE.STRING: bytes, 136 | FIELD_TYPE.VAR_STRING: bytes, 137 | FIELD_TYPE.VARCHAR: bytes, 138 | FIELD_TYPE.JSON: bytes, 139 | } 140 | -------------------------------------------------------------------------------- /src/MySQLdb/cursors.py: -------------------------------------------------------------------------------- 1 | """MySQLdb Cursors 2 | 3 | This module implements Cursors of various types for MySQLdb. By 4 | default, MySQLdb uses the Cursor class. 5 | """ 6 | import re 7 | 8 | from ._exceptions import ProgrammingError 9 | 10 | 11 | #: Regular expression for ``Cursor.executemany```. 12 | #: executemany only supports simple bulk insert. 13 | #: You can use it to load large dataset. 14 | RE_INSERT_VALUES = re.compile( 15 | "".join( 16 | [ 17 | r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)", 18 | r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))", 19 | r"(\s*(?:ON DUPLICATE.*)?);?\s*\Z", 20 | ] 21 | ), 22 | re.IGNORECASE | re.DOTALL, 23 | ) 24 | 25 | 26 | class BaseCursor: 27 | """A base for Cursor classes. Useful attributes: 28 | 29 | description 30 | A tuple of DB API 7-tuples describing the columns in 31 | the last executed query; see PEP-249 for details. 32 | 33 | description_flags 34 | Tuple of column flags for last query, one entry per column 35 | in the result set. Values correspond to those in 36 | MySQLdb.constants.FLAG. See MySQL documentation (C API) 37 | for more information. Non-standard extension. 38 | 39 | arraysize 40 | default number of rows fetchmany() will fetch 41 | """ 42 | 43 | #: Max statement size which :meth:`executemany` generates. 44 | #: 45 | #: Max size of allowed statement is max_allowed_packet - packet_header_size. 46 | #: Default value of max_allowed_packet is 1048576. 47 | max_stmt_length = 64 * 1024 48 | 49 | from ._exceptions import ( 50 | MySQLError, 51 | Warning, 52 | Error, 53 | InterfaceError, 54 | DatabaseError, 55 | DataError, 56 | OperationalError, 57 | IntegrityError, 58 | InternalError, 59 | ProgrammingError, 60 | NotSupportedError, 61 | ) 62 | 63 | connection = None 64 | 65 | def __init__(self, connection): 66 | self.connection = connection 67 | self.description = None 68 | self.description_flags = None 69 | self.rowcount = 0 70 | self.arraysize = 1 71 | self._executed = None 72 | 73 | self.lastrowid = None 74 | self._result = None 75 | self.rownumber = None 76 | self._rows = None 77 | 78 | def _discard(self): 79 | self.description = None 80 | self.description_flags = None 81 | # Django uses some member after __exit__. 82 | # So we keep rowcount and lastrowid here. They are cleared in Cursor._query(). 83 | # self.rowcount = 0 84 | # self.lastrowid = None 85 | self._rows = None 86 | self.rownumber = None 87 | 88 | if self._result: 89 | self._result.discard() 90 | self._result = None 91 | 92 | con = self.connection 93 | if con is None: 94 | return 95 | while con.next_result() == 0: # -1 means no more data. 96 | con.discard_result() 97 | 98 | def close(self): 99 | """Close the cursor. No further queries will be possible.""" 100 | try: 101 | if self.connection is None: 102 | return 103 | self._discard() 104 | finally: 105 | self.connection = None 106 | self._result = None 107 | 108 | def __enter__(self): 109 | return self 110 | 111 | def __exit__(self, *exc_info): 112 | del exc_info 113 | self.close() 114 | 115 | def _check_executed(self): 116 | if not self._executed: 117 | raise ProgrammingError("execute() first") 118 | 119 | def nextset(self): 120 | """Advance to the next result set. 121 | 122 | Returns None if there are no more result sets. 123 | """ 124 | if self._executed: 125 | self.fetchall() 126 | 127 | db = self._get_db() 128 | nr = db.next_result() 129 | if nr == -1: 130 | return None 131 | self._do_get_result(db) 132 | self._post_get_result() 133 | return 1 134 | 135 | def _do_get_result(self, db): 136 | self._result = result = self._get_result() 137 | if result is None: 138 | self.description = self.description_flags = None 139 | else: 140 | self.description = result.describe() 141 | self.description_flags = result.field_flags() 142 | 143 | self.rowcount = db.affected_rows() 144 | self.rownumber = 0 145 | self.lastrowid = db.insert_id() 146 | 147 | def _post_get_result(self): 148 | pass 149 | 150 | def setinputsizes(self, *args): 151 | """Does nothing, required by DB API.""" 152 | 153 | def setoutputsizes(self, *args): 154 | """Does nothing, required by DB API.""" 155 | 156 | def _get_db(self): 157 | con = self.connection 158 | if con is None: 159 | raise ProgrammingError("cursor closed") 160 | return con 161 | 162 | def execute(self, query, args=None): 163 | """Execute a query. 164 | 165 | query -- string, query to execute on server 166 | args -- optional sequence or mapping, parameters to use with query. 167 | 168 | Note: If args is a sequence, then %s must be used as the 169 | parameter placeholder in the query. If a mapping is used, 170 | %(key)s must be used as the placeholder. 171 | 172 | Returns integer represents rows affected, if any 173 | """ 174 | self._discard() 175 | 176 | mogrified_query = self._mogrify(query, args) 177 | 178 | assert isinstance(mogrified_query, (bytes, bytearray)) 179 | res = self._query(mogrified_query) 180 | return res 181 | 182 | def _mogrify(self, query, args=None): 183 | """Return query after binding args.""" 184 | db = self._get_db() 185 | 186 | if isinstance(query, str): 187 | query = query.encode(db.encoding) 188 | 189 | if args is not None: 190 | if isinstance(args, dict): 191 | nargs = {} 192 | for key, item in args.items(): 193 | if isinstance(key, str): 194 | key = key.encode(db.encoding) 195 | nargs[key] = db.literal(item) 196 | args = nargs 197 | else: 198 | args = tuple(map(db.literal, args)) 199 | try: 200 | query = query % args 201 | except TypeError as m: 202 | raise ProgrammingError(str(m)) 203 | 204 | return query 205 | 206 | def mogrify(self, query, args=None): 207 | """Return query after binding args. 208 | 209 | query -- string, query to mogrify 210 | args -- optional sequence or mapping, parameters to use with query. 211 | 212 | Note: If args is a sequence, then %s must be used as the 213 | parameter placeholder in the query. If a mapping is used, 214 | %(key)s must be used as the placeholder. 215 | 216 | Returns string representing query that would be executed by the server 217 | """ 218 | return self._mogrify(query, args).decode(self._get_db().encoding) 219 | 220 | def executemany(self, query, args): 221 | # type: (str, list) -> int 222 | """Execute a multi-row query. 223 | 224 | :param query: query to execute on server 225 | :param args: Sequence of sequences or mappings. It is used as parameter. 226 | :return: Number of rows affected, if any. 227 | 228 | This method improves performance on multiple-row INSERT and 229 | REPLACE. Otherwise it is equivalent to looping over args with 230 | execute(). 231 | """ 232 | if not args: 233 | return 234 | 235 | m = RE_INSERT_VALUES.match(query) 236 | if m: 237 | q_prefix = m.group(1) % () 238 | q_values = m.group(2).rstrip() 239 | q_postfix = m.group(3) or "" 240 | assert q_values[0] == "(" and q_values[-1] == ")" 241 | return self._do_execute_many( 242 | q_prefix, 243 | q_values, 244 | q_postfix, 245 | args, 246 | self.max_stmt_length, 247 | self._get_db().encoding, 248 | ) 249 | 250 | self.rowcount = sum(self.execute(query, arg) for arg in args) 251 | return self.rowcount 252 | 253 | def _do_execute_many( 254 | self, prefix, values, postfix, args, max_stmt_length, encoding 255 | ): 256 | if isinstance(prefix, str): 257 | prefix = prefix.encode(encoding) 258 | if isinstance(values, str): 259 | values = values.encode(encoding) 260 | if isinstance(postfix, str): 261 | postfix = postfix.encode(encoding) 262 | sql = bytearray(prefix) 263 | args = iter(args) 264 | v = self._mogrify(values, next(args)) 265 | sql += v 266 | rows = 0 267 | for arg in args: 268 | v = self._mogrify(values, arg) 269 | if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: 270 | rows += self.execute(sql + postfix) 271 | sql = bytearray(prefix) 272 | else: 273 | sql += b"," 274 | sql += v 275 | rows += self.execute(sql + postfix) 276 | self.rowcount = rows 277 | return rows 278 | 279 | def callproc(self, procname, args=()): 280 | """Execute stored procedure procname with args 281 | 282 | procname -- string, name of procedure to execute on server 283 | 284 | args -- Sequence of parameters to use with procedure 285 | 286 | Returns the original args. 287 | 288 | Compatibility warning: PEP-249 specifies that any modified 289 | parameters must be returned. This is currently impossible 290 | as they are only available by storing them in a server 291 | variable and then retrieved by a query. Since stored 292 | procedures return zero or more result sets, there is no 293 | reliable way to get at OUT or INOUT parameters via callproc. 294 | The server variables are named @_procname_n, where procname 295 | is the parameter above and n is the position of the parameter 296 | (from zero). Once all result sets generated by the procedure 297 | have been fetched, you can issue a SELECT @_procname_0, ... 298 | query using .execute() to get any OUT or INOUT values. 299 | 300 | Compatibility warning: The act of calling a stored procedure 301 | itself creates an empty result set. This appears after any 302 | result sets generated by the procedure. This is non-standard 303 | behavior with respect to the DB-API. Be sure to use nextset() 304 | to advance through all result sets; otherwise you may get 305 | disconnected. 306 | """ 307 | db = self._get_db() 308 | if isinstance(procname, str): 309 | procname = procname.encode(db.encoding) 310 | if args: 311 | fmt = b"@_" + procname + b"_%d=%s" 312 | q = b"SET %s" % b",".join( 313 | fmt % (index, db.literal(arg)) for index, arg in enumerate(args) 314 | ) 315 | self._query(q) 316 | self.nextset() 317 | 318 | q = b"CALL %s(%s)" % ( 319 | procname, 320 | b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]), 321 | ) 322 | self._query(q) 323 | return args 324 | 325 | def _query(self, q): 326 | db = self._get_db() 327 | self._result = None 328 | self.rowcount = None 329 | self.lastrowid = None 330 | db.query(q) 331 | self._do_get_result(db) 332 | self._post_get_result() 333 | self._executed = q 334 | return self.rowcount 335 | 336 | def _fetch_row(self, size=1): 337 | if not self._result: 338 | return () 339 | return self._result.fetch_row(size, self._fetch_type) 340 | 341 | def __iter__(self): 342 | return iter(self.fetchone, None) 343 | 344 | Warning = Warning 345 | Error = Error 346 | InterfaceError = InterfaceError 347 | DatabaseError = DatabaseError 348 | DataError = DataError 349 | OperationalError = OperationalError 350 | IntegrityError = IntegrityError 351 | InternalError = InternalError 352 | ProgrammingError = ProgrammingError 353 | NotSupportedError = NotSupportedError 354 | 355 | 356 | class CursorStoreResultMixIn: 357 | """This is a MixIn class which causes the entire result set to be 358 | stored on the client side, i.e. it uses mysql_store_result(). If the 359 | result set can be very large, consider adding a LIMIT clause to your 360 | query, or using CursorUseResultMixIn instead.""" 361 | 362 | def _get_result(self): 363 | return self._get_db().store_result() 364 | 365 | def _post_get_result(self): 366 | self._rows = self._fetch_row(0) 367 | self._result = None 368 | 369 | def fetchone(self): 370 | """Fetches a single row from the cursor. None indicates that 371 | no more rows are available.""" 372 | self._check_executed() 373 | if self.rownumber >= len(self._rows): 374 | return None 375 | result = self._rows[self.rownumber] 376 | self.rownumber = self.rownumber + 1 377 | return result 378 | 379 | def fetchmany(self, size=None): 380 | """Fetch up to size rows from the cursor. Result set may be smaller 381 | than size. If size is not defined, cursor.arraysize is used.""" 382 | self._check_executed() 383 | end = self.rownumber + (size or self.arraysize) 384 | result = self._rows[self.rownumber : end] 385 | self.rownumber = min(end, len(self._rows)) 386 | return result 387 | 388 | def fetchall(self): 389 | """Fetches all available rows from the cursor.""" 390 | self._check_executed() 391 | if self.rownumber: 392 | result = self._rows[self.rownumber :] 393 | else: 394 | result = self._rows 395 | self.rownumber = len(self._rows) 396 | return result 397 | 398 | def scroll(self, value, mode="relative"): 399 | """Scroll the cursor in the result set to a new position according 400 | to mode. 401 | 402 | If mode is 'relative' (default), value is taken as offset to 403 | the current position in the result set, if set to 'absolute', 404 | value states an absolute target position.""" 405 | self._check_executed() 406 | if mode == "relative": 407 | r = self.rownumber + value 408 | elif mode == "absolute": 409 | r = value 410 | else: 411 | raise ProgrammingError("unknown scroll mode %s" % repr(mode)) 412 | if r < 0 or r >= len(self._rows): 413 | raise IndexError("out of range") 414 | self.rownumber = r 415 | 416 | def __iter__(self): 417 | self._check_executed() 418 | result = self.rownumber and self._rows[self.rownumber :] or self._rows 419 | return iter(result) 420 | 421 | 422 | class CursorUseResultMixIn: 423 | 424 | """This is a MixIn class which causes the result set to be stored 425 | in the server and sent row-by-row to client side, i.e. it uses 426 | mysql_use_result(). You MUST retrieve the entire result set and 427 | close() the cursor before additional queries can be performed on 428 | the connection.""" 429 | 430 | def _get_result(self): 431 | return self._get_db().use_result() 432 | 433 | def fetchone(self): 434 | """Fetches a single row from the cursor.""" 435 | self._check_executed() 436 | r = self._fetch_row(1) 437 | if not r: 438 | return None 439 | self.rownumber = self.rownumber + 1 440 | return r[0] 441 | 442 | def fetchmany(self, size=None): 443 | """Fetch up to size rows from the cursor. Result set may be smaller 444 | than size. If size is not defined, cursor.arraysize is used.""" 445 | self._check_executed() 446 | r = self._fetch_row(size or self.arraysize) 447 | self.rownumber = self.rownumber + len(r) 448 | return r 449 | 450 | def fetchall(self): 451 | """Fetches all available rows from the cursor.""" 452 | self._check_executed() 453 | r = self._fetch_row(0) 454 | self.rownumber = self.rownumber + len(r) 455 | return r 456 | 457 | def __iter__(self): 458 | return self 459 | 460 | def next(self): 461 | row = self.fetchone() 462 | if row is None: 463 | raise StopIteration 464 | return row 465 | 466 | __next__ = next 467 | 468 | 469 | class CursorTupleRowsMixIn: 470 | """This is a MixIn class that causes all rows to be returned as tuples, 471 | which is the standard form required by DB API.""" 472 | 473 | _fetch_type = 0 474 | 475 | 476 | class CursorDictRowsMixIn: 477 | """This is a MixIn class that causes all rows to be returned as 478 | dictionaries. This is a non-standard feature.""" 479 | 480 | _fetch_type = 1 481 | 482 | 483 | class Cursor(CursorStoreResultMixIn, CursorTupleRowsMixIn, BaseCursor): 484 | """This is the standard Cursor class that returns rows as tuples 485 | and stores the result set in the client.""" 486 | 487 | 488 | class DictCursor(CursorStoreResultMixIn, CursorDictRowsMixIn, BaseCursor): 489 | """This is a Cursor class that returns rows as dictionaries and 490 | stores the result set in the client.""" 491 | 492 | 493 | class SSCursor(CursorUseResultMixIn, CursorTupleRowsMixIn, BaseCursor): 494 | """This is a Cursor class that returns rows as tuples and stores 495 | the result set in the server.""" 496 | 497 | 498 | class SSDictCursor(CursorUseResultMixIn, CursorDictRowsMixIn, BaseCursor): 499 | """This is a Cursor class that returns rows as dictionaries and 500 | stores the result set in the server.""" 501 | -------------------------------------------------------------------------------- /src/MySQLdb/release.py: -------------------------------------------------------------------------------- 1 | __author__ = "Inada Naoki " 2 | __version__ = "2.2.7" 3 | version_info = (2, 2, 7, "final", 0) 4 | -------------------------------------------------------------------------------- /src/MySQLdb/times.py: -------------------------------------------------------------------------------- 1 | """times module 2 | 3 | This module provides some Date and Time classes for dealing with MySQL data. 4 | 5 | Use Python datetime module to handle date and time columns. 6 | """ 7 | from time import localtime 8 | from datetime import date, datetime, time, timedelta 9 | from MySQLdb._mysql import string_literal 10 | 11 | Date = date 12 | Time = time 13 | TimeDelta = timedelta 14 | Timestamp = datetime 15 | 16 | DateTimeDeltaType = timedelta 17 | DateTimeType = datetime 18 | 19 | 20 | def DateFromTicks(ticks): 21 | """Convert UNIX ticks into a date instance.""" 22 | return date(*localtime(ticks)[:3]) 23 | 24 | 25 | def TimeFromTicks(ticks): 26 | """Convert UNIX ticks into a time instance.""" 27 | return time(*localtime(ticks)[3:6]) 28 | 29 | 30 | def TimestampFromTicks(ticks): 31 | """Convert UNIX ticks into a datetime instance.""" 32 | return datetime(*localtime(ticks)[:6]) 33 | 34 | 35 | format_TIME = format_DATE = str 36 | 37 | 38 | def format_TIMEDELTA(v): 39 | seconds = int(v.seconds) % 60 40 | minutes = int(v.seconds // 60) % 60 41 | hours = int(v.seconds // 3600) % 24 42 | return "%d %d:%d:%d" % (v.days, hours, minutes, seconds) 43 | 44 | 45 | def format_TIMESTAMP(d): 46 | """ 47 | :type d: datetime.datetime 48 | """ 49 | if d.microsecond: 50 | fmt = " ".join( 51 | [ 52 | "{0.year:04}-{0.month:02}-{0.day:02}", 53 | "{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}", 54 | ] 55 | ) 56 | else: 57 | fmt = " ".join( 58 | [ 59 | "{0.year:04}-{0.month:02}-{0.day:02}", 60 | "{0.hour:02}:{0.minute:02}:{0.second:02}", 61 | ] 62 | ) 63 | return fmt.format(d) 64 | 65 | 66 | def DateTime_or_None(s): 67 | try: 68 | if len(s) < 11: 69 | return Date_or_None(s) 70 | 71 | micros = s[20:] 72 | 73 | if len(micros) == 0: 74 | # 12:00:00 75 | micros = 0 76 | elif len(micros) < 7: 77 | # 12:00:00.123456 78 | micros = int(micros) * 10 ** (6 - len(micros)) 79 | else: 80 | return None 81 | 82 | return datetime( 83 | int(s[:4]), # year 84 | int(s[5:7]), # month 85 | int(s[8:10]), # day 86 | int(s[11:13] or 0), # hour 87 | int(s[14:16] or 0), # minute 88 | int(s[17:19] or 0), # second 89 | micros, # microsecond 90 | ) 91 | except ValueError: 92 | return None 93 | 94 | 95 | def TimeDelta_or_None(s): 96 | try: 97 | h, m, s = s.split(":") 98 | if "." in s: 99 | s, ms = s.split(".") 100 | ms = ms.ljust(6, "0") 101 | else: 102 | ms = 0 103 | if h[0] == "-": 104 | negative = True 105 | else: 106 | negative = False 107 | h, m, s, ms = abs(int(h)), int(m), int(s), int(ms) 108 | td = timedelta(hours=h, minutes=m, seconds=s, microseconds=ms) 109 | if negative: 110 | return -td 111 | else: 112 | return td 113 | except ValueError: 114 | # unpacking or int/float conversion failed 115 | return None 116 | 117 | 118 | def Time_or_None(s): 119 | try: 120 | h, m, s = s.split(":") 121 | if "." in s: 122 | s, ms = s.split(".") 123 | ms = ms.ljust(6, "0") 124 | else: 125 | ms = 0 126 | h, m, s, ms = int(h), int(m), int(s), int(ms) 127 | return time(hour=h, minute=m, second=s, microsecond=ms) 128 | except ValueError: 129 | return None 130 | 131 | 132 | def Date_or_None(s): 133 | try: 134 | return date( 135 | int(s[:4]), 136 | int(s[5:7]), 137 | int(s[8:10]), 138 | ) # year # month # day 139 | except ValueError: 140 | return None 141 | 142 | 143 | def DateTime2literal(d, c): 144 | """Format a DateTime object as an ISO timestamp.""" 145 | return string_literal(format_TIMESTAMP(d)) 146 | 147 | 148 | def DateTimeDelta2literal(d, c): 149 | """Format a DateTimeDelta object as a time.""" 150 | return string_literal(format_TIMEDELTA(d)) 151 | -------------------------------------------------------------------------------- /tests/actions.cnf: -------------------------------------------------------------------------------- 1 | # To create your own custom version of this file, read 2 | # http://dev.mysql.com/doc/refman/5.1/en/option-files.html 3 | # and set TESTDB in your environment to the name of the file 4 | 5 | [MySQLdb-tests] 6 | host = 127.0.0.1 7 | port = 3306 8 | user = root 9 | database = mysqldb_test 10 | password = root 11 | default-character-set = utf8mb4 12 | -------------------------------------------------------------------------------- /tests/capabilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -O 2 | """ Script to test database capabilities and the DB-API interface 3 | for functionality and memory leaks. 4 | 5 | Adapted from a script by M-A Lemburg. 6 | 7 | """ 8 | from time import time 9 | import unittest 10 | from configdb import connection_factory 11 | 12 | 13 | class DatabaseTest(unittest.TestCase): 14 | db_module = None 15 | connect_args = () 16 | connect_kwargs = dict() 17 | create_table_extra = "" 18 | rows = 10 19 | debug = False 20 | 21 | def setUp(self): 22 | db = connection_factory(**self.connect_kwargs) 23 | self.connection = db 24 | self.cursor = db.cursor() 25 | self.BLOBUText = "".join([chr(i) for i in range(16384)]) 26 | self.BLOBBinary = self.db_module.Binary( 27 | ("".join([chr(i) for i in range(256)] * 16)).encode("latin1") 28 | ) 29 | 30 | leak_test = True 31 | 32 | def tearDown(self): 33 | if self.leak_test: 34 | import gc 35 | 36 | del self.cursor 37 | orphans = gc.collect() 38 | self.assertFalse( 39 | orphans, "%d orphaned objects found after deleting cursor" % orphans 40 | ) 41 | 42 | del self.connection 43 | orphans = gc.collect() 44 | self.assertFalse( 45 | orphans, "%d orphaned objects found after deleting connection" % orphans 46 | ) 47 | 48 | def table_exists(self, name): 49 | try: 50 | self.cursor.execute("select * from %s where 1=0" % name) 51 | except Exception: 52 | return False 53 | else: 54 | return True 55 | 56 | def quote_identifier(self, ident): 57 | return '"%s"' % ident 58 | 59 | def new_table_name(self): 60 | i = id(self.cursor) 61 | while True: 62 | name = self.quote_identifier("tb%08x" % i) 63 | if not self.table_exists(name): 64 | return name 65 | i = i + 1 66 | 67 | def create_table(self, columndefs): 68 | """Create a table using a list of column definitions given in 69 | columndefs. 70 | 71 | generator must be a function taking arguments (row_number, 72 | col_number) returning a suitable data object for insertion 73 | into the table. 74 | 75 | """ 76 | self.table = self.new_table_name() 77 | self.cursor.execute( 78 | "CREATE TABLE %s (%s) %s" 79 | % (self.table, ",\n".join(columndefs), self.create_table_extra) 80 | ) 81 | 82 | def check_data_integrity(self, columndefs, generator): 83 | # insert 84 | self.create_table(columndefs) 85 | insert_statement = "INSERT INTO {} VALUES ({})".format( 86 | self.table, 87 | ",".join(["%s"] * len(columndefs)), 88 | ) 89 | data = [ 90 | [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) 91 | ] 92 | self.cursor.executemany(insert_statement, data) 93 | self.connection.commit() 94 | # verify 95 | self.cursor.execute("select * from %s" % self.table) 96 | res = self.cursor.fetchall() 97 | self.assertEqual(len(res), self.rows) 98 | try: 99 | for i in range(self.rows): 100 | for j in range(len(columndefs)): 101 | self.assertEqual(res[i][j], generator(i, j)) 102 | finally: 103 | if not self.debug: 104 | self.cursor.execute("drop table %s" % (self.table)) 105 | 106 | def test_transactions(self): 107 | columndefs = ("col1 INT", "col2 VARCHAR(255)") 108 | 109 | def generator(row, col): 110 | if col == 0: 111 | return row 112 | else: 113 | return ("%i" % (row % 10)) * 255 114 | 115 | self.create_table(columndefs) 116 | insert_statement = "INSERT INTO {} VALUES ({})".format( 117 | self.table, 118 | ",".join(["%s"] * len(columndefs)), 119 | ) 120 | data = [ 121 | [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) 122 | ] 123 | self.cursor.executemany(insert_statement, data) 124 | # verify 125 | self.connection.commit() 126 | self.cursor.execute("select * from %s" % self.table) 127 | res = self.cursor.fetchall() 128 | self.assertEqual(len(res), self.rows) 129 | for i in range(self.rows): 130 | for j in range(len(columndefs)): 131 | self.assertEqual(res[i][j], generator(i, j)) 132 | delete_statement = "delete from %s where col1=%%s" % self.table 133 | self.cursor.execute(delete_statement, (0,)) 134 | self.cursor.execute(f"select col1 from {self.table} where col1=%s", (0,)) 135 | res = self.cursor.fetchall() 136 | self.assertFalse(res, "DELETE didn't work") 137 | self.connection.rollback() 138 | self.cursor.execute(f"select col1 from {self.table} where col1=%s", (0,)) 139 | res = self.cursor.fetchall() 140 | self.assertTrue(len(res) == 1, "ROLLBACK didn't work") 141 | self.cursor.execute("drop table %s" % (self.table)) 142 | 143 | def test_truncation(self): 144 | columndefs = ("col1 INT", "col2 VARCHAR(255)") 145 | 146 | def generator(row, col): 147 | if col == 0: 148 | return row 149 | else: 150 | return ("%i" % (row % 10)) * ((255 - self.rows // 2) + row) 151 | 152 | self.create_table(columndefs) 153 | insert_statement = "INSERT INTO {} VALUES ({})".format( 154 | self.table, 155 | ",".join(["%s"] * len(columndefs)), 156 | ) 157 | 158 | try: 159 | self.cursor.execute(insert_statement, (0, "0" * 256)) 160 | except self.connection.DataError: 161 | pass 162 | else: 163 | self.fail( 164 | "Over-long column did not generate warnings/exception with single insert" # noqa: E501 165 | ) 166 | 167 | self.connection.rollback() 168 | 169 | try: 170 | for i in range(self.rows): 171 | data = [] 172 | for j in range(len(columndefs)): 173 | data.append(generator(i, j)) 174 | self.cursor.execute(insert_statement, tuple(data)) 175 | except self.connection.DataError: 176 | pass 177 | else: 178 | self.fail( 179 | "Over-long columns did not generate warnings/exception with execute()" # noqa: E501 180 | ) 181 | 182 | self.connection.rollback() 183 | 184 | try: 185 | data = [ 186 | [generator(i, j) for j in range(len(columndefs))] 187 | for i in range(self.rows) 188 | ] 189 | self.cursor.executemany(insert_statement, data) 190 | except self.connection.DataError: 191 | pass 192 | else: 193 | self.fail( 194 | "Over-long columns did not generate warnings/exception with executemany()" # noqa: E501 195 | ) 196 | 197 | self.connection.rollback() 198 | self.cursor.execute("drop table %s" % (self.table)) 199 | 200 | def test_CHAR(self): 201 | # Character data 202 | def generator(row, col): 203 | return ("%i" % ((row + col) % 10)) * 255 204 | 205 | self.check_data_integrity(("col1 char(255)", "col2 char(255)"), generator) 206 | 207 | def test_INT(self): 208 | # Number data 209 | def generator(row, col): 210 | return row * row 211 | 212 | self.check_data_integrity(("col1 INT",), generator) 213 | 214 | def test_DECIMAL(self): 215 | # DECIMAL 216 | from decimal import Decimal 217 | 218 | def generator(row, col): 219 | return Decimal("%d.%02d" % (row, col)) 220 | 221 | self.check_data_integrity(("col1 DECIMAL(5,2)",), generator) 222 | 223 | val = Decimal("1.11111111111111119E-7") 224 | self.cursor.execute("SELECT %s", (val,)) 225 | result = self.cursor.fetchone()[0] 226 | self.assertEqual(result, val) 227 | self.assertIsInstance(result, Decimal) 228 | 229 | self.cursor.execute("SELECT %s + %s", (Decimal("0.1"), Decimal("0.2"))) 230 | result = self.cursor.fetchone()[0] 231 | self.assertEqual(result, Decimal("0.3")) 232 | self.assertIsInstance(result, Decimal) 233 | 234 | def test_DATE(self): 235 | ticks = time() 236 | 237 | def generator(row, col): 238 | return self.db_module.DateFromTicks(ticks + row * 86400 - col * 1313) 239 | 240 | self.check_data_integrity(("col1 DATE",), generator) 241 | 242 | def test_TIME(self): 243 | ticks = time() 244 | 245 | def generator(row, col): 246 | return self.db_module.TimeFromTicks(ticks + row * 86400 - col * 1313) 247 | 248 | self.check_data_integrity(("col1 TIME",), generator) 249 | 250 | def test_DATETIME(self): 251 | ticks = time() 252 | 253 | def generator(row, col): 254 | return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) 255 | 256 | self.check_data_integrity(("col1 DATETIME",), generator) 257 | 258 | def test_TIMESTAMP(self): 259 | ticks = time() 260 | 261 | def generator(row, col): 262 | return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) 263 | 264 | self.check_data_integrity(("col1 TIMESTAMP",), generator) 265 | 266 | def test_fractional_TIMESTAMP(self): 267 | ticks = time() 268 | 269 | def generator(row, col): 270 | return self.db_module.TimestampFromTicks( 271 | ticks + row * 86400 - col * 1313 + row * 0.7 * col / 3.0 272 | ) 273 | 274 | self.check_data_integrity(("col1 TIMESTAMP",), generator) 275 | 276 | def test_LONG(self): 277 | def generator(row, col): 278 | if col == 0: 279 | return row 280 | else: 281 | return self.BLOBUText # 'BLOB Text ' * 1024 282 | 283 | self.check_data_integrity(("col1 INT", "col2 LONG"), generator) 284 | 285 | def test_TEXT(self): 286 | def generator(row, col): 287 | return self.BLOBUText # 'BLOB Text ' * 1024 288 | 289 | self.check_data_integrity(("col2 TEXT",), generator) 290 | 291 | def test_LONG_BYTE(self): 292 | def generator(row, col): 293 | if col == 0: 294 | return row 295 | else: 296 | return self.BLOBBinary # 'BLOB\000Binary ' * 1024 297 | 298 | self.check_data_integrity(("col1 INT", "col2 LONG BYTE"), generator) 299 | 300 | def test_BLOB(self): 301 | def generator(row, col): 302 | if col == 0: 303 | return row 304 | else: 305 | return self.BLOBBinary # 'BLOB\000Binary ' * 1024 306 | 307 | self.check_data_integrity(("col1 INT", "col2 BLOB"), generator) 308 | 309 | def test_DOUBLE(self): 310 | for val in (18014398509481982.0, 0.1): 311 | self.cursor.execute("SELECT %s", (val,)) 312 | result = self.cursor.fetchone()[0] 313 | self.assertEqual(result, val) 314 | self.assertIsInstance(result, float) 315 | -------------------------------------------------------------------------------- /tests/configdb.py: -------------------------------------------------------------------------------- 1 | """Configure database connection for tests.""" 2 | 3 | from os import environ, path 4 | 5 | tests_path = path.dirname(__file__) 6 | conf_file = environ.get("TESTDB", "default.cnf") 7 | conf_path = path.join(tests_path, conf_file) 8 | connect_kwargs = dict( 9 | read_default_file=conf_path, 10 | read_default_group="MySQLdb-tests", 11 | ) 12 | 13 | 14 | def connection_kwargs(kwargs): 15 | db_kwargs = connect_kwargs.copy() 16 | db_kwargs.update(kwargs) 17 | return db_kwargs 18 | 19 | 20 | def connection_factory(**kwargs): 21 | import MySQLdb 22 | 23 | db_kwargs = connection_kwargs(kwargs) 24 | db = MySQLdb.connect(**db_kwargs) 25 | return db 26 | -------------------------------------------------------------------------------- /tests/default.cnf: -------------------------------------------------------------------------------- 1 | # To create your own custom version of this file, read 2 | # http://dev.mysql.com/doc/refman/5.1/en/option-files.html 3 | # and set TESTDB in your environment to the name of the file 4 | 5 | # $ docker run -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 --rm --name mysqld mysql:latest 6 | [MySQLdb-tests] 7 | host = 127.0.0.1 8 | user = root 9 | database = test 10 | #password = 11 | default-character-set = utf8mb4 12 | -------------------------------------------------------------------------------- /tests/test_MySQLdb_capabilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import capabilities 3 | from datetime import timedelta 4 | from contextlib import closing 5 | import unittest 6 | import MySQLdb 7 | from configdb import connection_factory 8 | import warnings 9 | 10 | 11 | warnings.filterwarnings("ignore") 12 | 13 | 14 | class test_MySQLdb(capabilities.DatabaseTest): 15 | db_module = MySQLdb 16 | connect_args = () 17 | connect_kwargs = dict( 18 | use_unicode=True, sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL" 19 | ) 20 | create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" 21 | leak_test = False 22 | 23 | def quote_identifier(self, ident): 24 | return "`%s`" % ident 25 | 26 | def test_TIME(self): 27 | def generator(row, col): 28 | return timedelta(0, row * 8000) 29 | 30 | self.check_data_integrity(("col1 TIME",), generator) 31 | 32 | def test_TINYINT(self): 33 | # Number data 34 | def generator(row, col): 35 | v = (row * row) % 256 36 | if v > 127: 37 | v = v - 256 38 | return v 39 | 40 | self.check_data_integrity(("col1 TINYINT",), generator) 41 | 42 | def test_stored_procedures(self): 43 | db = self.connection 44 | c = self.cursor 45 | self.create_table(("pos INT", "tree CHAR(20)")) 46 | c.executemany( 47 | "INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, 48 | list(enumerate("ash birch cedar Lärche pine".split())), 49 | ) 50 | db.commit() 51 | 52 | c.execute( 53 | """ 54 | CREATE PROCEDURE test_sp(IN t VARCHAR(255)) 55 | BEGIN 56 | SELECT pos FROM %s WHERE tree = t; 57 | END 58 | """ 59 | % self.table 60 | ) 61 | db.commit() 62 | 63 | c.callproc("test_sp", ("Lärche",)) 64 | rows = c.fetchall() 65 | self.assertEqual(len(rows), 1) 66 | self.assertEqual(rows[0][0], 3) 67 | c.nextset() 68 | 69 | c.execute("DROP PROCEDURE test_sp") 70 | c.execute("drop table %s" % (self.table)) 71 | 72 | def test_small_CHAR(self): 73 | # Character data 74 | def generator(row, col): 75 | i = (row * col + 62) % 256 76 | if i == 62: 77 | return "" 78 | if i == 63: 79 | return None 80 | return chr(i) 81 | 82 | self.check_data_integrity(("col1 char(1)", "col2 char(1)"), generator) 83 | 84 | def test_BIT(self): 85 | c = self.cursor 86 | try: 87 | c.execute( 88 | """create table test_BIT ( 89 | b3 BIT(3), 90 | b7 BIT(10), 91 | b64 BIT(64))""" 92 | ) 93 | 94 | one64 = "1" * 64 95 | c.execute( 96 | "insert into test_BIT (b3, b7, b64)" 97 | " VALUES (b'011', b'1111111111', b'%s')" % one64 98 | ) 99 | 100 | c.execute("SELECT b3, b7, b64 FROM test_BIT") 101 | row = c.fetchone() 102 | self.assertEqual(row[0], b"\x03") 103 | self.assertEqual(row[1], b"\x03\xff") 104 | self.assertEqual(row[2], b"\xff" * 8) 105 | finally: 106 | c.execute("drop table if exists test_BIT") 107 | 108 | def test_MULTIPOLYGON(self): 109 | c = self.cursor 110 | try: 111 | c.execute( 112 | """create table test_MULTIPOLYGON ( 113 | id INTEGER PRIMARY KEY, 114 | border MULTIPOLYGON)""" 115 | ) 116 | 117 | c.execute( 118 | """ 119 | INSERT INTO test_MULTIPOLYGON 120 | (id, border) 121 | VALUES (1, 122 | ST_Geomfromtext( 123 | 'MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))')) 124 | """ 125 | ) 126 | 127 | c.execute("SELECT id, ST_AsText(border) FROM test_MULTIPOLYGON") 128 | row = c.fetchone() 129 | self.assertEqual(row[0], 1) 130 | self.assertEqual( 131 | row[1], 132 | "MULTIPOLYGON(((1 1,1 -1,-1 -1,-1 1,1 1)),((1 1,3 1,3 3,1 3,1 1)))", 133 | ) 134 | 135 | c.execute("SELECT id, ST_AsWKB(border) FROM test_MULTIPOLYGON") 136 | row = c.fetchone() 137 | self.assertEqual(row[0], 1) 138 | self.assertNotEqual(len(row[1]), 0) 139 | 140 | c.execute("SELECT id, border FROM test_MULTIPOLYGON") 141 | row = c.fetchone() 142 | self.assertEqual(row[0], 1) 143 | self.assertNotEqual(len(row[1]), 0) 144 | finally: 145 | c.execute("drop table if exists test_MULTIPOLYGON") 146 | 147 | def test_bug_2671682(self): 148 | from MySQLdb.constants import ER 149 | 150 | try: 151 | self.cursor.execute("describe some_non_existent_table") 152 | except self.connection.ProgrammingError as msg: 153 | self.assertTrue(str(ER.NO_SUCH_TABLE) in str(msg)) 154 | 155 | def test_bug_3514287(self): 156 | c = self.cursor 157 | try: 158 | c.execute( 159 | """create table bug_3541287 ( 160 | c1 CHAR(10), 161 | t1 TIMESTAMP)""" 162 | ) 163 | c.execute("insert into bug_3541287 (c1,t1) values (%s, NOW())", ("blah",)) 164 | finally: 165 | c.execute("drop table if exists bug_3541287") 166 | 167 | def test_ping(self): 168 | self.connection.ping() 169 | 170 | def test_reraise_exception(self): 171 | c = self.cursor 172 | try: 173 | c.execute("SELECT x FROM not_existing_table") 174 | except MySQLdb.ProgrammingError as e: 175 | self.assertEqual(e.args[0], 1146) 176 | return 177 | self.fail("Should raise ProgrammingError") 178 | 179 | def test_binary_prefix(self): 180 | # verify prefix behaviour when enabled, disabled and for default (disabled) 181 | for binary_prefix in (True, False, None): 182 | kwargs = self.connect_kwargs.copy() 183 | # needs to be set to can guarantee CHARSET response for normal strings 184 | kwargs["charset"] = "utf8mb4" 185 | if binary_prefix is not None: 186 | kwargs["binary_prefix"] = binary_prefix 187 | 188 | with closing(connection_factory(**kwargs)) as conn: 189 | with closing(conn.cursor()) as c: 190 | c.execute("SELECT CHARSET(%s)", (MySQLdb.Binary(b"raw bytes"),)) 191 | self.assertEqual( 192 | c.fetchall()[0][0], "binary" if binary_prefix else "utf8mb4" 193 | ) 194 | # normal strings should not get prefix 195 | c.execute("SELECT CHARSET(%s)", ("str",)) 196 | self.assertEqual(c.fetchall()[0][0], "utf8mb4") 197 | 198 | 199 | if __name__ == "__main__": 200 | if test_MySQLdb.leak_test: 201 | import gc 202 | 203 | gc.enable() 204 | gc.set_debug(gc.DEBUG_LEAK) 205 | unittest.main() 206 | -------------------------------------------------------------------------------- /tests/test_MySQLdb_dbapi20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import dbapi20 3 | import unittest 4 | import MySQLdb 5 | from configdb import connection_kwargs 6 | import warnings 7 | 8 | warnings.simplefilter("ignore") 9 | 10 | 11 | class test_MySQLdb(dbapi20.DatabaseAPI20Test): 12 | driver = MySQLdb 13 | connect_args = () 14 | connect_kw_args = connection_kwargs( 15 | dict(sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL") 16 | ) 17 | 18 | def test_setoutputsize(self): 19 | pass 20 | 21 | def test_setoutputsize_basic(self): 22 | pass 23 | 24 | """The tests on fetchone and fetchall and rowcount bogusly 25 | test for an exception if the statement cannot return a 26 | result set. MySQL always returns a result set; it's just that 27 | some things return empty result sets.""" 28 | 29 | def test_fetchall(self): 30 | con = self._connect() 31 | try: 32 | cur = con.cursor() 33 | # cursor.fetchall should raise an Error if called 34 | # without executing a query that may return rows (such 35 | # as a select) 36 | self.assertRaises(self.driver.Error, cur.fetchall) 37 | 38 | self.executeDDL1(cur) 39 | for sql in self._populate(): 40 | cur.execute(sql) 41 | 42 | # cursor.fetchall should raise an Error if called 43 | # after executing a statement that cannot return rows 44 | # self.assertRaises(self.driver.Error,cur.fetchall) 45 | 46 | cur.execute("select name from %sbooze" % self.table_prefix) 47 | rows = cur.fetchall() 48 | self.assertTrue(cur.rowcount in (-1, len(self.samples))) 49 | self.assertEqual( 50 | len(rows), 51 | len(self.samples), 52 | "cursor.fetchall did not retrieve all rows", 53 | ) 54 | rows = [r[0] for r in rows] 55 | rows.sort() 56 | for i in range(0, len(self.samples)): 57 | self.assertEqual( 58 | rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows" 59 | ) 60 | rows = cur.fetchall() 61 | self.assertEqual( 62 | len(rows), 63 | 0, 64 | "cursor.fetchall should return an empty list if called " 65 | "after the whole result set has been fetched", 66 | ) 67 | self.assertTrue(cur.rowcount in (-1, len(self.samples))) 68 | 69 | self.executeDDL2(cur) 70 | cur.execute("select name from %sbarflys" % self.table_prefix) 71 | rows = cur.fetchall() 72 | self.assertTrue(cur.rowcount in (-1, 0)) 73 | self.assertEqual( 74 | len(rows), 75 | 0, 76 | "cursor.fetchall should return an empty list if " 77 | "a select query returns no rows", 78 | ) 79 | 80 | finally: 81 | con.close() 82 | 83 | def test_fetchone(self): 84 | con = self._connect() 85 | try: 86 | cur = con.cursor() 87 | 88 | # cursor.fetchone should raise an Error if called before 89 | # executing a select-type query 90 | self.assertRaises(self.driver.Error, cur.fetchone) 91 | 92 | # cursor.fetchone should raise an Error if called after 93 | # executing a query that cannot return rows 94 | self.executeDDL1(cur) 95 | # self.assertRaises(self.driver.Error,cur.fetchone) 96 | 97 | cur.execute("select name from %sbooze" % self.table_prefix) 98 | self.assertEqual( 99 | cur.fetchone(), 100 | None, 101 | "cursor.fetchone should return None if a query retrieves " "no rows", 102 | ) 103 | self.assertTrue(cur.rowcount in (-1, 0)) 104 | 105 | # cursor.fetchone should raise an Error if called after 106 | # executing a query that cannot return rows 107 | cur.execute( 108 | "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) 109 | ) 110 | # self.assertRaises(self.driver.Error,cur.fetchone) 111 | 112 | cur.execute("select name from %sbooze" % self.table_prefix) 113 | r = cur.fetchone() 114 | self.assertEqual( 115 | len(r), 1, "cursor.fetchone should have retrieved a single row" 116 | ) 117 | self.assertEqual( 118 | r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data" 119 | ) 120 | # self.assertEqual( 121 | # cur.fetchone(), 122 | # None, 123 | # "cursor.fetchone should return None if no more rows available", 124 | # ) 125 | self.assertTrue(cur.rowcount in (-1, 1)) 126 | finally: 127 | con.close() 128 | 129 | # Same complaint as for fetchall and fetchone 130 | def test_rowcount(self): 131 | con = self._connect() 132 | try: 133 | cur = con.cursor() 134 | self.executeDDL1(cur) 135 | # self.assertEqual(cur.rowcount,-1, 136 | # 'cursor.rowcount should be -1 after executing no-result ' 137 | # 'statements' 138 | # ) 139 | cur.execute( 140 | "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) 141 | ) 142 | # self.assertTrue(cur.rowcount in (-1,1), 143 | # 'cursor.rowcount should == number or rows inserted, or ' 144 | # 'set to -1 after executing an insert statement' 145 | # ) 146 | cur.execute("select name from %sbooze" % self.table_prefix) 147 | self.assertTrue( 148 | cur.rowcount in (-1, 1), 149 | "cursor.rowcount should == number of rows returned, or " 150 | "set to -1 after executing a select statement", 151 | ) 152 | self.executeDDL2(cur) 153 | # self.assertEqual(cur.rowcount,-1, 154 | # 'cursor.rowcount not being reset to -1 after executing ' 155 | # 'no-result statements' 156 | # ) 157 | finally: 158 | con.close() 159 | 160 | def test_callproc(self): 161 | pass # performed in test_MySQL_capabilities 162 | 163 | def help_nextset_setUp(self, cur): 164 | """ 165 | Should create a procedure called deleteme 166 | that returns two result sets, first the 167 | number of rows in booze then "name from booze" 168 | """ 169 | sql = """ 170 | create procedure deleteme() 171 | begin 172 | select count(*) from %(tp)sbooze; 173 | select name from %(tp)sbooze; 174 | end 175 | """ % dict( 176 | tp=self.table_prefix 177 | ) 178 | cur.execute(sql) 179 | 180 | def help_nextset_tearDown(self, cur): 181 | "If cleaning up is needed after nextSetTest" 182 | cur.execute("drop procedure deleteme") 183 | 184 | def test_nextset(self): 185 | # from warnings import warn 186 | 187 | con = self._connect() 188 | try: 189 | cur = con.cursor() 190 | if not hasattr(cur, "nextset"): 191 | return 192 | 193 | try: 194 | self.executeDDL1(cur) 195 | sql = self._populate() 196 | for sql in self._populate(): 197 | cur.execute(sql) 198 | 199 | self.help_nextset_setUp(cur) 200 | 201 | cur.callproc("deleteme") 202 | numberofrows = cur.fetchone() 203 | assert numberofrows[0] == len(self.samples) 204 | assert cur.nextset() 205 | names = cur.fetchall() 206 | assert len(names) == len(self.samples) 207 | s = cur.nextset() 208 | if s: 209 | empty = cur.fetchall() 210 | self.assertEqual( 211 | len(empty), 0, "non-empty result set after other result sets" 212 | ) 213 | # warn( 214 | # ": ".join( 215 | # [ 216 | # "Incompatibility", 217 | # "MySQL returns an empty result set for the CALL itself" 218 | # ] 219 | # ), 220 | # Warning, 221 | # ) 222 | # assert s == None, "No more return sets, should return None" 223 | finally: 224 | self.help_nextset_tearDown(cur) 225 | 226 | finally: 227 | con.close() 228 | 229 | 230 | if __name__ == "__main__": 231 | unittest.main() 232 | -------------------------------------------------------------------------------- /tests/test_MySQLdb_nonstandard.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from MySQLdb import _mysql 4 | import MySQLdb 5 | from MySQLdb.constants import FIELD_TYPE 6 | from configdb import connection_factory 7 | import warnings 8 | 9 | warnings.simplefilter("ignore") 10 | 11 | 12 | class TestDBAPISet(unittest.TestCase): 13 | def test_set_equality(self): 14 | self.assertTrue(MySQLdb.STRING == MySQLdb.STRING) 15 | 16 | def test_set_inequality(self): 17 | self.assertTrue(MySQLdb.STRING != MySQLdb.NUMBER) 18 | 19 | def test_set_equality_membership(self): 20 | self.assertTrue(FIELD_TYPE.VAR_STRING == MySQLdb.STRING) 21 | 22 | def test_set_inequality_membership(self): 23 | self.assertTrue(FIELD_TYPE.DATE != MySQLdb.STRING) 24 | 25 | 26 | class TestCoreModule(unittest.TestCase): 27 | """Core _mysql module features.""" 28 | 29 | def test_version(self): 30 | """Version information sanity.""" 31 | self.assertTrue(isinstance(_mysql.__version__, str)) 32 | 33 | self.assertTrue(isinstance(_mysql.version_info, tuple)) 34 | self.assertEqual(len(_mysql.version_info), 5) 35 | 36 | def test_client_info(self): 37 | self.assertTrue(isinstance(_mysql.get_client_info(), str)) 38 | 39 | def test_escape_string(self): 40 | self.assertEqual( 41 | _mysql.escape_string(b'foo"bar'), b'foo\\"bar', "escape byte string" 42 | ) 43 | self.assertEqual( 44 | _mysql.escape_string('foo"bar'), b'foo\\"bar', "escape unicode string" 45 | ) 46 | 47 | 48 | class CoreAPI(unittest.TestCase): 49 | """Test _mysql interaction internals.""" 50 | 51 | def setUp(self): 52 | self.conn = connection_factory(use_unicode=True) 53 | 54 | def tearDown(self): 55 | self.conn.close() 56 | 57 | def test_thread_id(self): 58 | tid = self.conn.thread_id() 59 | self.assertTrue(isinstance(tid, int), "thread_id didn't return an int.") 60 | 61 | self.assertRaises( 62 | TypeError, 63 | self.conn.thread_id, 64 | ("evil",), 65 | "thread_id shouldn't accept arguments.", 66 | ) 67 | 68 | def test_affected_rows(self): 69 | self.assertEqual( 70 | self.conn.affected_rows(), 0, "Should return 0 before we do anything." 71 | ) 72 | 73 | # def test_debug(self): 74 | # (FIXME) Only actually tests if you lack SUPER 75 | # self.assertRaises(MySQLdb.OperationalError, 76 | # self.conn.dump_debug_info) 77 | 78 | def test_charset_name(self): 79 | self.assertTrue( 80 | isinstance(self.conn.character_set_name(), str), "Should return a string." 81 | ) 82 | 83 | def test_host_info(self): 84 | self.assertTrue( 85 | isinstance(self.conn.get_host_info(), str), "Should return a string." 86 | ) 87 | 88 | def test_proto_info(self): 89 | self.assertTrue( 90 | isinstance(self.conn.get_proto_info(), int), "Should return an int." 91 | ) 92 | 93 | def test_server_info(self): 94 | self.assertTrue( 95 | isinstance(self.conn.get_server_info(), str), "Should return a string." 96 | ) 97 | 98 | def test_client_flag(self): 99 | conn = connection_factory( 100 | use_unicode=True, client_flag=MySQLdb.constants.CLIENT.FOUND_ROWS 101 | ) 102 | 103 | self.assertIsInstance(conn.client_flag, int) 104 | self.assertTrue(conn.client_flag & MySQLdb.constants.CLIENT.FOUND_ROWS) 105 | with self.assertRaises(AttributeError): 106 | conn.client_flag = 0 107 | 108 | conn.close() 109 | 110 | def test_fileno(self): 111 | self.assertGreaterEqual(self.conn.fileno(), 0) 112 | 113 | def test_context_manager(self): 114 | with connection_factory() as conn: 115 | self.assertFalse(conn.closed) 116 | self.assertTrue(conn.closed) 117 | 118 | 119 | class TestCollation(unittest.TestCase): 120 | """Test charset and collation connection options.""" 121 | 122 | def setUp(self): 123 | # Initialize a connection with a non-default character set and 124 | # collation. 125 | self.conn = connection_factory( 126 | charset="utf8mb4", 127 | collation="utf8mb4_esperanto_ci", 128 | ) 129 | 130 | def tearDown(self): 131 | self.conn.close() 132 | 133 | def test_charset_collation(self): 134 | c = self.conn.cursor() 135 | c.execute( 136 | """ 137 | SHOW VARIABLES WHERE 138 | Variable_Name="character_set_connection" OR 139 | Variable_Name="collation_connection"; 140 | """ 141 | ) 142 | row = c.fetchall() 143 | charset = row[0][1] 144 | collation = row[1][1] 145 | self.assertEqual(charset, "utf8mb4") 146 | self.assertEqual(collation, "utf8mb4_esperanto_ci") 147 | -------------------------------------------------------------------------------- /tests/test_MySQLdb_times.py: -------------------------------------------------------------------------------- 1 | from datetime import time, date, datetime, timedelta 2 | from time import gmtime 3 | import unittest 4 | from unittest import mock 5 | import warnings 6 | 7 | from MySQLdb import times 8 | 9 | 10 | warnings.simplefilter("ignore") 11 | 12 | 13 | class TestX_or_None(unittest.TestCase): 14 | def test_date_or_none(self): 15 | assert times.Date_or_None("1969-01-01") == date(1969, 1, 1) 16 | assert times.Date_or_None("2015-01-01") == date(2015, 1, 1) 17 | assert times.Date_or_None("2015-12-13") == date(2015, 12, 13) 18 | 19 | assert times.Date_or_None("") is None 20 | assert times.Date_or_None("fail") is None 21 | assert times.Date_or_None("2015-12") is None 22 | assert times.Date_or_None("2015-12-40") is None 23 | assert times.Date_or_None("0000-00-00") is None 24 | 25 | def test_time_or_none(self): 26 | assert times.Time_or_None("00:00:00") == time(0, 0) 27 | assert times.Time_or_None("01:02:03") == time(1, 2, 3) 28 | assert times.Time_or_None("01:02:03.123456") == time(1, 2, 3, 123456) 29 | 30 | assert times.Time_or_None("") is None 31 | assert times.Time_or_None("fail") is None 32 | assert times.Time_or_None("24:00:00") is None 33 | assert times.Time_or_None("01:02:03.123456789") is None 34 | 35 | def test_datetime_or_none(self): 36 | assert times.DateTime_or_None("1000-01-01") == date(1000, 1, 1) 37 | assert times.DateTime_or_None("2015-12-13") == date(2015, 12, 13) 38 | assert times.DateTime_or_None("2015-12-13 01:02") == datetime( 39 | 2015, 12, 13, 1, 2 40 | ) 41 | assert times.DateTime_or_None("2015-12-13T01:02") == datetime( 42 | 2015, 12, 13, 1, 2 43 | ) 44 | assert times.DateTime_or_None("2015-12-13 01:02:03") == datetime( 45 | 2015, 12, 13, 1, 2, 3 46 | ) 47 | assert times.DateTime_or_None("2015-12-13T01:02:03") == datetime( 48 | 2015, 12, 13, 1, 2, 3 49 | ) 50 | assert times.DateTime_or_None("2015-12-13 01:02:03.123") == datetime( 51 | 2015, 12, 13, 1, 2, 3, 123000 52 | ) 53 | assert times.DateTime_or_None("2015-12-13 01:02:03.000123") == datetime( 54 | 2015, 12, 13, 1, 2, 3, 123 55 | ) 56 | assert times.DateTime_or_None("2015-12-13 01:02:03.123456") == datetime( 57 | 2015, 12, 13, 1, 2, 3, 123456 58 | ) 59 | assert times.DateTime_or_None("2015-12-13T01:02:03.123456") == datetime( 60 | 2015, 12, 13, 1, 2, 3, 123456 61 | ) 62 | 63 | assert times.DateTime_or_None("") is None 64 | assert times.DateTime_or_None("fail") is None 65 | assert times.DateTime_or_None("0000-00-00 00:00:00") is None 66 | assert times.DateTime_or_None("0000-00-00 00:00:00.000000") is None 67 | assert times.DateTime_or_None("2015-12-13T01:02:03.123456789") is None 68 | 69 | def test_timedelta_or_none(self): 70 | assert times.TimeDelta_or_None("-1:0:0") == timedelta(0, -3600) 71 | assert times.TimeDelta_or_None("1:0:0") == timedelta(0, 3600) 72 | assert times.TimeDelta_or_None("12:55:30") == timedelta(0, 46530) 73 | assert times.TimeDelta_or_None("12:55:30.123456") == timedelta(0, 46530, 123456) 74 | assert times.TimeDelta_or_None("12:55:30.123456789") == timedelta( 75 | 0, 46653, 456789 76 | ) 77 | assert times.TimeDelta_or_None("12:55:30.123456789123456") == timedelta( 78 | 1429, 37719, 123456 79 | ) 80 | 81 | assert times.TimeDelta_or_None("") is None 82 | assert times.TimeDelta_or_None("0") is None 83 | assert times.TimeDelta_or_None("fail") is None 84 | 85 | 86 | class TestTicks(unittest.TestCase): 87 | @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) 88 | def test_date_from_ticks(self, mock): 89 | assert times.DateFromTicks(0) == date(1970, 1, 1) 90 | assert times.DateFromTicks(1430000000) == date(2015, 4, 25) 91 | 92 | @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) 93 | def test_time_from_ticks(self, mock): 94 | assert times.TimeFromTicks(0) == time(0, 0, 0) 95 | assert times.TimeFromTicks(1431100000) == time(15, 46, 40) 96 | assert times.TimeFromTicks(1431100000.123) == time(15, 46, 40) 97 | 98 | @mock.patch("MySQLdb.times.localtime", side_effect=gmtime) 99 | def test_timestamp_from_ticks(self, mock): 100 | assert times.TimestampFromTicks(0) == datetime(1970, 1, 1, 0, 0, 0) 101 | assert times.TimestampFromTicks(1430000000) == datetime(2015, 4, 25, 22, 13, 20) 102 | assert times.TimestampFromTicks(1430000000.123) == datetime( 103 | 2015, 4, 25, 22, 13, 20 104 | ) 105 | 106 | 107 | class TestToLiteral(unittest.TestCase): 108 | def test_datetime_to_literal(self): 109 | self.assertEqual( 110 | times.DateTime2literal(datetime(2015, 12, 13), ""), b"'2015-12-13 00:00:00'" 111 | ) 112 | self.assertEqual( 113 | times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13), ""), 114 | b"'2015-12-13 11:12:13'", 115 | ) 116 | self.assertEqual( 117 | times.DateTime2literal(datetime(2015, 12, 13, 11, 12, 13, 123456), ""), 118 | b"'2015-12-13 11:12:13.123456'", 119 | ) 120 | 121 | def test_datetimedelta_to_literal(self): 122 | d = datetime(2015, 12, 13, 1, 2, 3) - datetime(2015, 12, 13, 1, 2, 2) 123 | assert times.DateTimeDelta2literal(d, "") == b"'0 0:0:1'" 124 | 125 | 126 | class TestFormat(unittest.TestCase): 127 | def test_format_timedelta(self): 128 | d = datetime(2015, 1, 1) - datetime(2015, 1, 1) 129 | assert times.format_TIMEDELTA(d) == "0 0:0:0" 130 | 131 | d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 8, 9, 10) 132 | assert times.format_TIMEDELTA(d) == "0 2:2:2" 133 | 134 | d = datetime(2015, 1, 1, 10, 11, 12) - datetime(2015, 1, 1, 11, 12, 13) 135 | assert times.format_TIMEDELTA(d) == "-1 22:58:59" 136 | 137 | def test_format_timestamp(self): 138 | assert times.format_TIMESTAMP(datetime(2015, 2, 3)) == "2015-02-03 00:00:00" 139 | self.assertEqual( 140 | times.format_TIMESTAMP(datetime(2015, 2, 3, 17, 18, 19)), 141 | "2015-02-03 17:18:19", 142 | ) 143 | self.assertEqual( 144 | times.format_TIMESTAMP(datetime(15, 2, 3, 17, 18, 19)), 145 | "0015-02-03 17:18:19", 146 | ) 147 | -------------------------------------------------------------------------------- /tests/test__mysql.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from MySQLdb import _mysql 3 | 4 | 5 | def test_result_type(): 6 | with pytest.raises(TypeError): 7 | _mysql.result(b"xyz") 8 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from MySQLdb._exceptions import ProgrammingError 4 | 5 | from configdb import connection_factory 6 | 7 | 8 | def test_multi_statements_default_true(): 9 | conn = connection_factory() 10 | cursor = conn.cursor() 11 | 12 | cursor.execute("select 17; select 2") 13 | rows = cursor.fetchall() 14 | assert rows == ((17,),) 15 | 16 | 17 | def test_multi_statements_false(): 18 | conn = connection_factory(multi_statements=False) 19 | cursor = conn.cursor() 20 | 21 | with pytest.raises(ProgrammingError): 22 | cursor.execute("select 17; select 2") 23 | 24 | cursor.execute("select 17") 25 | rows = cursor.fetchall() 26 | assert rows == ((17,),) 27 | -------------------------------------------------------------------------------- /tests/test_cursor.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import MySQLdb.cursors 3 | from configdb import connection_factory 4 | 5 | 6 | _conns = [] 7 | _tables = [] 8 | 9 | 10 | def connect(**kwargs): 11 | conn = connection_factory(**kwargs) 12 | _conns.append(conn) 13 | return conn 14 | 15 | 16 | def teardown_function(function): 17 | if _tables: 18 | c = _conns[0] 19 | cur = c.cursor() 20 | for t in _tables: 21 | cur.execute(f"DROP TABLE {t}") 22 | cur.close() 23 | del _tables[:] 24 | 25 | for c in _conns: 26 | c.close() 27 | del _conns[:] 28 | 29 | 30 | def test_executemany(): 31 | conn = connect() 32 | cursor = conn.cursor() 33 | 34 | cursor.execute("create table test (data varchar(10))") 35 | _tables.append("test") 36 | 37 | m = MySQLdb.cursors.RE_INSERT_VALUES.match( 38 | "INSERT INTO TEST (ID, NAME) VALUES (%s, %s)" 39 | ) 40 | assert m is not None, "error parse %s" 41 | assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" 42 | 43 | m = MySQLdb.cursors.RE_INSERT_VALUES.match( 44 | "INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)" 45 | ) 46 | assert m is not None, "error parse %(name)s" 47 | assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" 48 | 49 | m = MySQLdb.cursors.RE_INSERT_VALUES.match( 50 | "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)" 51 | ) 52 | assert m is not None, "error parse %(id_name)s" 53 | assert m.group(3) == "", "group 3 not blank, bug in RE_INSERT_VALUES?" 54 | 55 | m = MySQLdb.cursors.RE_INSERT_VALUES.match( 56 | "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update" 57 | ) 58 | assert m is not None, "error parse %(id_name)s" 59 | assert ( 60 | m.group(3) == " ON duplicate update" 61 | ), "group 3 not ON duplicate update, bug in RE_INSERT_VALUES?" 62 | 63 | # https://github.com/PyMySQL/mysqlclient-python/issues/178 64 | m = MySQLdb.cursors.RE_INSERT_VALUES.match( 65 | "INSERT INTO bloup(foo, bar)VALUES(%s, %s)" 66 | ) 67 | assert m is not None 68 | 69 | # cursor._executed myst bee 70 | # """ 71 | # insert into test (data) 72 | # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9) 73 | # """ 74 | # list args 75 | data = [(i,) for i in range(10)] 76 | cursor.executemany("insert into test (data) values (%s)", data) 77 | assert cursor._executed.endswith( 78 | b",(7),(8),(9)" 79 | ), "execute many with %s not in one query" 80 | 81 | # dict args 82 | data_dict = [{"data": i} for i in range(10)] 83 | cursor.executemany("insert into test (data) values (%(data)s)", data_dict) 84 | assert cursor._executed.endswith( 85 | b",(7),(8),(9)" 86 | ), "execute many with %(data)s not in one query" 87 | 88 | # %% in column set 89 | cursor.execute( 90 | """\ 91 | CREATE TABLE percent_test ( 92 | `A%` INTEGER, 93 | `B%` INTEGER)""" 94 | ) 95 | try: 96 | q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)" 97 | assert MySQLdb.cursors.RE_INSERT_VALUES.match(q) is not None 98 | cursor.executemany(q, [(3, 4), (5, 6)]) 99 | assert cursor._executed.endswith( 100 | b"(3, 4),(5, 6)" 101 | ), "executemany with %% not in one query" 102 | finally: 103 | cursor.execute("DROP TABLE IF EXISTS percent_test") 104 | 105 | 106 | def test_pyparam(): 107 | conn = connect() 108 | cursor = conn.cursor() 109 | 110 | cursor.execute("SELECT %(a)s, %(b)s", {"a": 1, "b": 2}) 111 | assert cursor._executed == b"SELECT 1, 2" 112 | cursor.execute(b"SELECT %(a)s, %(b)s", {b"a": 3, b"b": 4}) 113 | assert cursor._executed == b"SELECT 3, 4" 114 | 115 | 116 | def test_dictcursor(): 117 | conn = connect() 118 | cursor = conn.cursor(MySQLdb.cursors.DictCursor) 119 | 120 | cursor.execute("CREATE TABLE t1 (a int, b int, c int)") 121 | _tables.append("t1") 122 | cursor.execute("INSERT INTO t1 (a,b,c) VALUES (1,1,47), (2,2,47)") 123 | 124 | cursor.execute("CREATE TABLE t2 (b int, c int)") 125 | _tables.append("t2") 126 | cursor.execute("INSERT INTO t2 (b,c) VALUES (1,1), (2,2)") 127 | 128 | cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b") 129 | rows = cursor.fetchall() 130 | 131 | assert len(rows) == 2 132 | assert rows[0] == {"a": 1, "b": 1, "c": 47, "t2.b": 1, "t2.c": 1} 133 | assert rows[1] == {"a": 2, "b": 2, "c": 47, "t2.b": 2, "t2.c": 2} 134 | 135 | names1 = sorted(rows[0]) 136 | names2 = sorted(rows[1]) 137 | for a, b in zip(names1, names2): 138 | assert a is b 139 | 140 | # Old fetchtype 141 | cursor._fetch_type = 2 142 | cursor.execute("SELECT * FROM t1 JOIN t2 ON t1.b=t2.b") 143 | rows = cursor.fetchall() 144 | 145 | assert len(rows) == 2 146 | assert rows[0] == {"t1.a": 1, "t1.b": 1, "t1.c": 47, "t2.b": 1, "t2.c": 1} 147 | assert rows[1] == {"t1.a": 2, "t1.b": 2, "t1.c": 47, "t2.b": 2, "t2.c": 2} 148 | 149 | names1 = sorted(rows[0]) 150 | names2 = sorted(rows[1]) 151 | for a, b in zip(names1, names2): 152 | assert a is b 153 | 154 | 155 | def test_mogrify_without_args(): 156 | conn = connect() 157 | cursor = conn.cursor() 158 | 159 | query = "SELECT VERSION()" 160 | mogrified_query = cursor.mogrify(query) 161 | cursor.execute(query) 162 | 163 | assert mogrified_query == query 164 | assert mogrified_query == cursor._executed.decode() 165 | 166 | 167 | def test_mogrify_with_tuple_args(): 168 | conn = connect() 169 | cursor = conn.cursor() 170 | 171 | query_with_args = "SELECT %s, %s", (1, 2) 172 | mogrified_query = cursor.mogrify(*query_with_args) 173 | cursor.execute(*query_with_args) 174 | 175 | assert mogrified_query == "SELECT 1, 2" 176 | assert mogrified_query == cursor._executed.decode() 177 | 178 | 179 | def test_mogrify_with_dict_args(): 180 | conn = connect() 181 | cursor = conn.cursor() 182 | 183 | query_with_args = "SELECT %(a)s, %(b)s", {"a": 1, "b": 2} 184 | mogrified_query = cursor.mogrify(*query_with_args) 185 | cursor.execute(*query_with_args) 186 | 187 | assert mogrified_query == "SELECT 1, 2" 188 | assert mogrified_query == cursor._executed.decode() 189 | 190 | 191 | # Test that cursor can be used without reading whole resultset. 192 | @pytest.mark.parametrize("Cursor", [MySQLdb.cursors.Cursor, MySQLdb.cursors.SSCursor]) 193 | def test_cursor_discard_result(Cursor): 194 | conn = connect() 195 | cursor = conn.cursor(Cursor) 196 | 197 | cursor.execute( 198 | """\ 199 | CREATE TABLE test_cursor_discard_result ( 200 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 201 | data VARCHAR(100) 202 | )""" 203 | ) 204 | _tables.append("test_cursor_discard_result") 205 | 206 | cursor.executemany( 207 | "INSERT INTO test_cursor_discard_result (id, data) VALUES (%s, %s)", 208 | [(i, f"row {i}") for i in range(1, 101)], 209 | ) 210 | 211 | cursor.execute( 212 | """\ 213 | SELECT * FROM test_cursor_discard_result WHERE id <= 10; 214 | SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 11 AND 20; 215 | SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 21 AND 30; 216 | """ 217 | ) 218 | cursor.nextset() 219 | assert cursor.fetchone() == (11, "row 11") 220 | 221 | cursor.execute( 222 | "SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 31 AND 40" 223 | ) 224 | assert cursor.fetchone() == (31, "row 31") 225 | 226 | 227 | def test_binary_prefix(): 228 | # https://github.com/PyMySQL/mysqlclient/issues/494 229 | conn = connect(binary_prefix=True) 230 | cursor = conn.cursor() 231 | 232 | cursor.execute("DROP TABLE IF EXISTS test_binary_prefix") 233 | cursor.execute( 234 | """\ 235 | CREATE TABLE test_binary_prefix ( 236 | id INTEGER NOT NULL AUTO_INCREMENT, 237 | json JSON NOT NULL, 238 | PRIMARY KEY (id) 239 | ) CHARSET=utf8mb4""" 240 | ) 241 | 242 | cursor.executemany( 243 | "INSERT INTO test_binary_prefix (id, json) VALUES (%(id)s, %(json)s)", 244 | ({"id": 1, "json": "{}"}, {"id": 2, "json": "{}"}), 245 | ) 246 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import MySQLdb.cursors 3 | from configdb import connection_factory 4 | 5 | 6 | _conns = [] 7 | _tables = [] 8 | 9 | 10 | def connect(**kwargs): 11 | conn = connection_factory(**kwargs) 12 | _conns.append(conn) 13 | return conn 14 | 15 | 16 | def teardown_function(function): 17 | if _tables: 18 | c = _conns[0] 19 | cur = c.cursor() 20 | for t in _tables: 21 | cur.execute(f"DROP TABLE {t}") 22 | cur.close() 23 | del _tables[:] 24 | 25 | for c in _conns: 26 | c.close() 27 | del _conns[:] 28 | 29 | 30 | def test_null(): 31 | """Inserting NULL into non NULLABLE column""" 32 | # https://github.com/PyMySQL/mysqlclient/issues/535 33 | table_name = "test_null" 34 | conn = connect() 35 | cursor = conn.cursor() 36 | 37 | cursor.execute(f"create table {table_name} (c1 int primary key)") 38 | _tables.append(table_name) 39 | 40 | with pytest.raises(MySQLdb.IntegrityError): 41 | cursor.execute(f"insert into {table_name} values (null)") 42 | 43 | 44 | def test_duplicated_pk(): 45 | """Inserting row with duplicated PK""" 46 | # https://github.com/PyMySQL/mysqlclient/issues/535 47 | table_name = "test_duplicated_pk" 48 | conn = connect() 49 | cursor = conn.cursor() 50 | 51 | cursor.execute(f"create table {table_name} (c1 int primary key)") 52 | _tables.append(table_name) 53 | 54 | cursor.execute(f"insert into {table_name} values (1)") 55 | with pytest.raises(MySQLdb.IntegrityError): 56 | cursor.execute(f"insert into {table_name} values (1)") 57 | -------------------------------------------------------------------------------- /tests/travis.cnf: -------------------------------------------------------------------------------- 1 | # To create your own custom version of this file, read 2 | # http://dev.mysql.com/doc/refman/5.1/en/option-files.html 3 | # and set TESTDB in your environment to the name of the file 4 | 5 | [MySQLdb-tests] 6 | host = 127.0.0.1 7 | port = 3306 8 | user = root 9 | database = mysqldb_test 10 | default-character-set = utf8mb4 11 | --------------------------------------------------------------------------------