├── .github └── workflows │ ├── fast_testing.yml │ ├── publish.yml │ └── reusable_testing.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── FindMySQL.cmake └── FindTarantool.cmake ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── docs ├── rules └── source │ └── format ├── mysql-scm-1.rockspec ├── mysql ├── CMakeLists.txt ├── driver.c └── init.lua ├── rpm └── tarantool-mysql.spec └── test ├── mysql.test.lua └── numeric_result.test.lua /.github/workflows/fast_testing.yml: -------------------------------------------------------------------------------- 1 | name: fast_testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - 'master' 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ubuntu-24.04 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | tarantool: 20 | - '2.11' 21 | 22 | env: 23 | MYSQL_HOST: 127.0.0.1 24 | MYSQL_PORT: 3306 25 | MYSQL_USER: tarantool 26 | MYSQL_DATABASE: tarantool_mysql_test 27 | 28 | steps: 29 | - name: Clone the module 30 | uses: actions/checkout@v4 31 | with: 32 | submodules: recursive 33 | 34 | - name: Setup tarantool ${{ matrix.tarantool }} 35 | uses: tarantool/setup-tarantool@v3 36 | with: 37 | tarantool-version: ${{ matrix.tarantool }} 38 | 39 | - name: Restart mysql service 40 | run: | 41 | sudo systemctl restart mysql 42 | 43 | - name: Prepare test environment 44 | run: | 45 | sudo mysql -proot -e "CREATE USER ${MYSQL_USER}@${MYSQL_HOST};" 46 | sudo mysql -proot -e "GRANT ALL PRIVILEGES ON *.* \ 47 | TO ${MYSQL_USER}@${MYSQL_HOST};" 48 | sudo mysql -proot -e "ALTER USER ${MYSQL_USER}@${MYSQL_HOST} \ 49 | IDENTIFIED WITH mysql_native_password BY '';" 50 | sudo mysql -proot -e "CREATE DATABASE ${MYSQL_DATABASE};" 51 | 52 | - run: cmake . && make 53 | - run: make check 54 | env: 55 | MYSQL: '${{ env.MYSQL_HOST }}:${{ env.MYSQL_PORT }}:${{ 56 | env.MYSQL_USER }}::${{ env.MYSQL_DATABASE }}' 57 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '*' 9 | 10 | env: 11 | ROCK_NAME: 'mysql' 12 | 13 | jobs: 14 | publish-rockspec-scm-1: 15 | runs-on: ubuntu-24.04 16 | if: github.ref == 'refs/heads/master' 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: tarantool/rocks.tarantool.org/github-action@master 21 | with: 22 | auth: ${{ secrets.ROCKS_AUTH }} 23 | files: ${{ env.ROCK_NAME }}-scm-1.rockspec 24 | 25 | publish-rockspec-tag: 26 | runs-on: ubuntu-24.04 27 | if: startsWith(github.ref, 'refs/tags') 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions 32 | - name: Set env 33 | run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 34 | 35 | - name: Create release rockspec 36 | run: | 37 | sed \ 38 | -e "s/branch = '.\+'/tag = '${GIT_TAG}'/g" \ 39 | -e "s/version = '.\+'/version = '${GIT_TAG}-1'/g" \ 40 | ${{ env.ROCK_NAME }}-scm-1.rockspec > ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 41 | - uses: tarantool/rocks.tarantool.org/github-action@master 42 | with: 43 | auth: ${{ secrets.ROCKS_AUTH }} 44 | files: ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 45 | -------------------------------------------------------------------------------- /.github/workflows/reusable_testing.yml: -------------------------------------------------------------------------------- 1 | name: reusable_testing 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | artifact_name: 7 | description: The name of the tarantool build artifact 8 | default: ubuntu-focal 9 | required: false 10 | type: string 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ubuntu-24.04 15 | 16 | env: 17 | MYSQL_HOST: 127.0.0.1 18 | MYSQL_PORT: 3306 19 | MYSQL_USER: tarantool 20 | MYSQL_DATABASE: tarantool_mysql_test 21 | 22 | steps: 23 | - name: Clone the mysql module 24 | uses: actions/checkout@v4 25 | with: 26 | repository: ${{ github.repository_owner }}/mysql 27 | submodules: recursive 28 | 29 | - name: Download the tarantool build artifact 30 | uses: actions/download-artifact@v4 31 | with: 32 | name: ${{ inputs.artifact_name }} 33 | 34 | - name: Install tarantool 35 | # Now we're lucky: all dependencies are already installed. Check package 36 | # dependencies when migrating to other OS version. 37 | run: sudo dpkg -i tarantool*.deb 38 | 39 | - name: Restart mysql service 40 | run: | 41 | sudo systemctl restart mysql 42 | 43 | - name: Prepare test environment 44 | run: | 45 | sudo mysql -proot -e "CREATE USER ${MYSQL_USER}@${MYSQL_HOST};" 46 | sudo mysql -proot -e "GRANT ALL PRIVILEGES ON *.* \ 47 | TO ${MYSQL_USER}@${MYSQL_HOST};" 48 | sudo mysql -proot -e "ALTER USER ${MYSQL_USER}@${MYSQL_HOST} \ 49 | IDENTIFIED WITH mysql_native_password BY '';" 50 | sudo mysql -proot -e "CREATE DATABASE ${MYSQL_DATABASE};" 51 | 52 | - run: cmake . && make 53 | - run: make check 54 | env: 55 | MYSQL: '${{ env.MYSQL_HOST }}:${{ env.MYSQL_PORT }}:${{ 56 | env.MYSQL_USER }}::${{ env.MYSQL_DATABASE }}' 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeFiles/ 2 | CMakeCache.txt 3 | Makefile 4 | cmake_*.cmake 5 | install_manifest.txt 6 | *.a 7 | *.cbp 8 | *.d 9 | *.dylib 10 | *.gcno 11 | *.gcda 12 | *.user 13 | *.o 14 | *.reject 15 | *.so 16 | *.snap* 17 | *.xlog* 18 | *~ 19 | .gdb_history 20 | mariadb-connector-c-prefix/ 21 | mariadb-connector-c/include/mariadb_version.h 22 | mariadb-connector-c/unittest/libmariadb/fingerprint.list 23 | mariadb-connector-c/unittest/libmariadb/ssl.c 24 | lib/ 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mariadb-connector-c"] 2 | path = mariadb-connector-c 3 | url = https://github.com/tarantool/mariadb-connector-c 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10 FATAL_ERROR) 2 | 3 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) 4 | include(ExternalProject) 5 | 6 | find_program(GIT git) 7 | 8 | project(mysql C) 9 | if(NOT CMAKE_BUILD_TYPE) 10 | set(CMAKE_BUILD_TYPE Debug) 11 | endif() 12 | 13 | # Find Tarantool 14 | set(TARANTOOL_FIND_REQUIRED ON) 15 | find_package(Tarantool) 16 | 17 | # Get git version only if source directory has .git repository, this 18 | # avoids git to search .git repository in parent 19 | # directories. 20 | # 21 | if (EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT) 22 | execute_process (COMMAND ${GIT} describe --long HEAD 23 | OUTPUT_VARIABLE TARANTOOL_MYSQL_GIT_VERSION 24 | OUTPUT_STRIP_TRAILING_WHITESPACE 25 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 26 | 27 | if (NOT ("${TARANTOOL_MYSQL_GIT_VERSION}" STREQUAL "${TARANTOOL_MYSQL_VERSION}")) 28 | set(TARANTOOL_MYSQL_VERSION "${TARANTOOL_MYSQL_GIT_VERSION}") 29 | message(STATUS "Generating VERSION file") 30 | file(WRITE ${VERSION_FILE} "${TARANTOOL_MYSQL_VERSION}\n") 31 | 32 | message(STATUS "Updating submodules") 33 | execute_process(COMMAND ${GIT} submodule update --init --recursive) 34 | endif() 35 | endif() 36 | 37 | add_subdirectory(mariadb-connector-c EXCLUDE_FROM_ALL) 38 | 39 | include_directories("${CMAKE_SOURCE_DIR}/mariadb-connector-c/include" "${CMAKE_BINARY_DIR}/mariadb-connector-c/include") 40 | include_directories(${TARANTOOL_INCLUDE_DIRS}) 41 | 42 | # Set CFLAGS 43 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 44 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra") 45 | 46 | if (APPLE) 47 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -undefined suppress -flat_namespace") 48 | endif(APPLE) 49 | 50 | # Build module 51 | add_subdirectory(mysql) 52 | 53 | add_custom_target(check 54 | COMMAND ${PROJECT_SOURCE_DIR}/test/mysql.test.lua 55 | COMMAND ${PROJECT_SOURCE_DIR}/test/numeric_result.test.lua) 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010-2013 Tarantool AUTHORS: 2 | please see AUTHORS file in tarantool/tarantool repository. 3 | 4 | /* 5 | * Redistribution and use in source and binary forms, with or 6 | * without modification, are permitted provided that the following 7 | * conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above 10 | * copyright notice, this list of conditions and the 11 | * following disclaimer. 12 | * 13 | * 2. Redistributions in binary form must reproduce the above 14 | * copyright notice, this list of conditions and the following 15 | * disclaimer in the documentation and/or other materials 16 | * provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY AUTHORS ``AS IS'' AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 22 | * AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 23 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 26 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 29 | * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 30 | * SUCH DAMAGE. 31 | */ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql - MySQL connector for [Tarantool][] 2 | 3 | [![fast-testing status][testing-actions-badge]][testing-actions-url] 4 | [![publish status][publish-actions-badge]][publish-actions-url] 5 | 6 | ## Getting Started 7 | 8 | ### Prerequisites 9 | 10 | * Tarantool 1.6.5+ with header files (tarantool && tarantool-dev / 11 | tarantool-devel packages). 12 | * MySQL 5.1 header files (libmysqlclient-dev package). 13 | * OpenSSL development package. 14 | * libucontext (only for Alpine). 15 | 16 | If you prefer to install the connector using a system package manager you don't 17 | need to manually install dependencies. 18 | 19 | ### Installation 20 | 21 | #### Build from sources 22 | 23 | Clone repository and then build it using CMake: 24 | 25 | ```sh 26 | git clone https://github.com/tarantool/mysql.git tarantool-mysql 27 | cd tarantool-mysql && cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo 28 | make 29 | make install 30 | ``` 31 | 32 | #### Run tests 33 | 34 | To run the tests, the following preparatory steps must be completed: 35 | * Run mysql instance. 36 | * Connect a client to the instance and execute the commands: 37 | * `CREATE USER 'test_user' IDENTIFIED WITH mysql_native_password BY 'pass';` 38 | * `CREATE DATABASE tarantool_mysql_test;` 39 | * `GRANT ALL PRIVILEGES ON *.* TO 'test_user';` 40 | * Define MYSQL environment variable using the following format:
41 | `ip:port:user:user_pass:db_name:`
42 | Example:
43 | `export MYSQL=127.0.0.1:3306:test_user:pass:tarantool_mysql_test:` 44 | 45 | The tests can now be run by `make check`. 46 | 47 | #### tt rocks 48 | 49 | You can also use `tt rocks`: 50 | 51 | ```sh 52 | tt rocks install mysql 53 | ``` 54 | 55 | #### Install a package 56 | 57 | [Enable tarantool repository][tarantool_download] and install tarantool-mysql 58 | package: 59 | 60 | ```sh 61 | apt-get install tarantool-mysql # Debian or Ubuntu 62 | yum install tarantool-mysql # CentOS 63 | dnf install tarantool-mysql # Fedora 64 | ``` 65 | 66 | [tarantool_download]: https://www.tarantool.io/en/download/ 67 | 68 | ### Usage 69 | 70 | ``` lua 71 | local mysql = require('mysql') 72 | local pool = mysql.pool_create({ host = '127.0.0.1', user = 'user', password = 'password', db = 'db', size = 5 }) 73 | local conn = pool:get() 74 | local tuples, status = conn:execute("SELECT ? AS a, 'xx' AS b, NULL as c", 42)) 75 | conn:begin() 76 | conn:execute("INSERT INTO test VALUES(1, 2, 3)") 77 | conn:commit() 78 | pool:put(conn) 79 | ``` 80 | 81 | ## API Documentation 82 | 83 | ### `conn = mysql.connect(opts)` 84 | 85 | Connect to a database. 86 | 87 | *Options*: 88 | 89 | - `host` - hostname to connect to 90 | - `port` - port number to connect to 91 | - `user` - username 92 | - `password` - password 93 | - `db` - database name 94 | - `use_numeric_result` - provide result of the "conn:execute" as ordered list 95 | (true/false); default value: false 96 | - `keep_null` - provide printing null fields in the result of the 97 | "conn:execute" (true/false); default value: false 98 | 99 | Throws an error on failure. 100 | 101 | *Returns*: 102 | 103 | - `connection ~= nil` on success 104 | 105 | ### `conn:execute(statement, ...)` 106 | 107 | Execute a statement with arguments in the current transaction. 108 | 109 | Throws an error on failure. 110 | 111 | *Returns*: 112 | 113 | - `results, true` on success, where `results` is in the following form: 114 | 115 | (when `use_numeric_result = false` or is not set on a pool/connection creation) 116 | 117 | ```lua 118 | { 119 | { -- result set 120 | {column1 = r1c1val, column2 = r1c2val, ...}, -- row 121 | {column1 = r2c1val, column2 = r2c2val, ...}, -- row 122 | ... 123 | }, 124 | ... 125 | } 126 | ``` 127 | 128 | (when `use_numeric_result = true` on a pool/connection creation) 129 | 130 | ```lua 131 | { 132 | { -- result set 133 | rows = { 134 | {r1c1val, r1c2val, ...}, -- row 135 | {r2c1val, r2c2val, ...}, -- row 136 | ... 137 | }, 138 | metadata = { 139 | {type = 'long', name = 'col1'}, -- column meta 140 | {type = 'long', name = 'col2'}, -- column meta 141 | ... 142 | }, 143 | }, 144 | ... 145 | } 146 | ``` 147 | 148 | *Example*: 149 | 150 | (when `keep_null = false` or is not set on a pool/connection creation) 151 | 152 | ``` 153 | tarantool> conn:execute("SELECT ? AS a, 'xx' AS b", NULL AS c , 42) 154 | --- 155 | - - - a: 42 156 | b: xx 157 | - true 158 | ... 159 | ``` 160 | 161 | (when `keep_null = true` on a pool/connection creation) 162 | 163 | ``` 164 | tarantool> conn:execute("SELECT ? AS a, 'xx' AS b", NULL AS c, 42) 165 | --- 166 | - - - a: 42 167 | b: xx 168 | c: null 169 | - true 170 | ... 171 | ``` 172 | 173 | ### `conn:begin()` 174 | 175 | Begin a transaction. 176 | 177 | *Returns*: `true` 178 | 179 | ### `conn:commit()` 180 | 181 | Commit current transaction. 182 | 183 | *Returns*: `true` 184 | 185 | ### `conn:rollback()` 186 | 187 | Rollback current transaction. 188 | 189 | *Returns*: `true` 190 | 191 | ### `conn:ping()` 192 | 193 | Execute a dummy statement to check that connection is alive. 194 | 195 | *Returns*: 196 | 197 | - `true` on success 198 | - `false` on failure 199 | 200 | ### `conn:quote()` 201 | 202 | Quote a query string. 203 | 204 | Throws an error on failure. 205 | 206 | *Returns*: 207 | 208 | - `quoted_string` on success 209 | 210 | ### `conn:reset(user, pass, db)` 211 | 212 | Update the connection authentication settings. 213 | 214 | *Options*: 215 | 216 | - `user` - username 217 | - `pass` - password 218 | - `db` - database name 219 | 220 | Throws an error on failure. 221 | 222 | ### `conn:close()` 223 | 224 | Close the individual connection or return it to a pool. 225 | 226 | Throws an error on failure. 227 | 228 | *Returns*: `true` 229 | 230 | ### `pool = mysql.pool_create(opts)` 231 | 232 | Create a connection pool with count of size established connections. 233 | 234 | *Options*: 235 | 236 | - `host` - hostname to connect to 237 | - `port` - port number to connect to 238 | - `user` - username 239 | - `password` - password 240 | - `db` - database name 241 | - `size` - count of connections in pool 242 | - `use_numeric_result` - provide result of the "conn:execute" as ordered list 243 | (true/false); default value: false 244 | - `keep_null` - provide printing null fields in the result of the 245 | "conn:execute" (true/false); default value: false 246 | 247 | Throws an error on failure. 248 | 249 | *Returns* 250 | 251 | - `pool ~= nil` on success 252 | 253 | ### `conn = pool:get(opts)` 254 | 255 | Get a connection from pool. Reset connection before returning it. If connection 256 | is broken then it will be reestablished. 257 | If there is no free connections and timeout is not specified then calling fiber 258 | will sleep until another fiber returns some connection to pool. 259 | If timeout is specified, and there is no free connections for the duration of the timeout, 260 | then the return value is nil. 261 | 262 | *Options*: 263 | 264 | - `timeout` - maximum number of seconds to wait for a connection 265 | 266 | *Returns*: 267 | 268 | - `conn ~= nil` on success 269 | - `conn == nil` on there is no free connections when timeout option is specified 270 | 271 | ### `pool:put(conn)` 272 | 273 | Return a connection to connection pool. 274 | 275 | *Options* 276 | 277 | - `conn` - a connection 278 | 279 | ### `pool:close()` 280 | 281 | Close all connections in pool. 282 | 283 | *Returns*: `true` 284 | 285 | ## Comments 286 | 287 | All calls to connections api will be serialized, so it should to be safe to 288 | use one connection from some count of fibers. But you should understand, 289 | that you can have some unwanted behavior across db calls, for example if 290 | another fiber 'injects' some sql between two your calls. 291 | 292 | ## See Also 293 | 294 | * [Tests][] 295 | * [Tarantool][] 296 | * [Tarantool Rocks][TarantoolRocks] 297 | 298 | [Tarantool]: http://github.com/tarantool/tarantool 299 | [Tests]: https://github.com/tarantool/mysql/tree/master/test 300 | [TarantoolRocks]: https://github.com/tarantool/rocks 301 | 302 | [testing-actions-badge]: https://github.com/tarantool/mysql/actions/workflows/fast_testing.yml/badge.svg 303 | [testing-actions-url]: https://github.com/tarantool/mysql/actions/workflows/fast_testing.yml 304 | [publish-actions-badge]: https://github.com/tarantool/mysql/actions/workflows/publish.yml/badge.svg 305 | [publish-actions-url]: https://github.com/tarantool/mysql/actions/workflows/publish.yml 306 | -------------------------------------------------------------------------------- /cmake/FindMySQL.cmake: -------------------------------------------------------------------------------- 1 | find_path(MYSQL_INCLUDE_DIR 2 | NAMES mysql.h 3 | PATH_SUFFIXES mysql 4 | ) 5 | find_library(MYSQL_LIBRARIES 6 | NAMES mysqlclient 7 | PATH_SUFFIXES mysql 8 | ) 9 | 10 | if(MYSQL_INCLUDE_DIR AND MYSQL_LIBRARIES) 11 | set(MYSQL_FOUND ON) 12 | endif(MYSQL_INCLUDE_DIR AND MYSQL_LIBRARIES) 13 | 14 | if(MYSQL_FOUND) 15 | if (NOT MYSQL_FIND_QUIETLY) 16 | message(STATUS "Found MySQL includes: ${MYSQL_INCLUDE_DIR}/mysql.h") 17 | message(STATUS "Found MySQL library: ${MYSQL_LIBRARIES}") 18 | endif (NOT MYSQL_FIND_QUIETLY) 19 | set(MYSQL_INCLUDE_DIRS ${MYSQL_INCLUDE_DIR}) 20 | else(MYSQL_FOUND) 21 | if (MYSQL_FIND_REQUIRED) 22 | message(FATAL_ERROR "Could not find mysql development files") 23 | endif (MYSQL_FIND_REQUIRED) 24 | endif (MYSQL_FOUND) 25 | -------------------------------------------------------------------------------- /cmake/FindTarantool.cmake: -------------------------------------------------------------------------------- 1 | # Define GNU standard installation directories 2 | include(GNUInstallDirs) 3 | 4 | macro(extract_definition name output input) 5 | string(REGEX MATCH "#define[\t ]+${name}[\t ]+\"([^\"]*)\"" 6 | _t "${input}") 7 | string(REGEX REPLACE "#define[\t ]+${name}[\t ]+\"(.*)\"" "\\1" 8 | ${output} "${_t}") 9 | endmacro() 10 | 11 | find_path(TARANTOOL_INCLUDE_DIR tarantool/module.h 12 | HINTS ENV TARANTOOL_DIR /usr/local/include 13 | ) 14 | 15 | if(TARANTOOL_INCLUDE_DIR) 16 | set(_config "-") 17 | file(READ "${TARANTOOL_INCLUDE_DIR}/tarantool/module.h" _config0) 18 | string(REPLACE "\\" "\\\\" _config ${_config0}) 19 | unset(_config0) 20 | extract_definition(PACKAGE_VERSION TARANTOOL_VERSION ${_config}) 21 | extract_definition(INSTALL_PREFIX _install_prefix ${_config}) 22 | unset(_config) 23 | endif() 24 | 25 | include(FindPackageHandleStandardArgs) 26 | find_package_handle_standard_args(TARANTOOL 27 | REQUIRED_VARS TARANTOOL_INCLUDE_DIR VERSION_VAR TARANTOOL_VERSION) 28 | if(TARANTOOL_FOUND) 29 | set(TARANTOOL_INCLUDE_DIRS "${TARANTOOL_INCLUDE_DIR}" 30 | "${TARANTOOL_INCLUDE_DIR}/tarantool/" 31 | CACHE PATH "Include directories for Tarantool") 32 | set(TARANTOOL_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/tarantool" 33 | CACHE PATH "Directory for storing Lua modules written in Lua") 34 | set(TARANTOOL_INSTALL_LUADIR "${CMAKE_INSTALL_DATADIR}/tarantool" 35 | CACHE PATH "Directory for storing Lua modules written in C") 36 | 37 | if (NOT TARANTOOL_FIND_QUIETLY AND NOT FIND_TARANTOOL_DETAILS) 38 | set(FIND_TARANTOOL_DETAILS ON CACHE INTERNAL "Details about TARANTOOL") 39 | message(STATUS "Tarantool LUADIR is ${TARANTOOL_INSTALL_LUADIR}") 40 | message(STATUS "Tarantool LIBDIR is ${TARANTOOL_INSTALL_LIBDIR}") 41 | endif () 42 | endif() 43 | mark_as_advanced(TARANTOOL_INCLUDE_DIRS TARANTOOL_INSTALL_LIBDIR 44 | TARANTOOL_INSTALL_LUADIR) 45 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | tarantool-mysql/ 2 | files 3 | stamp-* 4 | *.substvars 5 | *.log 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | tarantool-mysql (1.1.0) unstable; urgency=medium 2 | 3 | * Add connection pool support 4 | * Use coio and nonblocking mysql api instead of thread pool 5 | 6 | -- Georgy Kirichenko Tue, 26 Apr 2016 22:51:42 +0300 7 | 8 | tarantool-mysql (1.0.3-1) unstable; urgency=medium 9 | 10 | * Initial release 11 | 12 | -- Roman Tsisyk Wed, 16 Sep 2015 17:33:00 +0300 13 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: tarantool-mysql 2 | Priority: optional 3 | Section: database 4 | Maintainer: Roman Tsisyk 5 | Build-Depends: debhelper (>= 9), cdbs, 6 | cmake (>= 2.8), 7 | libssl-dev, 8 | tarantool-dev (>= 1.6.8.0) 9 | Standards-Version: 3.9.6 10 | Homepage: https://github.com/tarantool/mysql 11 | Vcs-Git: git://github.com/tarantool/mysql.git 12 | Vcs-Browser: https://github.com/tarantool/mysql 13 | 14 | Package: tarantool-mysql 15 | Architecture: i386 amd64 armhf arm64 16 | Depends: tarantool (>= 1.6.8.0), ${shlibs:Depends}, ${misc:Depends} 17 | Pre-Depends: ${misc:Pre-Depends} 18 | Description: MySQL connector for Tarantool 19 | A MySQL connector for Tarantool. 20 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: tarantool-mysql 3 | Upstream-Contact: roman@tarantool.org 4 | Source: https://github.com/tarantool/mysql 5 | 6 | Files: * 7 | Copyright: 2010-2013 Tarantool AUTHORS 8 | License: BSD-2-Clause 9 | Redistribution and use in source and binary forms, with or 10 | without modification, are permitted provided that the following 11 | conditions are met: 12 | . 13 | 1. Redistributions of source code must retain the above 14 | copyright notice, this list of conditions and the 15 | following disclaimer. 16 | . 17 | 2. Redistributions in binary form must reproduce the above 18 | copyright notice, this list of conditions and the following 19 | disclaimer in the documentation and/or other materials 20 | provided with the distribution. 21 | . 22 | THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 24 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 29 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 30 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 33 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 34 | SUCH DAMAGE. 35 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DEB_CMAKE_EXTRA_FLAGS := -DCMAKE_INSTALL_LIBDIR=lib/$(DEB_HOST_MULTIARCH) \ 4 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 5 | DEB_MAKE_CHECK_TARGET := 6 | 7 | include /usr/share/cdbs/1/rules/debhelper.mk 8 | include /usr/share/cdbs/1/class/cmake.mk 9 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /mysql-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'mysql' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git+https://github.com/tarantool/mysql.git', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = "MySQL connector for Tarantool", 9 | homepage = 'https://github.com/tarantool/mysql', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1' 14 | } 15 | build = { 16 | type = 'cmake'; 17 | variables = { 18 | CMAKE_BUILD_TYPE="RelWithDebInfo"; 19 | TARANTOOL_INSTALL_LIBDIR="$(LIBDIR)"; 20 | TARANTOOL_INSTALL_LUADIR="$(LUADIR)"; 21 | }; 22 | } 23 | -- vim: syntax=lua 24 | -------------------------------------------------------------------------------- /mysql/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(driver SHARED driver.c) 2 | add_dependencies(driver mariadbclient) 3 | target_link_libraries(driver mariadbclient) 4 | 5 | # Check 'makecontext', 'getcontext', 'setcontext' and 'swapcontext' symbols. 6 | 7 | include(CheckLibraryExists) 8 | check_library_exists(c makecontext "" HAVE_UCONTEXT_LIBC) 9 | 10 | if (NOT HAVE_UCONTEXT_LIBC) 11 | # Search for libucontext. 12 | find_package(PkgConfig) 13 | if (PKG_CONFIG_FOUND) 14 | pkg_check_modules(libucontext IMPORTED_TARGET libucontext) 15 | if (libucontext_FOUND) 16 | target_link_libraries(driver PkgConfig::libucontext) 17 | else() 18 | message(FATAL_ERROR "Missing 'makecontext', 'getcontext', 'setcontext' or 'swapcontext' symbol in libc and no libucontext found.") 19 | endif() 20 | else() 21 | message(FATAL_ERROR "PkgConfig is required to link libucontext.") 22 | endif() 23 | endif() 24 | 25 | set_target_properties(driver PROPERTIES PREFIX "" OUTPUT_NAME "driver") 26 | install(TARGETS driver LIBRARY DESTINATION ${TARANTOOL_INSTALL_LIBDIR}/mysql) 27 | install(FILES init.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/mysql) 28 | -------------------------------------------------------------------------------- /mysql/driver.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Redistribution and use in source and binary forms, with or 3 | * without modification, are permitted provided that the following 4 | * conditions are met: 5 | * 6 | * 1. Redistributions of source code must retain the above 7 | * copyright notice, this list of conditions and the 8 | * following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above 11 | * copyright notice, this list of conditions and the following 12 | * disclaimer in the documentation and/or other materials 13 | * provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND 16 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 19 | * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 23 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 26 | * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | * SUCH DAMAGE. 28 | */ 29 | #include 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | #include 37 | #include 38 | 39 | #include 40 | #include 41 | 42 | #define TIMEOUT_INFINITY 365 * 86400 * 100.0 43 | static const char mysql_driver_label[] = "__tnt_mysql_driver"; 44 | 45 | static int luaL_nil_ref = LUA_REFNIL; 46 | 47 | /** 48 | * Push ffi's NULL (cdata: NULL) onto the stack. 49 | * Can be used as replacement of nil in Lua tables. 50 | * @param L stack 51 | */ 52 | void 53 | luaL_pushnull(struct lua_State *L) 54 | { 55 | lua_rawgeti(L, LUA_REGISTRYINDEX, luaL_nil_ref); 56 | } 57 | 58 | struct mysql_connection { 59 | MYSQL *raw_conn; 60 | int use_numeric_result; 61 | int keep_null; 62 | }; 63 | 64 | /* 65 | * A dumb hash for MySQL field type values. 66 | * 67 | * Assing zero-based sequential values for field types that are 68 | * used by a client: 69 | * 70 | * - MYSQL_TYPE_DECIMAL .. MYSQL_TYPE_BIT 71 | * - MYSQL_TYPE_JSON .. MYSQL_TYPE_GEOMETRY 72 | * 73 | * Assign FIELD_TYPE_HASH_MAX for an unknown value. 74 | * 75 | * See enum enum_field_types in 76 | * mariadb-connector-c/include/mariadb_com.h. 77 | */ 78 | #define FIELD_TYPE_HASH_MAX (MYSQL_TYPE_GEOMETRY + 1) 79 | #define FIELD_TYPE_HASH(type) ( \ 80 | ((type) >= MYSQL_TYPE_DECIMAL && (type) <= MYSQL_TYPE_BIT) ? \ 81 | ((type) - MYSQL_TYPE_DECIMAL) : \ 82 | ((type) >= MYSQL_TYPE_JSON && (type) <= MYSQL_TYPE_GEOMETRY) ? \ 83 | ((type) - MYSQL_TYPE_JSON + MYSQL_TYPE_BIT - \ 84 | MYSQL_TYPE_DECIMAL + 1) : \ 85 | FIELD_TYPE_HASH_MAX \ 86 | ) 87 | 88 | static const char *mysql_field_type_strs[FIELD_TYPE_HASH_MAX] = { 89 | [FIELD_TYPE_HASH(MYSQL_TYPE_DECIMAL)] = "decimal", 90 | [FIELD_TYPE_HASH(MYSQL_TYPE_TINY)] = "tiny", 91 | [FIELD_TYPE_HASH(MYSQL_TYPE_SHORT)] = "short", 92 | [FIELD_TYPE_HASH(MYSQL_TYPE_LONG)] = "long", 93 | [FIELD_TYPE_HASH(MYSQL_TYPE_FLOAT)] = "float", 94 | [FIELD_TYPE_HASH(MYSQL_TYPE_DOUBLE)] = "double", 95 | [FIELD_TYPE_HASH(MYSQL_TYPE_NULL)] = "null", 96 | [FIELD_TYPE_HASH(MYSQL_TYPE_TIMESTAMP)] = "timestamp", 97 | [FIELD_TYPE_HASH(MYSQL_TYPE_LONGLONG)] = "longlong", 98 | [FIELD_TYPE_HASH(MYSQL_TYPE_INT24)] = "int24", 99 | [FIELD_TYPE_HASH(MYSQL_TYPE_DATE)] = "date", 100 | [FIELD_TYPE_HASH(MYSQL_TYPE_TIME)] = "time", 101 | [FIELD_TYPE_HASH(MYSQL_TYPE_DATETIME)] = "datetime", 102 | [FIELD_TYPE_HASH(MYSQL_TYPE_YEAR)] = "year", 103 | [FIELD_TYPE_HASH(MYSQL_TYPE_NEWDATE)] = "newdate", 104 | [FIELD_TYPE_HASH(MYSQL_TYPE_VARCHAR)] = "varchar", 105 | [FIELD_TYPE_HASH(MYSQL_TYPE_BIT)] = "bit", 106 | [FIELD_TYPE_HASH(MYSQL_TYPE_JSON)] = "json", 107 | [FIELD_TYPE_HASH(MYSQL_TYPE_NEWDECIMAL)] = "newdecimal", 108 | [FIELD_TYPE_HASH(MYSQL_TYPE_ENUM)] = "enum", 109 | [FIELD_TYPE_HASH(MYSQL_TYPE_SET)] = "set", 110 | [FIELD_TYPE_HASH(MYSQL_TYPE_TINY_BLOB)] = "tiny_blob", 111 | [FIELD_TYPE_HASH(MYSQL_TYPE_MEDIUM_BLOB)] = "medium_blob", 112 | [FIELD_TYPE_HASH(MYSQL_TYPE_LONG_BLOB)] = "long_blob", 113 | [FIELD_TYPE_HASH(MYSQL_TYPE_BLOB)] = "blob", 114 | [FIELD_TYPE_HASH(MYSQL_TYPE_VAR_STRING)] = "var_string", 115 | [FIELD_TYPE_HASH(MYSQL_TYPE_STRING)] = "string", 116 | [FIELD_TYPE_HASH(MYSQL_TYPE_GEOMETRY)] = "geometry", 117 | }; 118 | 119 | static int 120 | save_pushstring_wrapped(struct lua_State *L) 121 | { 122 | char *str = (char *)lua_topointer(L, 1); 123 | lua_pushstring(L, str); 124 | return 1; 125 | } 126 | 127 | static int 128 | safe_pushstring(struct lua_State *L, char *str) 129 | { 130 | lua_pushcfunction(L, save_pushstring_wrapped); 131 | lua_pushlightuserdata(L, str); 132 | return lua_pcall(L, 1, 1, 0); 133 | } 134 | 135 | static inline struct mysql_connection * 136 | lua_check_mysqlconn(struct lua_State *L, int index) 137 | { 138 | struct mysql_connection **conn_p = (struct mysql_connection **) 139 | luaL_checkudata(L, index, mysql_driver_label); 140 | if (conn_p == NULL || *conn_p == NULL || (*conn_p)->raw_conn == NULL) 141 | luaL_error(L, "Driver fatal error (closed connection " 142 | "or not a connection)"); 143 | return *conn_p; 144 | } 145 | 146 | /* 147 | * Push native lua error with code -3 148 | */ 149 | static int 150 | lua_push_error(struct lua_State *L) 151 | { 152 | lua_pushnumber(L, -3); 153 | lua_insert(L, -2); 154 | return 2; 155 | } 156 | 157 | /* Push connection status and error message to lua stack. 158 | * Status is -1 if connection is dead or 0 if it is still alive 159 | */ 160 | static int 161 | lua_mysql_push_error(struct lua_State *L, MYSQL *raw_conn) 162 | { 163 | int err = mysql_errno(raw_conn); 164 | switch (err) { 165 | case CR_SERVER_LOST: 166 | case CR_SERVER_GONE_ERROR: 167 | lua_pushnumber(L, -1); 168 | break; 169 | default: 170 | lua_pushnumber(L, 1); 171 | } 172 | safe_pushstring(L, (char *) mysql_error(raw_conn)); 173 | return 2; 174 | } 175 | 176 | /* Returns string representation of MySQL field type */ 177 | static const char* 178 | lua_mysql_field_type_to_string(enum enum_field_types type) 179 | { 180 | int hash = FIELD_TYPE_HASH(type); 181 | if (hash == FIELD_TYPE_HASH_MAX) 182 | hash = FIELD_TYPE_HASH(MYSQL_TYPE_STRING); 183 | return mysql_field_type_strs[hash]; 184 | } 185 | 186 | /** 187 | * Push value retrieved from mysql field to lua stack. 188 | * 189 | * When `data` is NULL, `field` and len` parameters are 190 | * ignored and Lua nil or LuaJIT FFI NULL is pushed. 191 | */ 192 | static void 193 | lua_mysql_push_value(struct lua_State *L, MYSQL_FIELD *field, void *data, 194 | unsigned long len, int keep_null) 195 | { 196 | /* 197 | * Field type isn't MYSQL_TYPE_NULL actually in case of 198 | * Lua's nil passed as value. 199 | * Example: 'conn:execute('SELECT ? AS x', nil)'. 200 | */ 201 | if (data == NULL) 202 | field->type = MYSQL_TYPE_NULL; 203 | switch (field->type) { 204 | case MYSQL_TYPE_TINY: 205 | case MYSQL_TYPE_SHORT: 206 | case MYSQL_TYPE_LONG: 207 | case MYSQL_TYPE_FLOAT: 208 | case MYSQL_TYPE_INT24: 209 | case MYSQL_TYPE_DOUBLE: { 210 | char *data_end = data + len;; 211 | double v = strtod(data, &data_end); 212 | lua_pushnumber(L, v); 213 | break; 214 | } 215 | 216 | case MYSQL_TYPE_NULL: 217 | if (keep_null == 1) 218 | luaL_pushnull(L); 219 | else 220 | lua_pushnil(L); 221 | break; 222 | 223 | case MYSQL_TYPE_LONGLONG: { 224 | long long v = atoll(data); 225 | if (field->flags & UNSIGNED_FLAG) { 226 | luaL_pushuint64(L, v); 227 | } else { 228 | luaL_pushint64(L, v); 229 | } 230 | break; 231 | } 232 | 233 | /* AS string */ 234 | case MYSQL_TYPE_NEWDECIMAL: 235 | case MYSQL_TYPE_DECIMAL: 236 | case MYSQL_TYPE_TIMESTAMP: 237 | default: 238 | lua_pushlstring(L, data, len); 239 | break; 240 | } 241 | } 242 | 243 | /* Push mysql recordset to lua stack */ 244 | static int 245 | lua_mysql_fetch_result(struct lua_State *L) 246 | { 247 | struct mysql_connection *conn = 248 | (struct mysql_connection *) lua_topointer(L, 1); 249 | MYSQL_RES *result = (MYSQL_RES *) lua_topointer(L, 2); 250 | MYSQL_FIELD *fields = mysql_fetch_fields(result); 251 | const unsigned num_fields = mysql_num_fields(result); 252 | 253 | MYSQL_ROW row; 254 | int row_idx = 1; 255 | 256 | /* 257 | * When use_numeric_result is false the table is a result 258 | * set (a return value of this function). Otherwise it is 259 | * a value of "rows" field of the return value. 260 | */ 261 | lua_newtable(L); 262 | do { 263 | row = mysql_fetch_row(result); 264 | if (!row) 265 | break; 266 | /* Create and fill a row table. */ 267 | lua_newtable(L); 268 | unsigned long *len = mysql_fetch_lengths(result); 269 | unsigned col_no; 270 | for (col_no = 0; col_no < num_fields; ++col_no) { 271 | lua_mysql_push_value(L, fields + col_no, row[col_no], 272 | len[col_no], conn->keep_null); 273 | if (conn->use_numeric_result) { 274 | /* Assign to a column number. */ 275 | lua_rawseti(L, -2, col_no + 1); 276 | } else { 277 | /* Assign to a column name. */ 278 | lua_setfield(L, -2, fields[col_no].name); 279 | } 280 | } 281 | lua_rawseti(L, -2, row_idx); 282 | ++row_idx; 283 | } while (true); 284 | 285 | if (!conn->use_numeric_result) 286 | return 1; 287 | 288 | /* 289 | * Create a result set table (a return value of the 290 | * function), swap it with the rows table filled above and 291 | * set result_set.rows = rows. 292 | */ 293 | lua_newtable(L); 294 | lua_insert(L, -2); 295 | lua_setfield(L, -2, "rows"); 296 | 297 | /* 298 | * Create a metadata table and set 299 | * result_set.metadata = metadata. 300 | */ 301 | lua_newtable(L); 302 | unsigned col_no; 303 | for (col_no = 0; col_no < num_fields; ++col_no) { 304 | /* A column metadata. */ 305 | lua_newtable(L); 306 | lua_pushstring(L, fields[col_no].name); 307 | lua_setfield(L, -2, "name"); 308 | lua_pushstring(L, lua_mysql_field_type_to_string( 309 | fields[col_no].type)); 310 | lua_setfield(L, -2, "type"); 311 | lua_rawseti(L, -2, col_no + 1); 312 | } 313 | lua_setfield(L, -2, "metadata"); 314 | 315 | return 1; 316 | } 317 | 318 | /** 319 | * Execute plain sql script without parameters substitution 320 | */ 321 | static int 322 | lua_mysql_execute(struct lua_State *L) 323 | { 324 | struct mysql_connection *conn = lua_check_mysqlconn(L, 1); 325 | MYSQL *raw_conn = conn->raw_conn; 326 | size_t len; 327 | const char *sql = lua_tolstring(L, 2, &len); 328 | int err; 329 | 330 | err = mysql_real_query(raw_conn, sql, len); 331 | if (err) 332 | return lua_mysql_push_error(L, raw_conn); 333 | 334 | lua_pushnumber(L, 0); 335 | int ret_count = 2; 336 | int result_no = 0; 337 | 338 | lua_newtable(L); 339 | while (true) { 340 | MYSQL_RES *res = mysql_use_result(raw_conn); 341 | if (res) { 342 | lua_pushnumber(L, ++result_no); 343 | int fail = 0; 344 | lua_pushcfunction(L, lua_mysql_fetch_result); 345 | lua_pushlightuserdata(L, conn); 346 | lua_pushlightuserdata(L, res); 347 | fail = lua_pcall(L, 2, 1, 0); 348 | if (mysql_errno(raw_conn)) { 349 | ret_count = lua_mysql_push_error(L, raw_conn); 350 | mysql_free_result(res); 351 | return ret_count; 352 | } 353 | mysql_free_result(res); 354 | if (fiber_is_cancelled()) { 355 | lua_pushnumber(L, -2); 356 | safe_pushstring(L, "Fiber was cancelled"); 357 | return 2; 358 | } 359 | if (fail) { 360 | return lua_push_error(L); 361 | } 362 | lua_settable(L, -3); 363 | } 364 | int next_res = mysql_next_result(raw_conn); 365 | if (next_res < 0) 366 | break; 367 | } 368 | return ret_count; 369 | } 370 | 371 | /* 372 | * Push row retrieved from prepared statement 373 | */ 374 | static int 375 | lua_mysql_stmt_push_row(struct lua_State *L) 376 | { 377 | unsigned long col_count = lua_tonumber(L, 1); 378 | MYSQL_BIND *results = (MYSQL_BIND *)lua_topointer(L, 2); 379 | MYSQL_FIELD *fields = (MYSQL_FIELD *)lua_topointer(L, 3); 380 | int keep_null = lua_tointeger(L, 4); 381 | 382 | lua_newtable(L); 383 | unsigned col_no; 384 | for (col_no = 0; col_no < col_count; ++col_no) { 385 | void *data = *results[col_no].is_null ? NULL : 386 | results[col_no].buffer; 387 | lua_pushstring(L, fields[col_no].name); 388 | lua_mysql_push_value(L, fields + col_no, data, 389 | *results[col_no].length, keep_null); 390 | lua_settable(L, -3); 391 | } 392 | return 1; 393 | } 394 | 395 | /* 396 | * Execute sql statement as prepared statement with params 397 | */ 398 | static int 399 | lua_mysql_execute_prepared(struct lua_State *L) 400 | { 401 | struct mysql_connection *conn = lua_check_mysqlconn(L, 1); 402 | size_t len; 403 | const char *sql = lua_tolstring(L, 2, &len); 404 | int ret_count = 0, fail = 0, error = 0; 405 | 406 | MYSQL_STMT *stmt = NULL; 407 | MYSQL_RES *meta = NULL; 408 | MYSQL_BIND *param_binds = NULL; 409 | MYSQL_BIND *result_binds = NULL; 410 | /* Temporary buffer for input parameters. sizeof(uint64_t) should be 411 | * enough to store any number value, any other will be passed as 412 | * string from lua */ 413 | uint64_t *values = NULL; 414 | 415 | /* We hope that all should be fine and push 0 (OK) */ 416 | lua_pushnumber(L, 0); 417 | lua_newtable(L); 418 | ret_count = 2; 419 | stmt = mysql_stmt_init(conn->raw_conn); 420 | if ((error = !stmt)) 421 | goto done; 422 | error = mysql_stmt_prepare(stmt, sql, len); 423 | if (error) 424 | goto done; 425 | /* Alloc space for input parameters */ 426 | unsigned long paramCount = mysql_stmt_param_count(stmt); 427 | param_binds = (MYSQL_BIND *)calloc(sizeof(*param_binds), paramCount); 428 | values = (uint64_t *)calloc(sizeof(*values), paramCount); 429 | /* Setup input bind buffer */ 430 | unsigned param_no; 431 | for (param_no = 0; param_no < paramCount; ++param_no) { 432 | if ((unsigned long)lua_gettop(L) <= param_no + 3) { 433 | param_binds[param_no].buffer_type = MYSQL_TYPE_NULL; 434 | continue; 435 | } 436 | switch (lua_type(L, 3 + param_no)) { 437 | case LUA_TNIL: 438 | param_binds[param_no].buffer_type = MYSQL_TYPE_NULL; 439 | break; 440 | case LUA_TBOOLEAN: 441 | param_binds[param_no].buffer_type = MYSQL_TYPE_TINY; 442 | param_binds[param_no].buffer = values + param_no; 443 | *(bool *)(values + param_no) = 444 | lua_toboolean(L, 3 + param_no); 445 | param_binds[param_no].buffer_length = 1; 446 | break; 447 | case LUA_TNUMBER: 448 | param_binds[param_no].buffer_type = MYSQL_TYPE_DOUBLE; 449 | param_binds[param_no].buffer = values + param_no; 450 | *(double *)(values + param_no) = 451 | lua_tonumber(L, 3 + param_no); 452 | param_binds[param_no].buffer_length = 8; 453 | break; 454 | default: 455 | param_binds[param_no].buffer_type = MYSQL_TYPE_STRING; 456 | param_binds[param_no].buffer = 457 | (char *)lua_tolstring(L, 3 + param_no, &len); 458 | param_binds[param_no].buffer_length = len; 459 | } 460 | } 461 | mysql_stmt_bind_param(stmt, param_binds); 462 | error = mysql_stmt_execute(stmt); 463 | if (error) 464 | goto done; 465 | 466 | meta = mysql_stmt_result_metadata(stmt); 467 | if (!meta) 468 | goto done; 469 | /* Alloc space for output */ 470 | unsigned long col_count = mysql_num_fields(meta); 471 | result_binds = (MYSQL_BIND *)calloc(sizeof(MYSQL_BIND), col_count); 472 | MYSQL_FIELD *fields = mysql_fetch_fields(meta); 473 | unsigned long col_no; 474 | for (col_no = 0; col_no < col_count; ++col_no) { 475 | result_binds[col_no].buffer_type = MYSQL_TYPE_STRING; 476 | result_binds[col_no].buffer = 477 | (char *) malloc(fields[col_no].length); 478 | result_binds[col_no].buffer_length = fields[col_no].length; 479 | result_binds[col_no].length = (unsigned long *) malloc( 480 | sizeof(unsigned long)); 481 | result_binds[col_no].is_null = (my_bool *) malloc( 482 | sizeof(my_bool)); 483 | } 484 | mysql_stmt_bind_result(stmt, result_binds); 485 | lua_pushnumber(L, 1); 486 | lua_newtable(L); 487 | unsigned int row_idx = 1; 488 | while (true) { 489 | int has_no_row = mysql_stmt_fetch(stmt); 490 | if (has_no_row) 491 | break; 492 | lua_pushnumber(L, row_idx); 493 | lua_pushcfunction(L, lua_mysql_stmt_push_row); 494 | lua_pushnumber(L, col_count); 495 | lua_pushlightuserdata(L, result_binds); 496 | lua_pushlightuserdata(L, fields); 497 | lua_pushinteger(L, conn->keep_null); 498 | if ((fail = lua_pcall(L, 4, 1, 0))) 499 | goto done; 500 | lua_settable(L, -3); 501 | ++row_idx; 502 | } 503 | lua_settable(L, -3); 504 | 505 | done: 506 | if (error) 507 | ret_count = lua_mysql_push_error(L, conn->raw_conn); 508 | if (values) 509 | free(values); 510 | if (param_binds) 511 | free(param_binds); 512 | if (result_binds) { 513 | unsigned long col_no; 514 | for (col_no = 0; col_no < col_count; ++col_no) { 515 | free(result_binds[col_no].buffer); 516 | free(result_binds[col_no].length); 517 | free(result_binds[col_no].is_null); 518 | } 519 | free(result_binds); 520 | } 521 | if (meta) 522 | mysql_stmt_free_result(stmt); 523 | if (stmt) 524 | mysql_stmt_close(stmt); 525 | if (fiber_is_cancelled()) { 526 | lua_pushnumber(L, -2); 527 | safe_pushstring(L, "Fiber was cancelled"); 528 | return 2; 529 | } 530 | return fail ? lua_push_error(L) : ret_count; 531 | } 532 | 533 | /** 534 | * close connection 535 | */ 536 | static int 537 | lua_mysql_close(struct lua_State *L) 538 | { 539 | struct mysql_connection **conn_p = (struct mysql_connection **) 540 | luaL_checkudata(L, 1, mysql_driver_label); 541 | if (conn_p == NULL || *conn_p == NULL || (*conn_p)->raw_conn == NULL) { 542 | lua_pushboolean(L, 0); 543 | return 1; 544 | } 545 | mysql_close((*conn_p)->raw_conn); 546 | (*conn_p)->raw_conn = NULL; 547 | free(*conn_p); 548 | *conn_p = NULL; 549 | lua_pushboolean(L, 1); 550 | return 1; 551 | } 552 | 553 | /** 554 | * collect connection 555 | */ 556 | static int 557 | lua_mysql_gc(struct lua_State *L) 558 | { 559 | struct mysql_connection **conn_p = (struct mysql_connection **) 560 | luaL_checkudata(L, 1, mysql_driver_label); 561 | if (conn_p != NULL && *conn_p != NULL && (*conn_p)->raw_conn != NULL) { 562 | mysql_close((*conn_p)->raw_conn); 563 | (*conn_p)->raw_conn = NULL; 564 | } 565 | if (conn_p != NULL && *conn_p != NULL) { 566 | free(*conn_p); 567 | *conn_p = NULL; 568 | } 569 | return 0; 570 | } 571 | 572 | static int 573 | lua_mysql_tostring(struct lua_State *L) 574 | { 575 | MYSQL *raw_conn = lua_check_mysqlconn(L, 1)->raw_conn; 576 | lua_pushfstring(L, "MYSQL: %p", raw_conn); 577 | return 1; 578 | } 579 | 580 | /** 581 | * quote variable 582 | */ 583 | int 584 | lua_mysql_quote(struct lua_State *L) 585 | { 586 | MYSQL *raw_conn = lua_check_mysqlconn(L, 1)->raw_conn; 587 | if (lua_gettop(L) < 2) { 588 | lua_pushnil(L); 589 | return 1; 590 | } 591 | 592 | size_t len; 593 | const char *s = lua_tolstring(L, -1, &len); 594 | char *sout = (char*)malloc(len * 2 + 1); 595 | if (!sout) { 596 | luaL_error(L, "Can't allocate memory for variable"); 597 | } 598 | 599 | len = mysql_real_escape_string(raw_conn, sout, s, len); 600 | lua_pushlstring(L, sout, len); 601 | free(sout); 602 | return 1; 603 | } 604 | 605 | static int 606 | mysql_wait_for_io(my_socket socket, my_bool is_read, int timeout) 607 | { 608 | int coio_event = is_read ? COIO_READ : COIO_WRITE; 609 | int wait_res; 610 | wait_res = coio_wait(socket, coio_event, 611 | timeout >= 0? timeout / 1000.0: TIMEOUT_INFINITY); 612 | if (wait_res == 0) 613 | return 0; 614 | return 1; 615 | } 616 | 617 | /** 618 | * connect to MySQL 619 | */ 620 | static int 621 | lua_mysql_connect(struct lua_State *L) 622 | { 623 | if (lua_gettop(L) < 7) { 624 | luaL_error(L, "Usage: mysql.connect(host, port, user, " 625 | "password, db, use_numeric_result, keep_null)"); 626 | } 627 | 628 | const char *host = lua_tostring(L, 1); 629 | const char *port = lua_tostring(L, 2); 630 | const char *user = lua_tostring(L, 3); 631 | const char *pass = lua_tostring(L, 4); 632 | const char *db = lua_tostring(L, 5); 633 | const int use_numeric_result = lua_toboolean(L, 6); 634 | const int keep_null = lua_toboolean(L, 7); 635 | 636 | MYSQL *raw_conn, *tmp_raw_conn = mysql_init(NULL); 637 | if (!tmp_raw_conn) { 638 | lua_pushinteger(L, -1); 639 | int fail = safe_pushstring(L, 640 | "Can not allocate memory for connector"); 641 | return fail ? lua_push_error(L): 2; 642 | } 643 | 644 | int iport = 0; 645 | const char *usocket = 0; 646 | 647 | if (host != NULL && strcmp(host, "unix/") == 0) { 648 | usocket = port; 649 | host = NULL; 650 | } else if (port != NULL) { 651 | iport = atoi(port); /* 0 is ok */ 652 | } 653 | 654 | mysql_options(tmp_raw_conn, MYSQL_OPT_IO_WAIT, mysql_wait_for_io); 655 | 656 | raw_conn = mysql_real_connect(tmp_raw_conn, host, user, pass, 657 | db, iport, usocket, 658 | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); 659 | 660 | if (!raw_conn) { 661 | lua_pushinteger(L, -1); 662 | int fail = safe_pushstring(L, 663 | (char *) mysql_error(tmp_raw_conn)); 664 | mysql_close(tmp_raw_conn); 665 | return fail ? lua_push_error(L) : 2; 666 | } 667 | 668 | lua_pushnumber(L, 0); 669 | struct mysql_connection *conn = (struct mysql_connection *) 670 | calloc(1, sizeof(struct mysql_connection)); 671 | if (!conn) { 672 | lua_pushinteger(L, -1); 673 | int fail = safe_pushstring(L, 674 | "Can not allocate memory for connector options"); 675 | return fail ? lua_push_error(L): 2; 676 | } 677 | struct mysql_connection **conn_p = (struct mysql_connection **) 678 | lua_newuserdata(L, sizeof(struct mysql_connection *)); 679 | *conn_p = conn; 680 | (*conn_p)->raw_conn = raw_conn; 681 | (*conn_p)->use_numeric_result = use_numeric_result; 682 | (*conn_p)->keep_null = keep_null; 683 | luaL_getmetatable(L, mysql_driver_label); 684 | lua_setmetatable(L, -2); 685 | 686 | return 2; 687 | } 688 | 689 | static int 690 | lua_mysql_reset(lua_State *L) 691 | { 692 | MYSQL *raw_conn = lua_check_mysqlconn(L, 1)->raw_conn; 693 | const char *user = lua_tostring(L, 2); 694 | const char *pass = lua_tostring(L, 3); 695 | const char *db = lua_tostring(L, 4); 696 | 697 | if (mysql_change_user(raw_conn, user, pass, db) == 0) { 698 | lua_pushboolean(L, 1); 699 | } else { 700 | lua_pushboolean(L, 0); 701 | } 702 | 703 | return 1; 704 | } 705 | 706 | LUA_API int 707 | luaopen_mysql_driver(lua_State *L) 708 | { 709 | if (mysql_library_init(0, NULL, NULL)) 710 | luaL_error(L, "Failed to initialize mysql library"); 711 | 712 | /* Create NULL constant. */ 713 | *(void **) luaL_pushcdata(L, luaL_ctypeid(L, "void *")) = NULL; 714 | luaL_nil_ref = luaL_ref(L, LUA_REGISTRYINDEX); 715 | 716 | static const struct luaL_Reg methods [] = { 717 | {"execute_prepared", lua_mysql_execute_prepared}, 718 | {"execute", lua_mysql_execute}, 719 | {"quote", lua_mysql_quote}, 720 | {"close", lua_mysql_close}, 721 | {"reset", lua_mysql_reset}, 722 | {"__tostring", lua_mysql_tostring}, 723 | {"__gc", lua_mysql_gc}, 724 | {NULL, NULL} 725 | }; 726 | 727 | luaL_newmetatable(L, mysql_driver_label); 728 | lua_pushvalue(L, -1); 729 | luaL_register(L, NULL, methods); 730 | lua_setfield(L, -2, "__index"); 731 | lua_pushstring(L, mysql_driver_label); 732 | lua_setfield(L, -2, "__metatable"); 733 | lua_pop(L, 1); 734 | 735 | lua_newtable(L); 736 | static const struct luaL_Reg meta [] = { 737 | {"connect", lua_mysql_connect}, 738 | {NULL, NULL} 739 | }; 740 | luaL_register(L, NULL, meta); 741 | return 1; 742 | } 743 | -------------------------------------------------------------------------------- /mysql/init.lua: -------------------------------------------------------------------------------- 1 | -- init.lua (internal file) 2 | 3 | local fiber = require('fiber') 4 | local driver = require('mysql.driver') 5 | local ffi = require('ffi') 6 | local log = require('log') 7 | 8 | local pool_mt 9 | local conn_mt 10 | 11 | -- The marker for empty slots in a connection pool. 12 | -- 13 | -- Note: It should not be equal to `nil`, because fiber channel's 14 | -- `get` method returns `nil` when a timeout is reached. 15 | local POOL_EMPTY_SLOT = true 16 | 17 | --create a new connection 18 | local function conn_create(mysql_conn) 19 | local queue = fiber.channel(1) 20 | queue:put(true) 21 | local conn = setmetatable({ 22 | usable = true, 23 | conn = mysql_conn, 24 | queue = queue, 25 | }, conn_mt) 26 | return conn 27 | end 28 | 29 | -- There is no reason to make it configurable: either everyting is alright and 30 | -- channel put is immediate or pool is doomed. 31 | local CONN_GC_HOOK_TIMEOUT = 0 32 | 33 | local function conn_gc_hook(pool, conn_id) 34 | local success = pool.queue:put(POOL_EMPTY_SLOT, CONN_GC_HOOK_TIMEOUT) 35 | if not success then 36 | log.error('mysql pool %s internal queue unexpected state: there are no ' .. 37 | 'empty slots, connection %s cannot be put back. It is likely ' .. 38 | 'that someone had messed with pool.queue manually. Closing ' .. 39 | 'the pool...', pool, conn_id) 40 | log.error(debug.traceback) 41 | 42 | pool:close() 43 | end 44 | end 45 | 46 | -- get connection from pool 47 | local function conn_get(pool, timeout) 48 | local mysql_conn = pool.queue:get(timeout) 49 | 50 | -- A timeout was reached. 51 | if mysql_conn == nil then return nil end 52 | 53 | if mysql_conn == POOL_EMPTY_SLOT then 54 | local status 55 | status, mysql_conn = driver.connect(pool.host, pool.port or 0, 56 | pool.user, pool.pass, 57 | pool.db, pool.use_numeric_result, 58 | pool.keep_null) 59 | if status < 0 then 60 | error(mysql_conn) 61 | end 62 | end 63 | 64 | local conn = conn_create(mysql_conn) 65 | local conn_id = tostring(conn) 66 | -- we can use ffi gc to return mysql connection to pool 67 | conn.__gc_hook = ffi.gc(ffi.new('void *'), 68 | function(self) 69 | mysql_conn:close() 70 | -- Fiber yields are prohibited in gc since Tarantool 71 | -- * 2.6.0-138-gd3f1dd720 72 | -- * 2.5.1-105-gc690b3337 73 | -- * 2.4.2-89-g83037df15 74 | -- * 1.10.7-47-g8099cb053 75 | fiber.new(conn_gc_hook, pool, conn_id) 76 | end) 77 | -- If the connection belongs to a connection pool, it must be returned to 78 | -- the pool when calling "close" without actually closing the connection. 79 | -- In the case of a double "close", the behavior is the same as with a 80 | -- simple connection. 81 | conn.close = function(self) 82 | if not self.usable then 83 | error('Connection is not usable') 84 | end 85 | pool:put(self) 86 | return true 87 | end 88 | conn.pool = pool 89 | return conn 90 | end 91 | 92 | local function conn_put(conn) 93 | local mysqlconn = conn.conn 94 | ffi.gc(conn.__gc_hook, nil) 95 | local result = (conn.queue:get() and mysqlconn) or POOL_EMPTY_SLOT 96 | conn.usable = false 97 | conn.queue:put(false) 98 | return result 99 | end 100 | 101 | local function conn_acquire_lock(conn) 102 | if not conn.usable then 103 | error('Connection is not usable') 104 | end 105 | if not conn.queue:get() then 106 | -- Connection could become unusable, so that release lock 107 | -- taken above and throw error. 108 | if not conn.usable then 109 | conn.queue:put(false) 110 | error('Connection is not usable') 111 | end 112 | conn.queue:put(false) 113 | error('Connection is broken') 114 | end 115 | end 116 | 117 | conn_mt = { 118 | __index = { 119 | execute = function(self, sql, ...) 120 | conn_acquire_lock(self) 121 | local status, datas 122 | if select('#', ...) > 0 then 123 | status, datas = self.conn:execute_prepared(sql, ...) 124 | else 125 | status, datas = self.conn:execute(sql) 126 | end 127 | if status ~= 0 then 128 | self.queue:put(status > 0) 129 | error(datas) 130 | end 131 | self.queue:put(true) 132 | return datas, true 133 | end, 134 | begin = function(self) 135 | return self:execute('BEGIN') ~= nil 136 | end, 137 | commit = function(self) 138 | return self:execute('COMMIT') ~= nil 139 | end, 140 | rollback = function(self) 141 | return self:execute('ROLLBACK') ~= nil 142 | end, 143 | ping = function(self) 144 | local status, data, msg = pcall(self.execute, self, 'SELECT 1 AS code') 145 | return msg and data[1][1].code == 1 146 | end, 147 | close = function(self) 148 | conn_acquire_lock(self) 149 | self.usable = false 150 | self.conn:close() 151 | self.queue:put(false) 152 | return true 153 | end, 154 | reset = function(self, user, pass, db) 155 | conn_acquire_lock(self) 156 | -- If the update of the connection settings fails, we must set 157 | -- the connection to a "broken" state and throw an error. 158 | local status = self.conn:reset(user, pass, db) 159 | if not status then 160 | self.queue:put(false) 161 | error('Сonnection settings update failed.') 162 | end 163 | self.queue:put(true) 164 | end, 165 | quote = function(self, value) 166 | conn_acquire_lock(self) 167 | local ret = self.conn:quote(value) 168 | self.queue:put(true) 169 | return ret 170 | end 171 | } 172 | } 173 | 174 | -- Create connection pool. Accepts mysql connection params (host, port, user, 175 | -- password, dbname), size. 176 | local function pool_create(opts) 177 | opts = opts or {} 178 | opts.size = opts.size or 1 179 | local queue = fiber.channel(opts.size) 180 | 181 | for i = 1, opts.size do 182 | local status, conn = driver.connect(opts.host, opts.port or 0, 183 | opts.user, opts.password, 184 | opts.db, opts.use_numeric_result, 185 | opts.keep_null) 186 | if status < 0 then 187 | while queue:count() > 0 do 188 | local mysql_conn = queue:get() 189 | mysql_conn:close() 190 | end 191 | error(conn) 192 | end 193 | queue:put(conn) 194 | end 195 | 196 | return setmetatable({ 197 | -- connection variables 198 | host = opts.host, 199 | port = opts.port, 200 | user = opts.user, 201 | pass = opts.password, 202 | db = opts.db, 203 | size = opts.size, 204 | use_numeric_result = opts.use_numeric_result, 205 | keep_null = opts.keep_null, 206 | 207 | -- private variables 208 | queue = queue, 209 | usable = true 210 | }, pool_mt) 211 | end 212 | 213 | -- Close pool 214 | local function pool_close(self) 215 | self.usable = false 216 | for i = 1, self.size do 217 | local mysql_conn = self.queue:get() 218 | if mysql_conn ~= POOL_EMPTY_SLOT then 219 | mysql_conn:close() 220 | end 221 | end 222 | return true 223 | end 224 | 225 | -- Returns connection 226 | local function pool_get(self, opts) 227 | opts = opts or {} 228 | 229 | if not self.usable then 230 | error('Pool is not usable') 231 | end 232 | local conn = conn_get(self, opts.timeout) 233 | 234 | -- A timeout was reached. 235 | if conn == nil then return nil end 236 | 237 | conn:reset(self.user, self.pass, self.db) 238 | return conn 239 | end 240 | 241 | -- Free binded connection 242 | local function pool_put(self, conn) 243 | if conn.usable then 244 | if conn.pool ~= self then 245 | local msg = ('Trying to put connection from pool %s to pool %s'): 246 | format(conn.pool, self) 247 | error(msg) 248 | end 249 | 250 | self.queue:put(conn_put(conn)) 251 | else 252 | error('Connection is not usable') 253 | end 254 | end 255 | 256 | pool_mt = { 257 | __index = { 258 | get = pool_get; 259 | put = pool_put; 260 | close = pool_close; 261 | } 262 | } 263 | 264 | -- Create connection. Accepts mysql connection params (host, port, user, 265 | -- password, dbname) 266 | local function connect(opts) 267 | opts = opts or {} 268 | 269 | local status, mysql_conn = driver.connect(opts.host, opts.port or 0, 270 | opts.user, opts.password, 271 | opts.db, opts.use_numeric_result, 272 | opts.keep_null) 273 | if status < 0 then 274 | error(mysql_conn) 275 | end 276 | return conn_create(mysql_conn) 277 | end 278 | 279 | return { 280 | connect = connect; 281 | pool_create = pool_create; 282 | } 283 | -------------------------------------------------------------------------------- /rpm/tarantool-mysql.spec: -------------------------------------------------------------------------------- 1 | Name: tarantool-mysql 2 | Version: 1.1.0 3 | Release: 1%{?dist} 4 | Summary: MySQL connector for Tarantool 5 | Group: Applications/Databases 6 | License: BSD 7 | URL: https://github.com/tarantool/mysql 8 | Source0: https://github.com/tarantool/%{name}/archive/%{version}/%{name}-%{version}.tar.gz 9 | BuildRequires: cmake >= 2.8 10 | %if (0%{?fedora} >= 22 || 0%{?rhel} >= 7 || 0%{?sle_version} >= 1500) 11 | # RHEL 6 requires devtoolset 12 | BuildRequires: gcc >= 4.5 13 | %endif 14 | BuildRequires: openssl-devel 15 | BuildRequires: tarantool-devel >= 1.6.8.0 16 | Requires: tarantool >= 1.6.8.0 17 | 18 | %description 19 | MySQL connector for Tarantool. 20 | 21 | %prep 22 | %setup -q -n %{name}-%{version} 23 | 24 | %build 25 | %cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo 26 | make %{?_smp_mflags} 27 | 28 | ## Requires MySQL 29 | #%%check 30 | #make %%{?_smp_mflags} check 31 | 32 | %install 33 | %make_install 34 | 35 | %files 36 | %{_libdir}/tarantool/*/ 37 | %{_datarootdir}/tarantool/*/ 38 | %doc README.md 39 | %{!?_licensedir:%global license %doc} 40 | %license LICENSE 41 | 42 | %changelog 43 | * Wed Feb 17 2016 Roman Tsisyk 1.0.1-1 44 | - Initial version of the RPM spec 45 | -------------------------------------------------------------------------------- /test/mysql.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | package.path = "../?/init.lua;./?/init.lua" 4 | package.cpath = "../?.so;../?.dylib;./?.so;./?.dylib" 5 | 6 | local mysql = require('mysql') 7 | local json = require('json') 8 | local tap = require('tap') 9 | local fiber = require('fiber') 10 | local fio = require('fio') 11 | local ffi = require('ffi') 12 | 13 | local host, port, user, password, db = string.match(os.getenv('MYSQL') or '', 14 | "([^:]*):([^:]*):([^:]*):([^:]*):([^:]*)") 15 | 16 | local conn, err = mysql.connect({ host = host, port = port, user = user, 17 | password = password, db = db }) 18 | if conn == nil then error(err) end 19 | 20 | local p, err = mysql.pool_create({ host = host, port = port, user = user, 21 | password = password, db = db, size = 2 }) 22 | if p == nil then error(err) end 23 | 24 | local function test_old_api(t, conn) 25 | t:plan(16) 26 | -- Add an extension to 'tap' module 27 | getmetatable(t).__index.q = function(test, stmt, result, ...) 28 | test:is_deeply({conn:execute(stmt, ...)}, {{result}, true}, 29 | ... ~= nil and stmt..' % '..json.encode({...}) or stmt) 30 | end 31 | t:ok(conn:ping(), "ping") 32 | t:q("SELECT '123' AS bla, 345", {{ bla = '123', ['345'] = 345 }}) 33 | t:q('SELECT -1 AS neg, NULL AS abc', {{ neg = -1 }}) 34 | t:q('SELECT -1.1 AS neg, 1.2 AS pos', {{ neg = "-1.1", pos = "1.2" }}) 35 | t:q('SELECT ? AS val', {{ val = 'abc' }}, 'abc') 36 | t:q('SELECT ? AS val', {{ val = '1' }}, '1') 37 | t:q('SELECT ? AS val', {{ val = 123 }},123) 38 | t:q('SELECT ? AS val', {{ val = 1 }}, true) 39 | t:q('SELECT ? AS val', {{ val = 0 }}, false) 40 | t:q('SELECT ? AS val, ? AS num, ? AS str', 41 | {{ val = 0, num = 123, str = 'abc'}}, false, 123, 'abc') 42 | t:q('SELECT ? AS bool1, ? AS bool2, ? AS nil, ? AS num, ? AS str', 43 | {{ bool1 = 1, bool2 = 0, num = 123, str = 'abc' }}, true, false, nil, 44 | 123, 'abc') 45 | t:q('SELECT 1 AS one UNION ALL SELECT 2', {{ one = 1}, { one = 2}}) 46 | 47 | t:test("tx", function(t) 48 | t:plan(8) 49 | if not conn:execute("CREATE TEMPORARY TABLE _tx_test (a int)") then 50 | return 51 | end 52 | 53 | t:ok(conn:begin(), "begin") 54 | local _, status = conn:execute("INSERT INTO _tx_test VALUES(10)"); 55 | t:is(status, true, "insert") 56 | t:q('SELECT * FROM _tx_test', {{ a = 10 }}) 57 | t:ok(conn:rollback(), "roolback") 58 | t:q('SELECT * FROM _tx_test', {}) 59 | 60 | t:ok(conn:begin(), "begin") 61 | conn:execute("INSERT INTO _tx_test VALUES(10)"); 62 | t:ok(conn:commit(), "commit") 63 | t:q('SELECT * FROM _tx_test', {{ a = 10 }}) 64 | 65 | conn:execute("DROP TABLE _tx_test") 66 | end) 67 | 68 | t:q('DROP TABLE IF EXISTS unknown_table', nil) 69 | local _, reason = pcall(conn.execute, conn, 'DROP TABLE unknown_table') 70 | t:like(reason, 'unknown_table', 'error') 71 | t:ok(conn:close(), "close") 72 | end 73 | 74 | local function test_gc(test, pool) 75 | test:plan(3) 76 | 77 | -- Case: verify that a pool tracks connections that are not 78 | -- put back, but were collected by GC. 79 | test:test('loss a healthy connection', function(test) 80 | test:plan(1) 81 | 82 | assert(pool.size >= 2, 'test case precondition fails') 83 | 84 | -- Loss one connection. 85 | pool:get() 86 | 87 | -- Loss another one. 88 | local conn = pool:get() -- luacheck: no unused 89 | conn = nil 90 | 91 | -- Collect lost connections. 92 | collectgarbage('collect') 93 | collectgarbage('collect') 94 | 95 | -- Run a fiber scheduler cycle to finish gc process. 96 | fiber.yield() 97 | 98 | -- Verify that a pool is aware of collected connections. 99 | test:is(pool.queue:count(), pool.size, 'all connections are put back') 100 | end) 101 | 102 | -- Case: the same, but for broken connection. 103 | test:test('loss a broken connection', function(test) 104 | test:plan(2) 105 | 106 | assert(pool.size >= 1, 'test case precondition fails') 107 | 108 | -- Get a connection, make a bad query and loss the 109 | -- connection. 110 | local conn = pool:get() 111 | local ok = pcall(conn.execute, conn, 'bad query') 112 | test:ok(not ok, 'a query actually fails') 113 | conn = nil -- luacheck: no unused 114 | 115 | -- Collect the lost connection. 116 | collectgarbage('collect') 117 | collectgarbage('collect') 118 | 119 | -- Run a fiber scheduler cycle to finish gc process. 120 | fiber.yield() 121 | 122 | -- Verify that a pool is aware of collected connections. 123 | test:is(pool.queue:count(), pool.size, 'all connections are put back') 124 | end) 125 | 126 | -- Case: the same, but for closed connection. 127 | test:test('loss a closed connection', function(test) 128 | test:plan(1) 129 | 130 | assert(pool.size >= 1, 'test case precondition fails') 131 | 132 | -- Get a connection, close it and loss the connection. 133 | local conn = pool:get() 134 | conn:close() 135 | conn = nil -- luacheck: no unused 136 | 137 | -- Collect the lost connection. 138 | collectgarbage('collect') 139 | collectgarbage('collect') 140 | 141 | -- Run a fiber scheduler cycle to finish gc process. 142 | fiber.yield() 143 | 144 | -- Verify that a pool is aware of collected connections. 145 | test:is(pool.queue:count(), pool.size, 'all connections are put back') 146 | end) 147 | end 148 | 149 | local function test_conn_fiber1(c, q) 150 | for _ = 1, 10 do 151 | c:execute('SELECT sleep(0.05)') 152 | end 153 | q:put(true) 154 | end 155 | 156 | local function test_conn_fiber2(c, q) 157 | for _ = 1, 25 do 158 | c:execute('SELECT sleep(0.02)') 159 | end 160 | q:put(true) 161 | end 162 | 163 | local function test_conn_concurrent(t, p) 164 | t:plan(1) 165 | local c = p:get() 166 | local q = fiber.channel(2) 167 | local t1 = fiber.time() 168 | fiber.create(test_conn_fiber1, c, q) 169 | fiber.create(test_conn_fiber2, c, q) 170 | q:get() 171 | q:get() 172 | p:put(c) 173 | t:ok(fiber.time() - t1 >= 0.95, 'concurrent connections') 174 | end 175 | 176 | local function test_mysql_int64(t, p) 177 | t:plan(1) 178 | local conn = p:get() 179 | conn:execute('create table int64test (id bigint)') 180 | conn:execute('insert into int64test values(1234567890123456789)') 181 | local d, _ = conn:execute('select id from int64test') 182 | conn:execute('drop table int64test') 183 | t:ok(d[1][1]['id'] == 1234567890123456789LL, 'int64 test') 184 | p:put(conn) 185 | end 186 | 187 | local function test_connection_pool(test, pool) 188 | test:plan(11) 189 | 190 | -- {{{ Case group: all connections are consumed initially. 191 | 192 | assert(pool.queue:is_full(), 'case group precondition fails') 193 | 194 | -- Grab all connections from a pool. 195 | local connections = {} 196 | for _ = 1, pool.size do 197 | table.insert(connections, pool:get()) 198 | end 199 | 200 | -- Case: get and put connections from / to a pool. 201 | test:test('pool:get({}) and pool:put()', function(test) 202 | test:plan(2) 203 | assert(pool.queue:is_empty(), 'test case precondition fails') 204 | 205 | -- Verify that we're unable to get one more connection. 206 | local latch = fiber.channel(1) 207 | local conn 208 | fiber.create(function() 209 | conn = pool:get() 210 | latch:put(true) 211 | end) 212 | local res = latch:get(1) 213 | test:is(res, nil, 'unable to get more connections then a pool size') 214 | 215 | -- Give a connection back and verify that now the fiber 216 | -- above gets this connection. 217 | pool:put(table.remove(connections)) 218 | latch:get() 219 | test:ok(conn ~= nil, 'able to get a connection when it was given back') 220 | 221 | -- Restore everything as it was. 222 | table.insert(connections, conn) 223 | conn = nil -- luacheck: no unused 224 | 225 | assert(pool.queue:is_empty(), 'test case postcondition fails') 226 | end) 227 | 228 | -- Case: get a connection with a timeout. 229 | test:test('pool:get({timeout = <...>})', function(test) 230 | test:plan(3) 231 | assert(pool.queue:is_empty(), 'test case precondition fails') 232 | 233 | -- Verify that we blocks until reach a timeout, then 234 | -- unblocks and get `nil` as a result. 235 | local latch = fiber.channel(1) 236 | local conn 237 | fiber.create(function() 238 | conn = pool:get({timeout = 2}) 239 | latch:put(true) 240 | end) 241 | local res = latch:get(1) 242 | test:is(res, nil, 'pool:get() blocks until a timeout') 243 | local res = latch:get() 244 | test:ok(res ~= nil, 'pool:get() unblocks after a timeout') 245 | test:is(conn, nil, 'pool:get() returns nil if a timeout was reached') 246 | 247 | assert(pool.queue:is_empty(), 'test case postcondition fails') 248 | end) 249 | 250 | -- Give all connections back to the poll. 251 | for _, conn in ipairs(connections) do 252 | pool:put(conn) 253 | end 254 | 255 | assert(pool.queue:is_full(), 'case group postcondition fails') 256 | 257 | -- }}} 258 | 259 | -- {{{ Case group: all connections are ready initially. 260 | 261 | assert(pool.queue:is_full(), 'case group precondition fails') 262 | 263 | -- XXX: Maybe the cases below will look better if rewrite them 264 | -- in a declarative way, like so: 265 | -- 266 | -- local cases = { 267 | -- { 268 | -- 'case name', 269 | -- after_get = function(test, context) 270 | -- -- * Do nothing. 271 | -- -- * Or make a bad query (and assert that 272 | -- -- :execute() fails). 273 | -- -- * Or close the connection. 274 | -- end, 275 | -- after_put = function(test, context) 276 | -- -- * Do nothing. 277 | -- -- * Or loss `context.conn` and trigger 278 | -- -- GC. 279 | -- end, 280 | -- } 281 | -- } 282 | -- 283 | -- Or so: 284 | -- 285 | -- local cases = { 286 | -- { 287 | -- 'case name', 288 | -- after_get = function(test, context) 289 | -- -- * Do nothing. 290 | -- -- * Or make a bad query (and assert that 291 | -- -- :execute() fails). 292 | -- -- * Or close the connection. 293 | -- end, 294 | -- loss_after_put = , 295 | -- } 296 | -- } 297 | -- 298 | -- `loss_after_put` will do the following after put (see 299 | -- comments in cases below): 300 | -- 301 | -- context.conn = nil 302 | -- collectgarbage('collect') 303 | -- assert(pool.queue:is_full(), <...>) 304 | -- local item = pool.queue:get() 305 | -- pool.queue:put(item) 306 | -- test:ok(true, <...>) 307 | 308 | -- Case: get a connection and put it back. 309 | test:test('get and put a connection', function(test) 310 | test:plan(1) 311 | 312 | assert(pool.size >= 1, 'test case precondition fails') 313 | 314 | -- Get a connection. 315 | local conn = pool:get() 316 | 317 | -- Put the connection back and verify that the pool is full. 318 | pool:put(conn) 319 | test:ok(pool.queue:is_full(), 'a connection was given back') 320 | end) 321 | 322 | -- Case: the same, but loss and collect a connection after 323 | -- put. 324 | test:test('get, put and loss a connection', function(test) 325 | test:plan(1) 326 | 327 | assert(pool.size >= 1, 'test case precondition fails') 328 | 329 | -- Get a connection. 330 | local conn = pool:get() 331 | 332 | -- Put the connection back, loss it and trigger GC. 333 | pool:put(conn) 334 | conn = nil -- luacheck: no unused 335 | collectgarbage('collect') 336 | collectgarbage('collect') 337 | 338 | -- Run a fiber scheduler cycle to finish gc process. 339 | fiber.yield() 340 | 341 | -- Verify that the pool is full. 342 | test:ok(pool.queue:is_full(), 'a connection was given back') 343 | end) 344 | 345 | -- Case: get a connection, broke it and put back. 346 | test:test('get, broke and put a connection', function(test) 347 | test:plan(2) 348 | 349 | assert(pool.size >= 1, 'test case precondition fails') 350 | 351 | -- Get a connection and make a bad query. 352 | local conn = pool:get() 353 | local ok = pcall(conn.execute, conn, 'bad query') 354 | test:ok(not ok, 'a query actually fails') 355 | 356 | -- Put the connection back and verify that the pool is full. 357 | pool:put(conn) 358 | test:ok(pool.queue:is_full(), 'a broken connection was given back') 359 | end) 360 | 361 | -- Case: the same, but loss and collect a connection after 362 | -- put. 363 | test:test('get, broke, put and loss a connection', function(test) 364 | test:plan(2) 365 | 366 | assert(pool.size >= 1, 'test case precondition fails') 367 | 368 | -- Get a connection and make a bad query. 369 | local conn = pool:get() 370 | local ok = pcall(conn.execute, conn, 'bad query') 371 | test:ok(not ok, 'a query actually fails') 372 | 373 | -- Put the connection back, loss it and trigger GC. 374 | pool:put(conn) 375 | conn = nil -- luacheck: no unused 376 | collectgarbage('collect') 377 | collectgarbage('collect') 378 | 379 | -- Run a fiber scheduler cycle to finish gc process. 380 | fiber.yield() 381 | 382 | -- Verify that the pool is full 383 | test:ok(pool.queue:is_full(), 'a broken connection was given back') 384 | end) 385 | 386 | -- Case: get a connection and close it. 387 | test:test('get and close a connection', function(test) 388 | test:plan(1) 389 | 390 | assert(pool.size >= 1, 'test case precondition fails') 391 | 392 | -- Get a connection and close it. 393 | local conn = pool:get() 394 | conn:close() 395 | 396 | test:ok(pool.queue:is_full(), 'a connection was given back') 397 | end) 398 | 399 | -- Case: the same, but loss and collect a connection after 400 | -- close. 401 | test:test('get, close and loss a connection', function(test) 402 | test:plan(1) 403 | 404 | assert(pool.size >= 1, 'test case precondition fails') 405 | 406 | -- Get a connection and close it. 407 | local conn = pool:get() 408 | conn:close() 409 | 410 | conn = nil -- luacheck: no unused 411 | collectgarbage('collect') 412 | collectgarbage('collect') 413 | 414 | -- Run a fiber scheduler cycle to finish gc process. 415 | fiber.yield() 416 | 417 | -- Verify that the pool is full 418 | test:ok(pool.queue:is_full(), 'a broken connection was given back') 419 | end) 420 | 421 | -- Case: get a connection, close and put it back. 422 | test:test('get, close and put a connection', function(test) 423 | test:plan(2) 424 | 425 | assert(pool.size >= 1, 'test case precondition fails') 426 | 427 | -- Get a connection. 428 | local conn = pool:get() 429 | 430 | conn:close() 431 | test:ok(pool.queue:is_full(), 'a connection was given back') 432 | 433 | -- Put must throw an error. 434 | local res = pcall(pool.put, pool, conn) 435 | test:ok(not res, 'an error is thrown on "put" after "close"') 436 | end) 437 | 438 | -- Case: close the same connection twice. 439 | test:test('close a connection twice', function(test) 440 | test:plan(2) 441 | 442 | assert(pool.size >= 1, 'test case precondition fails') 443 | 444 | local conn = pool:get() 445 | conn:close() 446 | test:ok(pool.queue:is_full(), 'a connection was given back') 447 | 448 | local res = pcall(conn.close, conn) 449 | test:ok(not res, 'an error is thrown on double "close"') 450 | end) 451 | 452 | -- Case: put the same connection twice. 453 | test:test('put a connection twice', function(test) 454 | test:plan(3) 455 | 456 | assert(pool.size >= 2, 'test case precondition fails') 457 | 458 | local conn_1 = pool:get() 459 | local conn_2 = pool:get() 460 | pool:put(conn_1) 461 | 462 | test:ok(not pool.queue:is_full(), 463 | 'the same connection has not been "put" twice') 464 | 465 | local res = pcall(pool.put, pool, conn_1) 466 | test:ok(not res, 'an error is thrown on double "put"') 467 | 468 | pool:put(conn_2) 469 | test:ok(pool.queue:is_full(), 470 | 'all connections were returned to the pool') 471 | end) 472 | 473 | assert(pool.queue:is_full(), 'case group postcondition fails') 474 | 475 | -- }}} 476 | end 477 | 478 | local function test_connection_reset(test, pool) 479 | test:plan(2) 480 | 481 | assert(pool.queue:is_full(), 'test case precondition fails') 482 | 483 | -- Case: valid credentials were used to "reset" the connection 484 | test:test('reset connection successfully', function(test) 485 | test:plan(1) 486 | 487 | assert(pool.size >= 1, 'test case precondition fails') 488 | 489 | local conn = pool:get() 490 | conn:reset(pool.user, pool.pass, pool.db) 491 | test:ok(conn:ping(), 'connection "reset" successfully') 492 | pool:put(conn) 493 | end) 494 | 495 | -- Case: invalid credentials were used to "reset" the connection 496 | test:test('reset connection failed', function(test) 497 | test:plan(1) 498 | 499 | assert(pool.size >= 1, 'test case precondition fails') 500 | 501 | local conn = pool:get() 502 | local check = pcall(conn.reset, conn, "guinea pig", pool.pass, pool.db) 503 | test:ok(not check, 'connection "reset" fails') 504 | pool:put(conn) 505 | end) 506 | 507 | assert(pool.queue:is_full(), 'test case postcondition fails') 508 | end 509 | 510 | local function test_underlying_conn_closed_during_gc(test) 511 | test:plan(1) 512 | if jit.os ~= 'Linux' then 513 | test:skip('non-Linux OS') 514 | return 515 | end 516 | -- Basing on the statement, that file descriptors are recycled 517 | -- in ascending order. It means, that if we call open() and 518 | -- immediately close(), we get the first vacant index for a 519 | -- new resource in the file descriptor table. It is important 520 | -- not to call any procedures between open() and close() that 521 | -- may affect the file descriptor table. After that, we 522 | -- immediately create connection, which creates a socket. 523 | -- Socket is a resource, which occupies the first vacant index 524 | -- in the file descriptor table. We check that the socket is 525 | -- indeed destroyed by using fcntl() for this index (handle). 526 | -- See: https://www.win.tue.nl/~aeb/linux/vfs/trail-2.html 527 | local fh, err = fio.open('/dev/zero', {'O_RDONLY'}) 528 | if fh == nil then error(err) end 529 | local handle = fh.fh 530 | fh:close() 531 | local conn, err = mysql.connect({ host = host, port = port, user = user, 532 | password = password, db = db }) 533 | if conn == nil then error(err) end 534 | 535 | -- Somehow we lost the connection handle. 536 | conn = nil 537 | collectgarbage('collect') 538 | collectgarbage('collect') 539 | 540 | -- Run a fiber scheduler cycle to finish gc process. 541 | fiber.yield() 542 | 543 | ffi.cdef([[ int fcntl(int fd, int cmd, ...); ]]) 544 | local F_GETFD = 1 545 | test:ok(ffi.C.fcntl(handle, F_GETFD) == -1, 'descriptor is closed') 546 | end 547 | 548 | local function test_ffi_null_printing(test, pool) 549 | test:plan(4) 550 | local function json_result(keep_null, prepared) 551 | local conn, err = mysql.connect({host = host, port = port, user = user, 552 | password = password, db = db, keep_null = keep_null}) 553 | if conn == nil then error(err) end 554 | local rows 555 | if prepared then 556 | rows = conn:execute('SELECT 1 AS w, ? AS x', nil) 557 | else 558 | rows = conn:execute('SELECT 1 AS w, NULL AS x') 559 | end 560 | return json.encode(rows) 561 | end 562 | local res = json_result(true, true) 563 | test:ok(res == '[[{"x":null,"w":1}]]', 'execute_prepared keep_null enabled') 564 | res = json_result(false, true) 565 | test:ok(res == '[[{"w":1}]]', 'execute_prepared keep_null disabled') 566 | res = json_result(true, false) 567 | test:ok(res == '[[{"x":null,"w":1}]]', 'execute keep_null enabled') 568 | res = json_result(false, false) 569 | test:ok(res == '[[{"w":1}]]', 'execute keep_null disabled') 570 | end 571 | 572 | --- gh-34: Check that fiber is not blocked in the following case. 573 | -- A connection conn is acquired from a pool and we acquire a 574 | -- lock. Then we start the first fiber with pool:put(conn), which 575 | -- try to acquire lock and yield. Then the second fiber is started 576 | -- to perform conn:execute(), conn:reset() or conn:quote() which 577 | -- try to acquire lock, because the connection is still marked as 578 | -- 'usable' and yield too. It's appeared that the lock is never 579 | -- released. Now, it's fixed. 580 | -- In the case of conn:close() check that error message is proper. 581 | local function test_block_fiber_inf(test, pool) 582 | test:plan(7) 583 | 584 | local function fiber_block_execute(conn) 585 | local res, err = pcall(conn.execute, conn, 'SELECT "a"') 586 | test:ok(res == false and string.find(err, 'Connection is not usable'), 587 | 'execute failed') 588 | end 589 | 590 | local function fiber_block_reset(conn, pool) 591 | local res, err = pcall(conn.reset, conn, pool.user, pool.pass, pool.db) 592 | test:ok(res == false and string.find(err, 'Connection is not usable'), 593 | 'reset failed') 594 | end 595 | 596 | local function fiber_block_quote(conn, pool) 597 | local res, err = pcall(conn.quote, conn, 1) 598 | test:ok(res == false and string.find(err, 'Connection is not usable'), 599 | 'quote failed') 600 | end 601 | 602 | local timeout = 5 603 | local cases = {fiber_block_execute, fiber_block_reset, fiber_block_quote} 604 | for _, case in ipairs(cases) do 605 | local conn = pool:get() 606 | 607 | conn.queue:get() 608 | fiber.create(pool.put, pool, conn) 609 | local blocked_fiber = fiber.create(case, conn, pool) 610 | conn.queue:put(true) 611 | 612 | local start_time = fiber.time() 613 | local is_alive = true 614 | 615 | -- Wait for the blocked fiber to finish, 616 | -- but no more than "timeout" seconds. 617 | while fiber.time() - start_time < timeout and is_alive do 618 | fiber.sleep(0.1) 619 | is_alive = blocked_fiber:status() ~= 'dead' 620 | end 621 | 622 | test:ok(is_alive == false, 'fiber is not blocked') 623 | end 624 | 625 | local non_pool_conn, err = mysql.connect({ host = host, port = port, 626 | user = user, password = password, db = db }) 627 | if non_pool_conn == nil then error(err) end 628 | 629 | local function make_unusable(conn) 630 | conn.queue:get() 631 | conn.usable = false 632 | conn.queue:put(false) 633 | end 634 | 635 | local function fiber_block_close(conn) 636 | local res, err = pcall(conn.close, conn) 637 | test:ok(res == false and string.find(err, 'Connection is not usable'), 638 | 'close failed') 639 | end 640 | 641 | non_pool_conn.queue:get() 642 | fiber.create(make_unusable, non_pool_conn) 643 | local blocked_fiber = fiber.create(fiber_block_close, non_pool_conn) 644 | non_pool_conn.queue:put(true) 645 | 646 | local start_time = fiber.time() 647 | local is_alive = true 648 | 649 | while fiber.time() - start_time < timeout and is_alive do 650 | fiber.sleep(0.1) 651 | is_alive = blocked_fiber:status() ~= 'dead' 652 | end 653 | end 654 | 655 | local function test_put_to_wrong_pool(test) 656 | test:plan(2) 657 | 658 | local pool_cfg = { 659 | host = host, 660 | port = port, 661 | user = user, 662 | password = password, 663 | db = db, 664 | size = 1 665 | } 666 | 667 | local p1 = mysql.pool_create(pool_cfg) 668 | local p2 = mysql.pool_create(pool_cfg) 669 | 670 | local c = p1:get() 671 | local ok, err = pcall(p2.put, p2, c) 672 | test:is(ok, false, 'Put is failed') 673 | test:like(tostring(err), 674 | ('Trying to put connection from pool %s to pool %s'): 675 | format(p1, p2)) 676 | end 677 | 678 | -- gh-67: Check that garbage collection of a connection from a connection pool 679 | -- not yields. Yielding in gc is prohibited since Tarantool 680 | -- 2.6.0-138-gd3f1dd720, 2.5.1-105-gc690b3337, 2.4.2-89-g83037df15, 681 | -- 1.10.7-47-g8099cb053. If fiber yield happens in object gc, Tarantool process 682 | -- exits with code 1. 683 | -- 684 | -- Gc hook of a connection from a connection pool calls `pool.queue:put(...)`. 685 | -- Fiber `channel:put()` may yield if the channel is full. 686 | -- `channel:put()`/`channel:get()` in mysql driver connection pool is balanced: 687 | -- * the channel capacity is equal to the connection count; 688 | -- * pool create fills the channel with multiple `channel:put()`s 689 | -- all the way through; 690 | -- * `pool:get()` causes single `channel:get()` and creates a connection object 691 | -- with gc hook; 692 | -- * `pool:put()` causes single `channel:put()` and removes gc hook; 693 | -- * gc hook causes single `channel:put()`. 694 | -- 695 | -- There are no other cases of `channel:put()`. `pool:put()` and gc hook cannot 696 | -- be executed both in the same time: either a connection is used inside 697 | -- `pool:put()` and won't be garbage collected or a connection is no longer used 698 | -- anywhere else (including `pool:put()`) and it will be garbage collected. 699 | -- So now there are no valid cases when gc hook may yield unless someone messes 700 | -- up with pool queue. Messing up with pool queue anyway breaks a connection 701 | -- pool internal logic, but at least it won't be causing a process exit. 702 | local function test_conn_from_pool_gc_yield(test) 703 | test:plan(2) 704 | 705 | local p = mysql.pool_create({ 706 | host = host, 707 | port = port, 708 | user = user, 709 | password = password, 710 | db = db, 711 | size = 1 712 | }) 713 | 714 | local c = p:get() 715 | local res = c:ping() 716 | test:ok(res, 'Connection is ok') 717 | p.queue:put('killer msg') 718 | 719 | c = nil -- luacheck: no unused 720 | collectgarbage('collect') 721 | collectgarbage('collect') 722 | 723 | -- Pool should became unavailable after gc hook finish. 724 | -- It may be a couple of fiber scheduler cycles. 725 | local pool_is_usable = true 726 | for _= 1,10 do 727 | fiber.yield() 728 | 729 | pool_is_usable = p.usable 730 | if pool_is_usable == false then 731 | break 732 | end 733 | end 734 | 735 | test:is(pool_is_usable, false, 'Pool is not usable') 736 | end 737 | 738 | local test = tap.test('mysql connector') 739 | test:plan(12) 740 | 741 | test:test('connection old api', test_old_api, conn) 742 | local pool_conn = p:get() 743 | test:test('connection old api via pool', test_old_api, pool_conn) 744 | test:test('garbage collection', test_gc, p) 745 | test:test('concurrent connections', test_conn_concurrent, p) 746 | test:test('int64', test_mysql_int64, p) 747 | test:test('connection pool', test_connection_pool, p) 748 | test:test('connection reset', test_connection_reset, p) 749 | test:test('test_underlying_conn_closed_during_gc', 750 | test_underlying_conn_closed_during_gc, p) 751 | test:test('ffi null printing', test_ffi_null_printing, p) 752 | test:test('test_block_fiber_inf', test_block_fiber_inf, p) 753 | test:test('test_put_to_wrong_pool', test_put_to_wrong_pool) 754 | test:test('test_conn_from_pool_gc_yield', test_conn_from_pool_gc_yield) 755 | p:close() 756 | 757 | os.exit(test:check() and 0 or 1) 758 | -------------------------------------------------------------------------------- /test/numeric_result.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | package.path = "../?/init.lua;./?/init.lua" 4 | package.cpath = "../?.so;../?.dylib;./?.so;./?.dylib" 5 | 6 | local mysql = require('mysql') 7 | local tap = require('tap') 8 | 9 | local host, port, user, password, db = string.match(os.getenv('MYSQL') or '', 10 | "([^:]*):([^:]*):([^:]*):([^:]*):([^:]*)") 11 | 12 | local conn, err = mysql.connect({ host = host, port = port, user = user, 13 | password = password, db = db, use_numeric_result = true }) 14 | if conn == nil then error(err) end 15 | 16 | local p, err = mysql.pool_create({ host = host, port = port, user = user, 17 | password = password, db = db, size = 1, use_numeric_result = true }) 18 | if p == nil then error(err) end 19 | 20 | local function test_mysql_numeric_result(t, conn) 21 | t:plan(1) 22 | 23 | -- Prepare a table. 24 | conn:execute('CREATE TABLE test_numeric_result (' .. 25 | 'col1 INTEGER, col2 INTEGER, col3 INTEGER)') 26 | conn:execute('INSERT INTO test_numeric_result VALUES ' .. 27 | '(1, 2, 3), (4, 5, 6), (7, 8, 9)') 28 | 29 | local results, ok = conn:execute( 30 | 'SELECT col1, col2, col3 FROM test_numeric_result') 31 | local expected = { 32 | { 33 | rows = { 34 | {1, 2, 3}, 35 | {4, 5, 6}, 36 | {7, 8, 9}, 37 | }, 38 | metadata = { 39 | {type = 'long', name = 'col1'}, 40 | {type = 'long', name = 'col2'}, 41 | {type = 'long', name = 'col3'}, 42 | } 43 | } 44 | } 45 | 46 | t:is_deeply({ok, results}, {true, expected}, 'results contain numeric rows') 47 | 48 | -- Drop the table. 49 | conn:execute('DROP TABLE test_numeric_result') 50 | end 51 | 52 | local test = tap.test('use_numeric_result option') 53 | test:plan(2) 54 | 55 | test:test('use_numeric_result via connection', test_mysql_numeric_result, conn) 56 | 57 | local pool_conn = p:get() 58 | test:test('use_numeric_result via pool', test_mysql_numeric_result, pool_conn) 59 | p:put(pool_conn) 60 | p:close() 61 | 62 | os.exit(test:check() == true and 0 or 1) 63 | --------------------------------------------------------------------------------