├── debian ├── compat ├── docs ├── source │ └── format ├── .gitignore ├── changelog ├── rules ├── control └── copyright ├── .gitignore ├── pg ├── CMakeLists.txt ├── init.lua └── driver.c ├── pg-scm-1.rockspec ├── CMakeLists.txt ├── rpm └── tarantool-pg.spec ├── LICENSE ├── .github └── workflows │ └── test.yml ├── cmake ├── FindTarantool.cmake └── FindPostgreSQL.cmake ├── README.md └── test └── pg.test.lua /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | tarantool-pg/ 2 | files 3 | stamp-* 4 | *.substvars 5 | *.log 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | tarantool-pg (1.0.2-1) unstable; urgency=medium 2 | 3 | * Initial release 4 | 5 | -- Roman Tsisyk Wed, 16 Sep 2015 17:16:00 +0300 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pg/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(driver SHARED driver.c) 2 | target_link_libraries(driver ${PostgreSQL_LIBRARIES} -rdynamic) 3 | set_target_properties(driver PROPERTIES PREFIX "" OUTPUT_NAME "driver") 4 | 5 | install(TARGETS driver LIBRARY DESTINATION ${TARANTOOL_INSTALL_LIBDIR}/pg) 6 | install(FILES init.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/pg) 7 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: tarantool-pg 2 | Priority: optional 3 | Section: database 4 | Maintainer: Roman Tsisyk 5 | Build-Depends: debhelper (>= 9), cdbs, 6 | cmake (>= 2.8), 7 | tarantool-dev (>= 1.6.8.0), 8 | libpq-dev (>= 8.1.0) 9 | Standards-Version: 3.9.6 10 | Homepage: https://github.com/tarantool/pg 11 | Vcs-Git: git://github.com/tarantool/pg.git 12 | Vcs-Browser: https://github.com/tarantool/pg 13 | 14 | Package: tarantool-pg 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: PostgreSQL connector for Tarantool 19 | A PostgreSQL connector for Tarantool. 20 | -------------------------------------------------------------------------------- /pg-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'pg' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git+https://github.com/tarantool/pg.git', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = "PostgreSQL connector for Tarantool", 9 | homepage = 'https://github.com/tarantool/pg', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1' 14 | } 15 | external_dependencies = { 16 | TARANTOOL = { 17 | header = 'tarantool/module.h'; 18 | }; 19 | } 20 | build = { 21 | type = 'cmake'; 22 | variables = { 23 | CMAKE_BUILD_TYPE="RelWithDebInfo"; 24 | TARANTOOL_DIR="$(TARANTOOL_DIR)"; 25 | TARANTOOL_INSTALL_LIBDIR="$(LIBDIR)"; 26 | TARANTOOL_INSTALL_LUADIR="$(LUADIR)"; 27 | }; 28 | } 29 | -- vim: syntax=lua 30 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8 FATAL_ERROR) 2 | 3 | project(pg C) 4 | if(NOT CMAKE_BUILD_TYPE) 5 | set(CMAKE_BUILD_TYPE Debug) 6 | endif() 7 | set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) 8 | 9 | # Find Tarantool 10 | set(Tarantool_FIND_REQUIRED ON) 11 | find_package(Tarantool) 12 | include_directories(${TARANTOOL_INCLUDE_DIRS}) 13 | 14 | # Find Postgrsql 15 | set(PostgreSQL_FIND_REQUIRED ON) 16 | find_package(PostgreSQL) 17 | include_directories(${PostgreSQL_INCLUDE_DIRS}) 18 | link_directories(${PostgreSQL_LIBRARY_DIRS}) 19 | 20 | # Set CFLAGS 21 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 22 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra") 23 | 24 | if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") 25 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -undefined dynamic_lookup") 26 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -undefined dynamic_lookup") 27 | endif() 28 | 29 | # Build module 30 | add_subdirectory(pg) 31 | 32 | add_custom_target(check 33 | COMMAND ${PROJECT_SOURCE_DIR}/test/pg.test.lua) 34 | -------------------------------------------------------------------------------- /rpm/tarantool-pg.spec: -------------------------------------------------------------------------------- 1 | Name: tarantool-pg 2 | Version: 1.0.2 3 | Release: 1%{?dist} 4 | Summary: PostgreSQL connector for Tarantool 5 | Group: Applications/Databases 6 | License: BSD 7 | URL: https://github.com/tarantool/pg 8 | Source0: https://github.com/tarantool/%{name}/archive/%{version}/%{name}-%{version}.tar.gz 9 | BuildRequires: cmake >= 2.8 10 | BuildRequires: gcc >= 4.5 11 | BuildRequires: tarantool-devel >= 1.6.8.0 12 | BuildRequires: postgresql-devel >= 8.1.0 13 | Requires: tarantool >= 1.6.8.0 14 | 15 | %description 16 | PostgreSQL connector for Tarantool. 17 | 18 | %prep 19 | %setup -q -n %{name}-%{version} 20 | 21 | %build 22 | %cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo 23 | make %{?_smp_mflags} 24 | 25 | ## Requires postgresql 26 | #%%check 27 | #make %%{?_smp_mflags} check 28 | 29 | %install 30 | %make_install 31 | 32 | %files 33 | %{_libdir}/tarantool/*/ 34 | %{_datarootdir}/tarantool/*/ 35 | %doc README.md 36 | %{!?_licensedir:%global license %doc} 37 | %license LICENSE 38 | 39 | %changelog 40 | * Wed Feb 17 2016 Roman Tsisyk 1.0.1-1 41 | - Initial version of the RPM spec 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: tarantool-pg 3 | Upstream-Contact: roman@tarantool.org 4 | Source: https://github.com/tarantool/pg 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 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | run-tests-ce: 9 | if: | 10 | github.event_name == 'push' || 11 | github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | tarantool: ['1.10', '2.5', '2.6', '2.7', '2.8', '2.x-latest'] 16 | postgres: [10, 11, 12, 13, 14] 17 | runs-on: [ubuntu-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup Tarantool (version is not equal to latest 2.x) 22 | if: matrix.tarantool != '2.x-latest' 23 | uses: tarantool/setup-tarantool@v1 24 | with: 25 | tarantool-version: ${{ matrix.tarantool }} 26 | 27 | - name: Setup Tarantool 2.x (latest) 28 | if: matrix.tarantool == '2.x-latest' 29 | run: | 30 | curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash 31 | sudo apt install -y tarantool tarantool-dev 32 | 33 | - name: Setup requirements 34 | run: | 35 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 36 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 37 | sudo apt-get update 38 | sudo apt-get -y install postgresql-${{ matrix.postgres }} libpq-dev 39 | 40 | - name: Restart PostgreSQL service 41 | run: sudo service postgresql restart 42 | 43 | - name: Build module 44 | run: cmake . && make 45 | 46 | - name: Run tests 47 | run: sudo -u postgres tarantool test/pg.test.lua 48 | -------------------------------------------------------------------------------- /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 ${TARANTOOL_DIR} ENV TARANTOOL_DIR 13 | PATH_SUFFIXES include 14 | ) 15 | 16 | if(TARANTOOL_INCLUDE_DIR) 17 | set(_config "-") 18 | file(READ "${TARANTOOL_INCLUDE_DIR}/tarantool/module.h" _config0) 19 | string(REPLACE "\\" "\\\\" _config ${_config0}) 20 | unset(_config0) 21 | extract_definition(PACKAGE_VERSION TARANTOOL_VERSION ${_config}) 22 | extract_definition(INSTALL_PREFIX _install_prefix ${_config}) 23 | unset(_config) 24 | endif() 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(Tarantool 28 | REQUIRED_VARS TARANTOOL_INCLUDE_DIR VERSION_VAR TARANTOOL_VERSION) 29 | if(TARANTOOL_FOUND) 30 | set(TARANTOOL_INCLUDE_DIRS "${TARANTOOL_INCLUDE_DIR}" 31 | "${TARANTOOL_INCLUDE_DIR}/tarantool/" 32 | CACHE PATH "Include directories for Tarantool") 33 | set(TARANTOOL_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/tarantool" 34 | CACHE PATH "Directory for storing Lua modules written in Lua") 35 | set(TARANTOOL_INSTALL_LUADIR "${CMAKE_INSTALL_DATADIR}/tarantool" 36 | CACHE PATH "Directory for storing Lua modules written in C") 37 | 38 | if (NOT TARANTOOL_FIND_QUIETLY AND NOT FIND_TARANTOOL_DETAILS) 39 | set(FIND_TARANTOOL_DETAILS ON CACHE INTERNAL "Details about TARANTOOL") 40 | message(STATUS "Tarantool LUADIR is ${TARANTOOL_INSTALL_LUADIR}") 41 | message(STATUS "Tarantool LIBDIR is ${TARANTOOL_INSTALL_LIBDIR}") 42 | endif () 43 | endif() 44 | mark_as_advanced(TARANTOOL_INCLUDE_DIRS TARANTOOL_INSTALL_LIBDIR 45 | TARANTOOL_INSTALL_LUADIR) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg - PostgreSQL connector for [Tarantool][] 2 | 3 | [![Build and Test](https://github.com/tarantool/pg/actions/workflows/test.yml/badge.svg)](https://github.com/tarantool/pg/actions/workflows/test.yml) 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | * Tarantool 1.6.5+ with header files (tarantool && tarantool-dev packages) 10 | * PostgreSQL 8.1+ header files (libpq-dev package) 11 | 12 | ### Installation 13 | 14 | Clone repository and then build it using CMake: 15 | 16 | ``` bash 17 | git clone https://github.com/tarantool/pg.git 18 | cd pg && cmake . -DCMAKE_BUILD_TYPE=RelWithDebugInfo 19 | make 20 | make install 21 | ``` 22 | 23 | You can also use LuaRocks: 24 | 25 | ``` bash 26 | luarocks install https://raw.githubusercontent.com/tarantool/pg/master/pg-scm-1.rockspec --local 27 | ``` 28 | 29 | See [tarantool/rocks][TarantoolRocks] for LuaRocks configuration details. 30 | 31 | ### Usage 32 | 33 | ``` lua 34 | local pg = require('pg') 35 | local conn = pg.connect({host = localhost, user = 'user', pass = 'pass', db = 'db'}) 36 | local tuples = conn:execute("SELECT $1 AS a, 'xx' AS b", 42) 37 | conn:begin() 38 | conn:execute("INSERT INTO test VALUES(1, 2, 3)") 39 | conn:commit() 40 | ``` 41 | 42 | ## API Documentation 43 | 44 | ### `conn = pg:connect(opts = {})` 45 | 46 | Connect to a database. 47 | 48 | *Options*: 49 | 50 | - `host` - a hostname to connect 51 | - `port` - a port numner to connect 52 | - `user` - username 53 | - `pass` or `password` - a password 54 | - `db` - a database name 55 | - `conn_string` (mutual exclusive with host, port, user, pass, db) - PostgreSQL 56 | [connection string][PQconnstring] 57 | 58 | *Returns*: 59 | 60 | - `connection ~= nil` on success 61 | - `error(reason)` on error 62 | 63 | ### `conn:execute(statement, ...)` 64 | 65 | Execute a statement with arguments in the current transaction. 66 | 67 | *Returns*: 68 | - `{ { { column1 = value, column2 = value }, ... }, { {column1 = value, ... }, ...}, ...}, true` on success 69 | - `error(reason)` on error 70 | 71 | *Example*: 72 | ``` 73 | tarantool> conn:execute("SELECT $1 AS a, 'xx' AS b", 42) 74 | --- 75 | - - - a: 42 76 | b: xx 77 | ... 78 | ``` 79 | 80 | ### `conn:begin()` 81 | 82 | Begin a transaction. 83 | 84 | *Returns*: `true` 85 | 86 | ### `conn:commit()` 87 | 88 | Commit current transaction. 89 | 90 | *Returns*: `true` 91 | 92 | ### `conn:rollback()` 93 | 94 | Rollback current transaction. 95 | 96 | *Returns*: `true` 97 | 98 | ### `conn:ping()` 99 | 100 | Execute a dummy statement to check that connection is alive. 101 | 102 | *Returns*: 103 | 104 | - `true` on success 105 | - `false` on failure 106 | 107 | #### `pool = pg.pool_create(opts = {})` 108 | 109 | Create a connection pool with count of size established connections. 110 | 111 | *Options*: 112 | 113 | - `host` - hostname to connect to 114 | - `port` - port number to connect to 115 | - `user` - username 116 | - `password` - password 117 | - `db` - database name 118 | - `size` - count of connections in pool 119 | 120 | *Returns* 121 | 122 | - `pool ~=nil` on success 123 | - `error(reason)` on error 124 | 125 | ### `conn = pool:get()` 126 | 127 | Get a connection from pool. Reset connection before returning it. If connection 128 | is broken then it will be reestablished. If there is no free connections then 129 | calling fiber will sleep until another fiber returns some connection to pool. 130 | 131 | *Returns*: 132 | 133 | - `conn ~= nil` 134 | 135 | ### `pool:put(conn)` 136 | 137 | Return a connection to connection pool. 138 | 139 | *Options* 140 | 141 | - `conn` - a connection 142 | 143 | ## Comments 144 | 145 | All calls to connections api will be serialized, so it should to be safe to 146 | use one connection from some count of fibers. But you should understand, 147 | that you can have some unwanted behavior across db calls, for example if 148 | another fiber 'injects' some sql between two your calls. 149 | 150 | # See Also 151 | 152 | * [Tests][] 153 | * [Tarantool][] 154 | * [Tarantool Rocks][TarantoolRocks] 155 | 156 | [Tarantool]: http://github.com/tarantool/tarantool 157 | [Tests]: https://github.com/tarantool/pg/tree/master/test 158 | [PQconnstring]: http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-CONNSTRING 159 | [TarantoolRocks]: https://github.com/tarantool/rocks 160 | -------------------------------------------------------------------------------- /test/pg.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 pg = require('pg') 7 | local json = require('json') 8 | local tap = require('tap') 9 | local f = require('fiber') 10 | 11 | local host, port, user, pass, db = string.match(os.getenv('PG') or '', 12 | "([^:]*):([^:]*):([^:]*):([^:]*):([^:]*)") 13 | 14 | local p, msg = pg.pool_create({ host = host, port = port, user = user, pass = pass, 15 | db = db, raise = false, size = 2 }) 16 | if p == nil then error(msg) end 17 | 18 | local conn, msg = pg.connect({ host = host, port = port, user = user, pass = pass, 19 | db = db, raise = false}) 20 | if conn == nil then error(msg) end 21 | 22 | function test_old_api(t, c) 23 | t:plan(15) 24 | t:ok(c ~= nil, "connection") 25 | -- Add an extension to 'tap' module 26 | getmetatable(t).__index.q = function(test, stmt, result, ...) 27 | test:is_deeply({c:execute(stmt, ...)}, {{result}, true}, 28 | ... ~= nil and stmt..' % '..json.encode({...}) or stmt) 29 | end 30 | t:ok(c:ping(), "ping") 31 | if p == nil then 32 | return 33 | end 34 | t:q('SELECT 123::text AS bla, 345', {{ bla = '123', ['?column?'] = 345 }}) 35 | t:q('SELECT -1 AS neg, NULL AS abc', {{ neg = -1 }}) 36 | t:q('SELECT -1.1 AS neg, 1.2 AS pos', {{ neg = -1.1, pos = 1.2 }}) 37 | t:q('SELECT ARRAY[1,2] AS arr, 1.2 AS pos', {{ arr = '{1,2}', pos = 1.2}}) 38 | t:q('SELECT $1 AS val', {{ val = 'abc' }}, 'abc') 39 | t:q('SELECT $1 AS val', {{ val = 123 }}, 123) 40 | t:q('SELECT $1 AS val', {{ val = true }}, true) 41 | t:q('SELECT $1 AS val', {{ val = false }}, false) 42 | t:q('SELECT $1 AS val, $2 AS num, $3 AS str', 43 | {{ val = false, num = 123, str = 'abc'}}, false, 123, 'abc') 44 | t:q('SELECT * FROM (VALUES (1,2), (2,3)) t', { 45 | { column1 = 1, column2 = 2}, { column1 = 2, column2 = 3}}) 46 | 47 | t:test("tx", function(t) 48 | t:plan(7) 49 | if not c:execute("CREATE TABLE _tx_test (a int)") then 50 | return 51 | end 52 | 53 | t:ok(c:begin(), "begin") 54 | c:execute("INSERT INTO _tx_test VALUES(10)"); 55 | t:q('SELECT * FROM _tx_test', {{ a = 10 }}) 56 | t:ok(c:rollback(), "roolback") 57 | t:q('SELECT * FROM _tx_test', {}) 58 | 59 | t:ok(c:begin(), "begin") 60 | c:execute("INSERT INTO _tx_test VALUES(10)"); 61 | t:ok(c:commit(), "commit") 62 | t:q('SELECT * FROM _tx_test', {{ a = 10 }}) 63 | 64 | c:execute("DROP TABLE _tx_test") 65 | end) 66 | 67 | t:test("numeric string binding", function(t) 68 | t:plan(1) 69 | if not c:execute([[CREATE TABLE TEST1 ( TEST1_ID SERIAL PRIMARY KEY, 70 | TEST1_REQUESTID VARCHAR(32) 71 | NOT NULL DEFAULT '' );]]) then 72 | return 73 | end 74 | c:execute('INSERT INTO TEST1 (TEST1_REQUESTID) VALUES ($1)', '12E30') 75 | t:q('SELECT TEST1_REQUESTID FROM TEST1;', 76 | {{ test1_requestid = '12E30'}}) 77 | 78 | c:execute("DROP TABLE TEST1") 79 | end) 80 | 81 | local status, reason = pcall(c.execute, c, 'DROP TABLE unknown_table') 82 | t:like(reason, 'unknown_table', 'error') 83 | end 84 | 85 | function test_gc(t, p) 86 | t:plan(1) 87 | p:get() 88 | local c = p:get() 89 | c = nil 90 | collectgarbage('collect') 91 | t:is(p.queue:count(), p.size, 'gc connections') 92 | end 93 | 94 | function test_conn_fiber1(c, q) 95 | for i = 1, 10 do 96 | c:execute('SELECT pg_sleep(0.05)') 97 | end 98 | q:put(true) 99 | end 100 | 101 | function test_conn_fiber2(c, q) 102 | for i = 1, 25 do 103 | c:execute('SELECT pg_sleep(0.02)') 104 | end 105 | q:put(true) 106 | end 107 | 108 | function test_conn_concurrent(t, p) 109 | t:plan(1) 110 | local c = p:get() 111 | local q = f.channel(2) 112 | local t1 = f.time() 113 | f.create(test_conn_fiber1, c, q) 114 | f.create(test_conn_fiber2, c, q) 115 | q:get() 116 | q:get() 117 | p:put(c) 118 | t:ok(f.time() - t1 >= 0.95, 'concurrent connections') 119 | end 120 | 121 | function test_pg_int64(t, p) 122 | t:plan(1) 123 | conn = p:get() 124 | conn:execute('create table int64test (id bigint)') 125 | conn:execute('insert into int64test values(1234567890123456789)') 126 | local r, m = conn:execute('select id from int64test') 127 | conn:execute('drop table int64test') 128 | t:ok(r[1][1]['id'] == 1234567890123456789LL, 'int64 test') 129 | p:put(conn) 130 | end 131 | 132 | tap.test('connection old api', test_old_api, conn) 133 | local pool_conn = p:get() 134 | tap.test('connection old api via pool', test_old_api, pool_conn) 135 | p:put(pool_conn) 136 | tap.test('test collection connections', test_gc, p) 137 | tap.test('connection concurrent', test_conn_concurrent, p) 138 | tap.test('int64', test_pg_int64, p) 139 | p:close() 140 | -------------------------------------------------------------------------------- /pg/init.lua: -------------------------------------------------------------------------------- 1 | -- init.lua (internal file) 2 | 3 | local fiber = require('fiber') 4 | local driver = require('pg.driver') 5 | local ffi = require('ffi') 6 | 7 | local pool_mt 8 | local conn_mt 9 | 10 | --create a new connection 11 | local function conn_create(pg_conn) 12 | local queue = fiber.channel(1) 13 | queue:put(true) 14 | local conn = setmetatable({ 15 | usable = true, 16 | conn = pg_conn, 17 | queue = queue, 18 | }, conn_mt) 19 | 20 | return conn 21 | end 22 | 23 | -- get connection from pool 24 | local function conn_get(pool) 25 | local pg_conn = pool.queue:get() 26 | local status 27 | if pg_conn == nil then 28 | status, pg_conn = driver.connect(pool.conn_string) 29 | if status < 0 then 30 | return error(pg_conn) 31 | end 32 | end 33 | local conn = conn_create(pg_conn, pool) 34 | conn.__gc_hook = ffi.gc(ffi.new('void *'), 35 | function(self) 36 | pg_conn:close() 37 | pool.queue:put(nil) 38 | end) 39 | return conn 40 | end 41 | 42 | local function conn_put(conn) 43 | local pgconn = conn.conn 44 | ffi.gc(conn.__gc_hook, nil) 45 | if not conn.queue:get() then 46 | conn.usable = false 47 | return nil 48 | end 49 | conn.usable = false 50 | return pgconn 51 | end 52 | 53 | conn_mt = { 54 | __index = { 55 | execute = function(self, sql, ...) 56 | if not self.usable then 57 | return get_error(self.raise.pool, 'Connection is not usable') 58 | end 59 | if not self.queue:get() then 60 | self.queue:put(false) 61 | return get_error(self.raise.pool, 'Connection is broken') 62 | end 63 | local status, datas = self.conn:execute(sql, ...) 64 | if status ~= 0 then 65 | self.queue:put(status > 0) 66 | return error(datas) 67 | end 68 | self.queue:put(true) 69 | return datas, true 70 | end, 71 | begin = function(self) 72 | return self:execute('BEGIN') ~= nil 73 | end, 74 | commit = function(self) 75 | return self:execute('COMMIT') ~= nil 76 | end, 77 | rollback = function(self) 78 | return self:execute('ROLLBACK') ~= nil 79 | end, 80 | ping = function(self) 81 | local status, data, msg = pcall(self.execute, self, 'SELECT 1 AS code') 82 | return msg and data[1][1].code == 1 83 | end, 84 | close = function(self) 85 | if not self.usable then 86 | return error('Connection is not usable') 87 | end 88 | if not self.queue:get() then 89 | self.queue:put(false) 90 | return error('Connection is broken') 91 | end 92 | self.usable = false 93 | self.conn:close() 94 | self.queue:put(false) 95 | return true 96 | end, 97 | active = function(self) 98 | if not self.usable then 99 | return get_error(self.raise.pool, 'Connection is not usable') 100 | end 101 | if not self.queue:get() then 102 | self.queue:put(false) 103 | return get_error(self.raise.pool, 'Connection is broken') 104 | end 105 | local status, msg = self.conn:active() 106 | if status ~= 1 then 107 | self.queue:put(false) 108 | return get_error(self.raise.pool, msg) 109 | end 110 | self.queue:put(true) 111 | return msg 112 | end 113 | } 114 | } 115 | 116 | local function build_conn_string(opts) 117 | if opts.conn_string then 118 | return opts.conn_string 119 | end 120 | local connb = {} 121 | if opts.host then 122 | table.insert(connb, string.format(" host='%s'", opts.host)) 123 | end 124 | if opts.port then 125 | table.insert(connb, string.format(" port='%s'", opts.port)) 126 | end 127 | if opts.user then 128 | table.insert(connb, string.format(" user='%s'", opts.user)) 129 | end 130 | if opts.pass or opts.password then 131 | table.insert(connb, string.format(" password='%s'", 132 | opts.pass or opts.password)) 133 | end 134 | if opts.db then 135 | table.insert(connb, string.format(" dbname='%s'", opts.db)) 136 | end 137 | return table.concat(connb) 138 | end 139 | 140 | -- Create connection pool. Accepts pg connection params (host, port, user, 141 | -- password, dbname) separatelly or in one string, size and raise flag. 142 | local function pool_create(opts) 143 | opts = opts or {} 144 | local conn_string = build_conn_string(opts) 145 | opts.size = opts.size or 1 146 | local queue = fiber.channel(opts.size) 147 | 148 | for i = 1, opts.size do 149 | local status, conn = driver.connect(conn_string) 150 | if status < 0 then 151 | while queue:count() > 0 do 152 | local pg_conn = queue:get() 153 | pg_conn:close() 154 | end 155 | if status < 0 then 156 | return error(conn) 157 | end 158 | end 159 | queue:put(conn) 160 | end 161 | 162 | return setmetatable({ 163 | -- connection variables 164 | host = opts.host, 165 | port = opts.port, 166 | user = opts.user, 167 | pass = opts.pass, 168 | db = opts.db, 169 | size = opts.size, 170 | conn_string = conn_string, 171 | 172 | -- private variables 173 | queue = queue, 174 | usable = true 175 | }, pool_mt) 176 | end 177 | 178 | -- Close pool 179 | local function pool_close(self) 180 | self.usable = false 181 | for i = 1, self.size do 182 | local pg_conn = self.queue:get() 183 | if pg_conn ~= nil then 184 | pg_conn:close() 185 | end 186 | end 187 | end 188 | 189 | -- Returns connection 190 | local function pool_get(self) 191 | if not self.usable then 192 | return get_error(self.raise, 'Pool is not usable') 193 | end 194 | local conn = conn_get(self) 195 | local reset_sql = 'BEGIN; RESET ALL; COMMIT;' 196 | if conn:active() then 197 | reset_sql = 'ROLLBACK; ' .. reset_sql 198 | end 199 | conn:execute(reset_sql) 200 | return conn 201 | end 202 | 203 | -- Free binded connection 204 | local function pool_put(self, conn) 205 | if conn.usable then 206 | self.queue:put(conn_put(conn)) 207 | end 208 | end 209 | 210 | pool_mt = { 211 | __index = { 212 | get = pool_get; 213 | put = pool_put; 214 | close = pool_close; 215 | } 216 | } 217 | 218 | -- Create connection. Accepts pg connection params (host, port, user, 219 | -- password, dbname) separatelly or in one string and raise flag. 220 | local function connect(opts) 221 | opts = opts or {} 222 | 223 | local conn_string = build_conn_string(opts) 224 | local status, pg_conn = driver.connect(conn_string) 225 | if status < 0 then 226 | return error(pg_conn) 227 | end 228 | return conn_create(pg_conn) 229 | end 230 | 231 | return { 232 | connect = connect; 233 | pool_create = pool_create; 234 | } 235 | -------------------------------------------------------------------------------- /cmake/FindPostgreSQL.cmake: -------------------------------------------------------------------------------- 1 | # - Find the PostgreSQL installation. 2 | # In Windows, we make the assumption that, if the PostgreSQL files are installed, the default directory 3 | # will be C:\Program Files\PostgreSQL. 4 | # 5 | # This module defines 6 | # PostgreSQL_LIBRARIES - the PostgreSQL libraries needed for linking 7 | # PostgreSQL_INCLUDE_DIRS - the directories of the PostgreSQL headers 8 | # PostgreSQL_VERSION_STRING - the version of PostgreSQL found (since CMake 2.8.8) 9 | 10 | #============================================================================= 11 | # Copyright 2004-2009 Kitware, Inc. 12 | # 13 | # Distributed under the OSI-approved BSD License (the "License"); 14 | # see accompanying file Copyright.txt for details. 15 | # 16 | # This software is distributed WITHOUT ANY WARRANTY; without even the 17 | # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 | # See the License for more information. 19 | #============================================================================= 20 | # (To distribute this file outside of CMake, substitute the full 21 | # License text for the above reference.) 22 | 23 | # ---------------------------------------------------------------------------- 24 | # History: 25 | # This module is derived from the module originally found in the VTK source tree. 26 | # 27 | # ---------------------------------------------------------------------------- 28 | # Note: 29 | # PostgreSQL_ADDITIONAL_VERSIONS is a variable that can be used to set the 30 | # version mumber of the implementation of PostgreSQL. 31 | # In Windows the default installation of PostgreSQL uses that as part of the path. 32 | # E.g C:\Program Files\PostgreSQL\8.4. 33 | # Currently, the following version numbers are known to this module: 34 | # "9.1" "9.0" "8.4" "8.3" "8.2" "8.1" "8.0" 35 | # 36 | # To use this variable just do something like this: 37 | # set(PostgreSQL_ADDITIONAL_VERSIONS "9.2" "8.4.4") 38 | # before calling find_package(PostgreSQL) in your CMakeLists.txt file. 39 | # This will mean that the versions you set here will be found first in the order 40 | # specified before the default ones are searched. 41 | # 42 | # ---------------------------------------------------------------------------- 43 | # You may need to manually set: 44 | # PostgreSQL_INCLUDE_DIR - the path to where the PostgreSQL include files are. 45 | # PostgreSQL_LIBRARY_DIR - The path to where the PostgreSQL library files are. 46 | # If FindPostgreSQL.cmake cannot find the include files or the library files. 47 | # 48 | # ---------------------------------------------------------------------------- 49 | # The following variables are set if PostgreSQL is found: 50 | # PostgreSQL_FOUND - Set to true when PostgreSQL is found. 51 | # PostgreSQL_INCLUDE_DIRS - Include directories for PostgreSQL 52 | # PostgreSQL_LIBRARY_DIRS - Link directories for PostgreSQL libraries 53 | # PostgreSQL_LIBRARIES - The PostgreSQL libraries. 54 | # 55 | # ---------------------------------------------------------------------------- 56 | # If you have installed PostgreSQL in a non-standard location. 57 | # (Please note that in the following comments, it is assumed that 58 | # points to the root directory of the include directory of PostgreSQL.) 59 | # Then you have three options. 60 | # 1) After CMake runs, set PostgreSQL_INCLUDE_DIR to /include and 61 | # PostgreSQL_LIBRARY_DIR to wherever the library pq (or libpq in windows) is 62 | # 2) Use CMAKE_INCLUDE_PATH to set a path to /PostgreSQL<-version>. This will allow find_path() 63 | # to locate PostgreSQL_INCLUDE_DIR by utilizing the PATH_SUFFIXES option. e.g. In your CMakeLists.txt file 64 | # set(CMAKE_INCLUDE_PATH ${CMAKE_INCLUDE_PATH} "/include") 65 | # 3) Set an environment variable called ${PostgreSQL_ROOT} that points to the root of where you have 66 | # installed PostgreSQL, e.g. . 67 | # 68 | # ---------------------------------------------------------------------------- 69 | 70 | set(PostgreSQL_INCLUDE_PATH_DESCRIPTION "top-level directory containing the PostgreSQL include directories. E.g /usr/local/include/PostgreSQL/8.4 or C:/Program Files/PostgreSQL/8.4/include") 71 | set(PostgreSQL_INCLUDE_DIR_MESSAGE "Set the PostgreSQL_INCLUDE_DIR cmake cache entry to the ${PostgreSQL_INCLUDE_PATH_DESCRIPTION}") 72 | set(PostgreSQL_LIBRARY_PATH_DESCRIPTION "top-level directory containing the PostgreSQL libraries.") 73 | set(PostgreSQL_LIBRARY_DIR_MESSAGE "Set the PostgreSQL_LIBRARY_DIR cmake cache entry to the ${PostgreSQL_LIBRARY_PATH_DESCRIPTION}") 74 | set(PostgreSQL_ROOT_DIR_MESSAGE "Set the PostgreSQL_ROOT system variable to where PostgreSQL is found on the machine E.g C:/Program Files/PostgreSQL/8.4") 75 | 76 | set(PostgreSQL_KNOWN_ROOTS "/usr/include" "/usr/include/postgres" 77 | "/usr/include/pgsql" "/usr/include/postgresql") 78 | set(PostgreSQL_KNOWN_VERSIONS "9.6" "9.5" "9.4" "9.3" "9.2" "9.1" "9.0") 79 | 80 | set(PostgreSQL_ROOT_DIRECTORIES) 81 | foreach (root ${PostgreSQL_KNOWN_ROOTS} ) 82 | list(APPEND PostgreSQL_ROOT_DIRECTORIES "${root}") 83 | foreach (version ${PostgreSQL_KNOWN_VERSIONS} ) 84 | list(APPEND PostgreSQL_ROOT_DIRECTORIES "${root}/${version}" ) 85 | endforeach() 86 | endforeach() 87 | 88 | foreach (version ${PostgreSQL_KNOWN_VERSIONS} ) 89 | list(APPEND PostgreSQL_ROOT_DIRECTORIES "/usr/pgsql-${version}" ) 90 | endforeach() 91 | 92 | # 93 | # Look for an installation. 94 | # 95 | find_path(PostgreSQL_INCLUDE_DIR 96 | NAMES libpq-fe.h 97 | PATHS 98 | # Look in other places. 99 | ${PostgreSQL_ROOT_DIRECTORIES} 100 | PATH_SUFFIXES 101 | pgsql 102 | postgresql 103 | include 104 | # Help the user find it if we cannot. 105 | DOC "The ${PostgreSQL_INCLUDE_DIR_MESSAGE}" 106 | ) 107 | 108 | # The PostgreSQL library. 109 | set (PostgreSQL_LIBRARY_TO_FIND pq) 110 | # Setting some more prefixes for the library 111 | set (PostgreSQL_LIB_PREFIX "") 112 | if ( WIN32 ) 113 | set (PostgreSQL_LIB_PREFIX ${PostgreSQL_LIB_PREFIX} "lib") 114 | set ( PostgreSQL_LIBRARY_TO_FIND ${PostgreSQL_LIB_PREFIX}${PostgreSQL_LIBRARY_TO_FIND}) 115 | endif() 116 | 117 | find_library( PostgreSQL_LIBRARY 118 | NAMES ${PostgreSQL_LIBRARY_TO_FIND} 119 | PATHS 120 | ${PostgreSQL_ROOT_DIRECTORIES} 121 | PATH_SUFFIXES 122 | lib 123 | ) 124 | get_filename_component(PostgreSQL_LIBRARY_DIR ${PostgreSQL_LIBRARY} PATH) 125 | 126 | if (PostgreSQL_INCLUDE_DIR AND EXISTS "${PostgreSQL_INCLUDE_DIR}/pg_config.h") 127 | file(STRINGS "${PostgreSQL_INCLUDE_DIR}/pg_config.h" pgsql_version_str 128 | REGEX "^#define[\t ]+PG_VERSION[\t ]+\".*\"") 129 | 130 | string(REGEX REPLACE "^#define[\t ]+PG_VERSION[\t ]+\"([^\"]*)\".*" "\\1" 131 | PostgreSQL_VERSION_STRING "${pgsql_version_str}") 132 | unset(pgsql_version_str) 133 | endif() 134 | 135 | # Did we find anything? 136 | include(FindPackageHandleStandardArgs) 137 | find_package_handle_standard_args(PostgreSQL 138 | REQUIRED_VARS PostgreSQL_LIBRARY PostgreSQL_INCLUDE_DIR 139 | VERSION_VAR PostgreSQL_VERSION_STRING) 140 | set( PostgreSQL_FOUND ${POSTGRESQL_FOUND}) 141 | 142 | # Now try to get the include and library path. 143 | if(PostgreSQL_FOUND) 144 | 145 | set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR} ) 146 | set(PostgreSQL_LIBRARY_DIRS ${PostgreSQL_LIBRARY_DIR} ) 147 | set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY_TO_FIND}) 148 | 149 | #message("Final PostgreSQL include dir: ${PostgreSQL_INCLUDE_DIRS}") 150 | #message("Final PostgreSQL library dir: ${PostgreSQL_LIBRARY_DIRS}") 151 | #message("Final PostgreSQL libraries: ${PostgreSQL_LIBRARIES}") 152 | endif() 153 | 154 | mark_as_advanced(PostgreSQL_INCLUDE_DIR PostgreSQL_LIBRARY ) 155 | -------------------------------------------------------------------------------- /pg/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 | #include 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | /* PostgreSQL types (see catalog/pg_type.h) */ 40 | #define INT2OID 21 41 | #define INT4OID 23 42 | #define INT8OID 20 43 | #define NUMERICOID 1700 44 | #define BOOLOID 16 45 | #define TEXTOID 25 46 | 47 | #include 48 | 49 | #undef PACKAGE_VERSION 50 | #include 51 | 52 | /** 53 | * Infinity timeout from tarantool_ev.c. I mean, this should be in 54 | * a module.h file. 55 | */ 56 | #define TIMEOUT_INFINITY 365 * 86400 * 100.0 57 | static const char pg_driver_label[] = "__tnt_pg_driver"; 58 | 59 | static int 60 | save_pushstring_wrapped(struct lua_State *L) 61 | { 62 | char *str = (char *)lua_topointer(L, 1); 63 | lua_pushstring(L, str); 64 | return 1; 65 | } 66 | 67 | static int 68 | safe_pushstring(struct lua_State *L, char *str) 69 | { 70 | lua_pushcfunction(L, save_pushstring_wrapped); 71 | lua_pushlightuserdata(L, str); 72 | return lua_pcall(L, 1, 1, 0); 73 | } 74 | 75 | static inline PGconn * 76 | lua_check_pgconn(struct lua_State *L, int index) 77 | { 78 | PGconn **conn_p = (PGconn **)luaL_checkudata(L, index, pg_driver_label); 79 | if (conn_p == NULL || *conn_p == NULL) 80 | luaL_error(L, "Driver fatal error (closed connection " 81 | "or not a connection)"); 82 | return *conn_p; 83 | } 84 | 85 | /** 86 | * Push native lua error with code -3 87 | */ 88 | static int 89 | lua_push_error(struct lua_State *L) 90 | { 91 | lua_pushnumber(L, -3); 92 | lua_insert(L, -2); 93 | return 2; 94 | } 95 | 96 | /** 97 | * Parse pg values to lua 98 | */ 99 | static int 100 | parse_pg_value(struct lua_State *L, PGresult *res, int row, int col) 101 | { 102 | if (PQgetisnull(res, row, col)) 103 | return false; 104 | // Procedure called in pcall environment, don't use safe_pushstring 105 | lua_pushstring(L, PQfname(res, col)); 106 | const char *val = PQgetvalue(res, row, col); 107 | int len = PQgetlength(res, row, col); 108 | 109 | switch (PQftype(res, col)) { 110 | case INT2OID: 111 | case INT4OID: 112 | case NUMERICOID: { 113 | lua_pushlstring(L, val, len); 114 | double v = lua_tonumber(L, -1); 115 | lua_pop(L, 1); 116 | lua_pushnumber(L, v); 117 | break; 118 | } 119 | case INT8OID: { 120 | long long v = strtoll(val, NULL, 10); 121 | luaL_pushint64(L, v); 122 | break; 123 | } 124 | case BOOLOID: 125 | if (*val == 't' || *val == 'T') 126 | lua_pushboolean(L, 1); 127 | else 128 | lua_pushboolean(L, 0); 129 | break; 130 | default: 131 | lua_pushlstring(L, val, len); 132 | } 133 | lua_settable(L, -3); 134 | return true; 135 | } 136 | 137 | /** 138 | * Push query result tuples to given table on lua stack 139 | */ 140 | static int 141 | safe_pg_parsetuples(struct lua_State *L) 142 | { 143 | PGresult *res = (PGresult *)lua_topointer(L, 1); 144 | int row, rows = PQntuples(res); 145 | int col, cols = PQnfields(res); 146 | lua_newtable(L); 147 | for (row = 0; row < rows; ++row) { 148 | lua_pushnumber(L, row + 1); 149 | lua_newtable(L); 150 | for (col = 0; col < cols; ++col) 151 | parse_pg_value(L, res, row, col); 152 | lua_settable(L, -3); 153 | } 154 | return 1; 155 | } 156 | 157 | #if 0 158 | Now we return only recordset without status 159 | /** 160 | * Push query execution status to lua stack 161 | */ 162 | static int 163 | safe_pg_parsestatus(struct lua_State *L) 164 | { 165 | PGresult *r = (PGresult *)lua_topointer(L, 1); 166 | lua_newtable(L); 167 | lua_pushstring(L, "tuples"); 168 | if (*PQcmdTuples(r) == 0) { 169 | lua_pushnumber(L, 0); 170 | } else { 171 | lua_pushstring(L, PQcmdTuples(r)); 172 | double v = lua_tonumber(L, -1); 173 | lua_pop(L, 1); 174 | lua_pushnumber(L, v); 175 | } 176 | lua_settable(L, -3); 177 | lua_pushstring(L, "message"); 178 | lua_pushstring(L, PQcmdStatus(r)); 179 | lua_settable(L, -3); 180 | return 1; 181 | } 182 | #endif 183 | 184 | /** 185 | * Wait until postgres returns something 186 | */ 187 | static int 188 | pg_wait_for_result(PGconn *conn) 189 | { 190 | int sock = PQsocket(conn); 191 | while (true) { 192 | if (fiber_is_cancelled()) 193 | return -2; 194 | if (PQconsumeInput(conn) != 1) 195 | return PQstatus(conn) == CONNECTION_BAD ? -1: 0; 196 | if (PQisBusy(conn)) 197 | coio_wait(sock, COIO_READ, TIMEOUT_INFINITY); 198 | else 199 | break; 200 | } 201 | return 1; 202 | } 203 | 204 | /** 205 | * Appends result from postgres to lua table 206 | */ 207 | static int 208 | pg_resultget(struct lua_State *L, PGconn *conn, int *res_no, int status_ok) 209 | { 210 | int wait_res = pg_wait_for_result(conn); 211 | if (wait_res != 1) 212 | { 213 | lua_pushinteger(L, wait_res); 214 | if (wait_res == -2) 215 | safe_pushstring(L, "Fiber was cancelled"); 216 | else 217 | lua_pushstring(L, PQerrorMessage(conn)); 218 | return 0; 219 | } 220 | 221 | PGresult *pg_res = PQgetResult(conn); 222 | if (!pg_res) { 223 | return 0; 224 | } 225 | if (status_ok != 1) { 226 | // Fail mode, just skip all other results 227 | PQclear(pg_res); 228 | return status_ok; 229 | } 230 | int res = -1; 231 | int fail = 0; 232 | int status = PQresultStatus(pg_res); 233 | switch (status) { 234 | case PGRES_TUPLES_OK: 235 | lua_pushinteger(L, (*res_no)++); 236 | lua_pushcfunction(L, safe_pg_parsetuples); 237 | lua_pushlightuserdata(L, pg_res); 238 | fail = lua_pcall(L, 1, 1, 0); 239 | if (!fail) 240 | lua_settable(L, -3); 241 | case PGRES_COMMAND_OK: 242 | res = 1; 243 | break; 244 | case PGRES_FATAL_ERROR: 245 | case PGRES_EMPTY_QUERY: 246 | case PGRES_NONFATAL_ERROR: 247 | lua_pushinteger(L, 248 | (PQstatus(conn) == CONNECTION_BAD) ? -1: 1); 249 | fail = safe_pushstring(L, PQerrorMessage(conn)); 250 | break; 251 | default: 252 | lua_pushinteger(L, -1); 253 | fail = safe_pushstring(L, 254 | "Unwanted execution result status"); 255 | } 256 | 257 | PQclear(pg_res); 258 | if (fail) { 259 | lua_push_error(L); 260 | res = -1; 261 | } 262 | return res; 263 | } 264 | 265 | /** 266 | * Parse lua value 267 | */ 268 | static void 269 | lua_parse_param(struct lua_State *L, 270 | int idx, const char **value, int *length, Oid *type) 271 | { 272 | if (lua_isnil(L, idx)) { 273 | *value = NULL; 274 | *length = 0; 275 | *type = 0; 276 | return; 277 | } 278 | 279 | if (lua_isboolean(L, idx)) { 280 | static const char pg_true[] = "t"; 281 | static const char pg_false[] = "f"; 282 | *value = lua_toboolean(L, idx) ? pg_true : pg_false; 283 | *length = 1; 284 | *type = BOOLOID; 285 | return; 286 | } 287 | 288 | if (lua_type(L, idx) == LUA_TNUMBER) { 289 | size_t len; 290 | *value = lua_tolstring(L, idx, &len); 291 | *length = len; 292 | *type = NUMERICOID; 293 | return; 294 | } 295 | 296 | // We will pass all other types as strings 297 | size_t len; 298 | *value = lua_tolstring(L, idx, &len); 299 | *length = len; 300 | *type = TEXTOID; 301 | } 302 | 303 | /** 304 | * Start query execution 305 | */ 306 | static int 307 | lua_pg_execute(struct lua_State *L) 308 | { 309 | PGconn *conn = lua_check_pgconn(L, 1); 310 | if (!lua_isstring(L, 2)) { 311 | safe_pushstring(L, "Second param should be a sql command"); 312 | return lua_push_error(L); 313 | } 314 | const char *sql = lua_tostring(L, 2); 315 | int paramCount = lua_gettop(L) - 2; 316 | 317 | const char **paramValues = NULL; 318 | int *paramLengths = NULL; 319 | Oid *paramTypes = NULL; 320 | 321 | int res = 0; 322 | if (paramCount > 0) { 323 | /* Allocate chunk of memory for params */ 324 | char *buf = (char *)lua_newuserdata(L, paramCount * 325 | (sizeof(*paramValues) + sizeof(*paramLengths) + 326 | sizeof(*paramTypes))); 327 | 328 | paramValues = (const char **) buf; 329 | buf += paramCount * sizeof(*paramValues); 330 | paramLengths = (int *) buf; 331 | buf += paramCount * sizeof(*paramLengths); 332 | paramTypes = (Oid *) buf; 333 | 334 | int idx; 335 | for (idx = 0; idx < paramCount; ++idx) { 336 | lua_parse_param(L, idx + 3, paramValues + idx, 337 | paramLengths + idx, paramTypes + idx); 338 | } 339 | res = PQsendQueryParams(conn, sql, paramCount, paramTypes, 340 | paramValues, paramLengths, NULL, 0); 341 | } 342 | else 343 | res = PQsendQuery(conn, sql); 344 | 345 | if (res == -1) { 346 | lua_pushinteger(L, PQstatus(conn) == CONNECTION_BAD ? -1: 0); 347 | lua_pushstring(L, PQerrorMessage(conn)); 348 | return 2; 349 | } 350 | lua_pushinteger(L, 0); 351 | lua_newtable(L); 352 | 353 | int res_no = 1; 354 | int status_ok = 1; 355 | while ((status_ok = pg_resultget(L, conn, &res_no, status_ok))); 356 | 357 | return 2; 358 | } 359 | 360 | /** 361 | * Test that connection has active transaction 362 | */ 363 | static int 364 | lua_pg_transaction_active(struct lua_State *L) 365 | { 366 | PGconn *conn = lua_check_pgconn(L, 1); 367 | PGTransactionStatusType status; 368 | switch (status = PQtransactionStatus(conn)){ 369 | case PQTRANS_IDLE: 370 | case PQTRANS_ACTIVE: 371 | case PQTRANS_INTRANS: 372 | case PQTRANS_INERROR: 373 | lua_pushinteger(L, 1); 374 | lua_pushboolean(L, status != PQTRANS_IDLE); 375 | return 2; 376 | default: 377 | lua_pushinteger(L, -1); 378 | lua_pushstring(L, PQerrorMessage(conn)); 379 | return 2; 380 | } 381 | } 382 | 383 | /** 384 | * Close connection 385 | */ 386 | static int 387 | lua_pg_close(struct lua_State *L) 388 | { 389 | PGconn **conn_p = (PGconn **)luaL_checkudata(L, 1, pg_driver_label); 390 | if (conn_p == NULL || *conn_p == NULL) { 391 | lua_pushboolean(L, 0); 392 | return 1; 393 | } 394 | PQfinish(*conn_p); 395 | *conn_p = NULL; 396 | lua_pushboolean(L, 1); 397 | return 1; 398 | } 399 | 400 | /** 401 | * Collect connection 402 | */ 403 | static int 404 | lua_pg_gc(struct lua_State *L) 405 | { 406 | PGconn **conn_p = (PGconn **)luaL_checkudata(L, 1, pg_driver_label); 407 | if (conn_p && *conn_p) 408 | PQfinish(*conn_p); 409 | if (conn_p) 410 | *conn_p = NULL; 411 | return 0; 412 | } 413 | 414 | static int 415 | lua_pg_tostring(struct lua_State *L) 416 | { 417 | PGconn *conn = lua_check_pgconn(L, 1); 418 | lua_pushfstring(L, "PQconn: %p", conn); 419 | return 1; 420 | } 421 | 422 | /** 423 | * Prints warnings from Postgresql into tarantool log 424 | */ 425 | static void 426 | pg_notice(void *arg, const char *message) 427 | { 428 | say_info("Postgresql: %s", message); 429 | (void)arg; 430 | } 431 | 432 | #if PG_VERSION_NUM >= 90000 433 | /** 434 | * Quote variable 435 | */ 436 | static int 437 | lua_pg_quote(struct lua_State *L) 438 | { 439 | if (lua_gettop(L) < 2) { 440 | lua_pushnil(L); 441 | return 1; 442 | } 443 | PGconn *conn = lua_check_pgconn(L, 1); 444 | size_t len; 445 | const char *s = lua_tolstring(L, 2, &len); 446 | 447 | s = PQescapeLiteral(conn, s, len); 448 | 449 | if (!s) 450 | luaL_error(L, "Can't allocate memory"); 451 | int fail = safe_pushstring(L, (char *)s); 452 | free((void *)s); 453 | return fail ? lua_push_error(L): 1; 454 | } 455 | 456 | /** 457 | * Quote identifier 458 | */ 459 | static int 460 | lua_pg_quote_ident(struct lua_State *L) 461 | { 462 | if (lua_gettop(L) < 2) { 463 | lua_pushnil(L); 464 | return 1; 465 | } 466 | PGconn *conn = lua_check_pgconn(L, 1); 467 | size_t len; 468 | const char *s = lua_tolstring(L, 2, &len); 469 | 470 | s = PQescapeIdentifier(conn, s, len); 471 | 472 | if (!s) 473 | luaL_error(L, "Can't allocate memory"); 474 | int fail = safe_pushstring(L, (char *)s); 475 | free((void *)s); 476 | return fail ? lua_push_error(L): 1; 477 | } 478 | 479 | #endif 480 | 481 | /** 482 | * Start connection to postgresql 483 | */ 484 | static int 485 | lua_pg_connect(struct lua_State *L) 486 | { 487 | if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) 488 | luaL_error(L, "Usage: pg.connect(connstring)"); 489 | 490 | const char *constr = lua_tostring(L, 1); 491 | PGconn *conn = NULL; 492 | 493 | conn = PQconnectStart(constr); 494 | if (!conn) { 495 | lua_pushinteger(L, -1); 496 | int fail = safe_pushstring(L, 497 | "Can't allocate PG connection structure"); 498 | return fail ? lua_push_error(L): 2; 499 | } 500 | 501 | if (PQstatus(conn) == CONNECTION_BAD) { 502 | lua_pushinteger(L, -1); 503 | int fail = safe_pushstring(L, PQerrorMessage(conn)); 504 | PQfinish(conn); 505 | return fail ? lua_push_error(L): 2; 506 | } 507 | 508 | PostgresPollingStatusType status = PGRES_POLLING_WRITING; 509 | while (true) { 510 | if (fiber_is_cancelled()) { 511 | lua_pushinteger(L, -2); 512 | safe_pushstring(L, "Fiber was cancelled"); 513 | return 1; 514 | } 515 | 516 | int sock = PQsocket(conn); 517 | if (status == PGRES_POLLING_READING) 518 | coio_wait(sock, COIO_READ, TIMEOUT_INFINITY); 519 | if (status == PGRES_POLLING_WRITING) 520 | coio_wait(sock, COIO_WRITE, TIMEOUT_INFINITY); 521 | 522 | status = PQconnectPoll(conn); 523 | if (status == PGRES_POLLING_OK) { 524 | PQsetNoticeProcessor(conn, pg_notice, NULL); 525 | lua_pushinteger(L, 1); 526 | PGconn **conn_p = (PGconn **) 527 | lua_newuserdata(L, sizeof(conn)); 528 | *conn_p = conn; 529 | luaL_getmetatable(L, pg_driver_label); 530 | lua_setmetatable(L, -2); 531 | return 2; 532 | } 533 | if (status == PGRES_POLLING_READING || status == PGRES_POLLING_WRITING) 534 | continue; 535 | break; 536 | } 537 | lua_pushinteger(L, -1); 538 | int fail = safe_pushstring(L, PQerrorMessage(conn)); 539 | PQfinish(conn); 540 | return fail ? lua_push_error(L): 2; 541 | } 542 | 543 | LUA_API int 544 | luaopen_pg_driver(lua_State *L) 545 | { 546 | static const struct luaL_Reg methods [] = { 547 | {"execute", lua_pg_execute}, 548 | #if PG_VERSION_NUM >= 90000 549 | {"quote", lua_pg_quote}, 550 | {"quote_ident", lua_pg_quote_ident}, 551 | #endif 552 | {"close", lua_pg_close}, 553 | {"active", lua_pg_transaction_active}, 554 | {"__tostring", lua_pg_tostring}, 555 | {"__gc", lua_pg_gc}, 556 | {NULL, NULL} 557 | }; 558 | 559 | luaL_newmetatable(L, pg_driver_label); 560 | lua_pushvalue(L, -1); 561 | luaL_register(L, NULL, methods); 562 | lua_setfield(L, -2, "__index"); 563 | lua_pushstring(L, pg_driver_label); 564 | lua_setfield(L, -2, "__metatable"); 565 | lua_pop(L, 1); 566 | 567 | lua_newtable(L); 568 | static const struct luaL_Reg meta [] = { 569 | {"connect", lua_pg_connect}, 570 | {NULL, NULL} 571 | }; 572 | luaL_register(L, NULL, meta); 573 | return 1; 574 | } 575 | --------------------------------------------------------------------------------