├── .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 |
--------------------------------------------------------------------------------