From 22e3687093e0fb81e081e53d2778b0d9355c587e Mon Sep 17 00:00:00 2001 From: SunBeau Date: Tue, 30 Apr 2024 08:46:16 +0800 Subject: [PATCH 1/3] rename qdatabase.c to qdatabase_mysql.c --- src/Makefile.in | 2 +- src/doxygen.conf | 2 +- src/extensions/{qdatabase.c => qdatabase_mysql.c} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/extensions/{qdatabase.c => qdatabase_mysql.c} (99%) diff --git a/src/Makefile.in b/src/Makefile.in index 698affb5..2b22288e 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -96,7 +96,7 @@ QLIBCEXT_OBJS = \ extensions/qaconf.o \ extensions/qlog.o \ extensions/qhttpclient.o \ - extensions/qdatabase.o \ + extensions/qdatabase_mysql.o \ extensions/qtokenbucket.o ## Which compiler & options for release diff --git a/src/doxygen.conf b/src/doxygen.conf index be196845..4530d7c5 100644 --- a/src/doxygen.conf +++ b/src/doxygen.conf @@ -770,7 +770,7 @@ INPUT = ../README.md \ extensions/qaconf.c \ extensions/qlog.c \ extensions/qhttpclient.c \ - extensions/qdatabase.c \ + extensions/qdatabase_mysql.c \ extensions/qtokenbucket.c # This tag can be used to specify the character encoding of the source files diff --git a/src/extensions/qdatabase.c b/src/extensions/qdatabase_mysql.c similarity index 99% rename from src/extensions/qdatabase.c rename to src/extensions/qdatabase_mysql.c index ad238307..4734e479 100644 --- a/src/extensions/qdatabase.c +++ b/src/extensions/qdatabase_mysql.c @@ -27,7 +27,7 @@ *****************************************************************************/ /** - * @file qdatabase.c Database wrapper. + * @file qdatabase_mysql.c Database wrapper. * * Database header files should be included prior to qlibcext.h in your source * codes like below. From 19e21b5c4102b3bbabb15ef56f15fb6a2bd3250e Mon Sep 17 00:00:00 2001 From: SunBeau Date: Tue, 30 Apr 2024 09:25:19 +0800 Subject: [PATCH 2/3] Support PostgreSQL: interface/test-cases/ci-testing --- .github/workflows/ci-run-test-extensions.yml | 89 ++ CMakeLists.txt | 31 +- Makefile.in | 4 +- README.md | 2 +- configure.ac | 22 + include/qlibc/extensions/qdatabase.h | 48 +- src/Makefile.in | 1 + src/doxygen.conf | 1 + src/extensions/qdatabase_mysql.c | 6 +- src/extensions/qdatabase_pgsql.c | 1031 ++++++++++++++++++ tests/CMakeLists.txt | 4 + tests/Makefile.in | 17 +- tests/test_qdatabase_pgsql.c | 327 ++++++ 13 files changed, 1568 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci-run-test-extensions.yml create mode 100644 src/extensions/qdatabase_pgsql.c create mode 100644 tests/test_qdatabase_pgsql.c diff --git a/.github/workflows/ci-run-test-extensions.yml b/.github/workflows/ci-run-test-extensions.yml new file mode 100644 index 00000000..8956692f --- /dev/null +++ b/.github/workflows/ci-run-test-extensions.yml @@ -0,0 +1,89 @@ +# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +name: ci-run-test-extensions + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + test-on-ubuntu-2204: + name: test-on-ubuntu-22.04 + runs-on: ubuntu-latest + container: + image: docker.io/ubuntu:22.04 + env: + TZ: Asia/Shanghai + DEBIAN_FRONTEND: noninteractive + volumes: + - ${{ github.workspace }}:${{ github.workspace }} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Intall Deps + run: | + export DEBIAN_FRONTEND=noninteractive + echo "Asia/Shanghai" > /etc/timezone + apt-get update + apt-get install -y gcc make cmake autoconf sudo lsb-release curl ca-certificates tzdata + dpkg-reconfigure --frontend noninteractive tzdata + + - name: Install database + run: | + install -d /usr/share/postgresql-common/pgdg + curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc + sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + apt-get update + apt-get install -y postgresql-15 libpq-dev + + - name: Start database + run: | + sudo -u postgres echo "123456" > /tmp/pg.pwfile + sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/data -U postgres -A md5 --pwfile=/tmp/pg.pwfile + sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/data -l /tmp/data/logfile start + PGPASSWORD=123456 psql -h 127.0.0.1 -U postgres -c "SELECT 1" + + - name: Configure And make + working-directory: ${{ github.workspace }} + run: | + autoconf + ./configure --with-pgsql \ + --with-pgsql-incdir=/usr/include/postgresql \ + --with-pgsql-libdir=/usr/lib/x86_64-linux-gnu + make + + - name: Test + working-directory: ${{ github.workspace }} + run: | + cd tests/ + make test + make test-ext + make clean + cd ../ + make clean + + - name: Cmake And make + working-directory: ${{ github.workspace }} + run: | + mkdir build/ && cd build/ + cmake .. \ + -DWITH_PGSQL=ON \ + -DWITH_PGSQL_INCLUDE_DIR=/usr/include/postgresql \ + -DWITH_PGSQL_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu + make && make install + + - name: Cmake Test + working-directory: ${{ github.workspace }} + run: | + cd build/ + make test diff --git a/CMakeLists.txt b/CMakeLists.txt index 63ce5540..103840bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,25 @@ IF (NOT CMAKE_USE_PTHREADS_INIT) MESSAGE(FATAL_ERROR "Couldn't find pthreads.") ENDIF() +IF(NOT "${WITH_PGSQL}" STREQUAL "") + message(STATUS "with_pgsql: ${WITH_PGSQL}") + # check "libpq-fe.h" + if(EXISTS "${WITH_PGSQL_INCLUDE_DIR}/libpq-fe.h") + message(STATUS "found 'libpq-fe.h' header.") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_PGSQL") + else() + message(FATAL_ERROR "Cannot find 'libpq-fe.h' header. Use '-DWITH_PGSQL_INCLUDE_DIR=DIR' to specify the directory where 'libpq-fe.h' is located.") + endif() + + # check "libpq.so" + if(EXISTS "${WITH_PGSQL_LIBRARY_DIR}/libpq.so") + message(STATUS "found 'libpq.so' library.") + SET(QLIBC_LINK_LIBS "pq") + else() + message(FATAL_ERROR "Cannot find 'libpq.so' library. Use '-DWITH_PGSQL_LIBRARY_DIR=DIR' to specify the directory where 'libpq.so' is located.") + endif() +ENDIF() + SET(SRC_SUBPATHS containers/*.c utilities/*.c @@ -80,20 +99,26 @@ FILE(GLOB_RECURSE SRC_LIB_EXT SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${qlibc_SOURCE_DIR}/lib) SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${qlibc_SOURCE_DIR}/lib) +LIST(APPEND QLIBC_INCLUDE_DIRECTORIES ${qlibc_SOURCE_DIR}/src/internal) +LIST(APPEND QLIBC_INCLUDE_DIRECTORIES ${WITH_PGSQL_INCLUDE_DIR}) +LIST(APPEND QLIBC_LINK_DIRECTORIES ${WITH_PGSQL_LIBRARY_DIR}) + +INCLUDE_DIRECTORIES(${QLIBC_INCLUDE_DIRECTORIES}) +LINK_DIRECTORIES(${QLIBC_LINK_DIRECTORIES}) + ADD_LIBRARY(qlibc-static STATIC ${SRC_LIB}) ADD_LIBRARY(qlibc SHARED ${SRC_LIB}) ADD_LIBRARY(qlibcext-static STATIC ${SRC_LIB_EXT}) ADD_LIBRARY(qlibcext SHARED ${SRC_LIB_EXT}) -INCLUDE_DIRECTORIES(${qlibc_SOURCE_DIR}/src/internal) TARGET_INCLUDE_DIRECTORIES(qlibc-static PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_INCLUDE_DIRECTORIES(qlibc PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_INCLUDE_DIRECTORIES(qlibcext-static PUBLIC ${qlibc_SOURCE_DIR}/include/qlibc) TARGET_LINK_LIBRARIES(qlibc-static PRIVATE ${CMAKE_THREAD_LIBS_INIT}) TARGET_LINK_LIBRARIES(qlibc PRIVATE ${CMAKE_THREAD_LIBS_INIT}) -TARGET_LINK_LIBRARIES(qlibcext-static PRIVATE ${CMAKE_THREAD_LIBS_INIT}) -TARGET_LINK_LIBRARIES(qlibcext PUBLIC qlibc) +TARGET_LINK_LIBRARIES(qlibcext-static PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${QLIBC_LINK_LIBS}) +TARGET_LINK_LIBRARIES(qlibcext PUBLIC qlibc ${QLIBC_LINK_LIBS}) SET(QLIBC_HEADER "${qlibc_SOURCE_DIR}/include/qlibc") INSTALL(DIRECTORY ${QLIBC_HEADER} DESTINATION include) diff --git a/Makefile.in b/Makefile.in index 6372eeb1..ae431b4e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -34,6 +34,9 @@ all: test: all make -C tests test +test-ext: all + make -C tests test-ext + install: make -C src install @@ -44,7 +47,6 @@ uninstall: clean: make -C src clean - distclean: clean @for DIR in src tests examples; do \ echo "===> $${DIR}"; \ diff --git a/README.md b/README.md index 86df44d3..dfcf1421 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please refer the LICENSE document included in the package for more details. * INI-style Configuration File Parser. * HTTP client. * Rotating File Logger. - * Database(MySQL) interface. + * Database(MySQL/PostgreSQL) interface. * [Token-Bucket](https://en.wikipedia.org/wiki/Token_bucket) ## qLibc Tables at a Glance diff --git a/configure.ac b/configure.ac index 0630fa55..bceef450 100644 --- a/configure.ac +++ b/configure.ac @@ -198,6 +198,28 @@ if test "$withval" = yes; then fi fi +AC_ARG_WITH([pgsql],[AS_HELP_STRING([--with-pgsql], [This will enable PostgreSQL database support in qdatabase extension API. When it's enabled, user applications need to link libpq library. (ex: -lpq)])],[],[withval=no]) +if test "$withval" = yes; then + # check libpq-fe.h + AC_CHECK_FILE([$with_pgsql_incdir/libpq-fe.h],[withval=yes],[withval=no]) + if test "$withval" = yes; then + AC_MSG_NOTICE([find '$with_pgsql_incdir/libpq-fe.h']) + CPPFLAGS="$CPPFLAGS -DENABLE_PGSQL -I$with_pgsql_incdir" + else + AC_MSG_FAILURE([Cannot find 'libpq-fe.h' header. Use --with-pgsql-incdir=DIR to specify the directory where 'libpq-fe.h' is located.]) + fi + # check libpq.so + AC_CHECK_FILE([$with_pgsql_libdir/libpq.so],[withval=yes],[withval=no]) + if test "$withval" = yes; then + AC_MSG_NOTICE([find '$with_pgsql_libdir/libpq.so']) + CPPFLAGS="$CPPFLAGS -Wl,-rpath,$with_pgsql_libdir -L$with_pgsql_libdir -lpq" + else + AC_MSG_FAILURE([Cannot find 'libpq.so' library. Use --with-pgsql-libdir=DIR to specify the directory where 'libpq.so' is located.]) + fi +fi +AC_ARG_WITH([pgsql_incdir],[AS_HELP_STRING([--with-pgsql-incdir=DIR], [site header files for PostgreSQL in DIR.])],[],[withval=no]) +AC_ARG_WITH([pgsql_libdir],[AS_HELP_STRING([--with-pgsql-libdir=DIR], [site library files for PostgreSQL in DIR])],[],[withval=no]) + AC_MSG_NOTICE([CFLAGS $CFLAGS]) AC_MSG_NOTICE([CPPFLAGS $CPPFLAGS]) #AC_MSG_NOTICE([LIBS $LIBS]) diff --git a/include/qlibc/extensions/qdatabase.h b/include/qlibc/extensions/qdatabase.h index 1101731d..13ad8c95 100644 --- a/include/qlibc/extensions/qdatabase.h +++ b/include/qlibc/extensions/qdatabase.h @@ -46,19 +46,45 @@ extern "C" { #endif +/* types */ +typedef struct qdbresult_s qdbresult_t; +typedef struct qdb_s qdb_t; + /* database header files should be included before this header file. */ #ifdef _mysql_h #define Q_ENABLE_MYSQL (1) +extern qdb_t *qdb_mysql(const char *dbtype, const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit); #endif /* _mysql_h */ -/* types */ -typedef struct qdbresult_s qdbresult_t; -typedef struct qdb_s qdb_t; +#ifdef LIBPQ_FE_H +#define Q_ENABLE_PGSQL (1) +extern qdb_t *qdb_pgsql(const char *dbtype, const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit); +#endif /* LIBPQ_FE_H */ /* public functions */ -extern qdb_t *qdb(const char *dbtype, - const char *addr, int port, const char *database, - const char *username, const char *password, bool autocommit); +static inline qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit) +{ + qdb_t *db = NULL; + +#ifdef Q_ENABLE_MYSQL + db = qdb_mysql(dbtype, addr, port, database, username, password, autocommit); + if (db) { + return db; + } +#endif /* Q_ENABLE_MYSQL */ + +#ifdef Q_ENABLE_PGSQL + db = qdb_pgsql(dbtype, addr, port, database, username, password, autocommit); + if (db) { + return db; + } +#endif /* Q_ENABLE_PGSQL */ + + return db; +} /** * qdbresult object structure @@ -86,6 +112,11 @@ struct qdbresult_s { int cols; int cursor; #endif + +#ifdef Q_ENABLE_PGSQL + /* private variables for pgsql database - do not access directly */ + void *pgsql; +#endif }; /* qdb object structure */ @@ -131,6 +162,11 @@ struct qdb_s { /* private variables for mysql database - do not access directly */ MYSQL *mysql; #endif + +#ifdef Q_ENABLE_PGSQL + /* private variables for pgsql database - do not access directly */ + void *pgsql; +#endif }; #ifdef __cplusplus diff --git a/src/Makefile.in b/src/Makefile.in index 2b22288e..3c01a77c 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -97,6 +97,7 @@ QLIBCEXT_OBJS = \ extensions/qlog.o \ extensions/qhttpclient.o \ extensions/qdatabase_mysql.o \ + extensions/qdatabase_pgsql.o \ extensions/qtokenbucket.o ## Which compiler & options for release diff --git a/src/doxygen.conf b/src/doxygen.conf index 4530d7c5..8e2ac5a2 100644 --- a/src/doxygen.conf +++ b/src/doxygen.conf @@ -771,6 +771,7 @@ INPUT = ../README.md \ extensions/qlog.c \ extensions/qhttpclient.c \ extensions/qdatabase_mysql.c \ + extensions/qdatabase_pgsql.c \ extensions/qtokenbucket.c # This tag can be used to specify the character encoding of the source files diff --git a/src/extensions/qdatabase_mysql.c b/src/extensions/qdatabase_mysql.c index 4734e479..468770be 100644 --- a/src/extensions/qdatabase_mysql.c +++ b/src/extensions/qdatabase_mysql.c @@ -150,7 +150,7 @@ static void result_free(qdbresult_t *result); * otherwise returns NULL. * * @code - * qdb_t *db = qdb("MYSQL", + * qdb_t *db = qdb_mysql("MYSQL", * "dbhost.qdecoder.org", 3306, "test", "secret", * "sampledb", true); * if (db == NULL) { @@ -159,8 +159,8 @@ static void result_free(qdbresult_t *result); * } * @endcode */ -qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, - const char *password, const char *database, bool autocommit) +qdb_t *qdb_mysql(const char *dbtype, const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit) { // check db type #ifdef Q_ENABLE_MYSQL diff --git a/src/extensions/qdatabase_pgsql.c b/src/extensions/qdatabase_pgsql.c new file mode 100644 index 00000000..30469093 --- /dev/null +++ b/src/extensions/qdatabase_pgsql.c @@ -0,0 +1,1031 @@ +/****************************************************************************** + * qLibc + * + * Copyright (c) 2024 SunBeau. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +/** + * @file qdatabase_pgsql.c Database wrapper. + * + * Database header files should be included prior to qlibcext.h in your source + * codes like below. + * + * @code + * #include "libpq-fe.h" + * #include "qlibcext.h" + * @endcode + * + * @code + * qdb_t *db = NULL; + * qdbresult_t *result = NULL; + * + * db = qdb("PGSQL", "127.0.0.1", 5432, + * "test", "secret", "sampledb", true); + * if (db == NULL) { + * printf("ERROR: Not supported database type.\n"); + * return -1; + * } + * + * // try to connect + * if (db->open(db) == false) { + * printf("WARNING: Can't connect to database.\n"); + * return -1; + * } + * + * // get results + * result = db->execute_query(db, "SELECT name, population FROM City"); + * if (result != NULL) { + * printf("COLS : %d , ROWS : %d\n", + * result->get_cols(result), result->get_rows(result)); + * while (result->get_next(result) == true) { + * char *pszName = result->get_str(result, "name"); + * int nPopulation = result->get_int(result, "population"); + * printf("Country : %s , Population : %d\n", pszName, nPopulation); + * } + * result->free(result); + * } + * + * // close connection + * db->close(db); + * + * // free db object + * db->free(db); + * @endcode + */ + +#ifndef DISABLE_QDATABASE + +#if defined(ENABLE_PGSQL) || defined( _DOXYGEN_SKIP) + +#define qtype_cast(_type, _src) ((_type)_src) + +#ifdef ENABLE_PGSQL +#include "libpq-fe.h" +/* pgsql specific connector options */ +#define Q_PGSQL_OPT_RECONNECT (1) +#define Q_PGSQL_OPT_CONNECT_TIMEOUT (10) +#define Q_PGSQL_OPT_READ_TIMEOUT (30) +#define Q_PGSQL_OPT_WRITE_TIMEOUT (30) +#endif + +#include +#include +#include +#include +#include + +#include "qinternal.h" +#include "extensions/qdatabase.h" + +/* + * Member method protos + */ +#ifndef _DOXYGEN_SKIP +// qdb_t object +static bool open_(qdb_t *db); +static bool close_(qdb_t *db); + +static int execute_update(qdb_t *db, const char *query); +static int execute_updatef(qdb_t *db, const char *format, ...); +static qdbresult_t *execute_query(qdb_t *db, const char *query); +static qdbresult_t *execute_queryf(qdb_t *db, const char *format, ...); + +static bool begin_tran(qdb_t *db); +static bool commit(qdb_t *db); +static bool rollback(qdb_t *db); + +static bool set_fetchtype(qdb_t *db, bool use); +static bool get_conn_status(qdb_t *db); +static bool ping(qdb_t *db); +static const char *get_error(qdb_t *db, unsigned int *errorno); +static void free_(qdb_t *db); + +// qdbresult_t object +static const char *_resultGetStr(qdbresult_t *result, const char *field); +static const char *_resultGetStrAt(qdbresult_t *result, int idx); +static int _resultGetInt(qdbresult_t *result, const char *field); +static int _resultGetIntAt(qdbresult_t *result, int idx); +static bool _resultGetNext(qdbresult_t *result); + +static int result_get_cols(qdbresult_t *result); +static int result_get_rows(qdbresult_t *result); +static int result_get_row(qdbresult_t *result); + +static void result_free(qdbresult_t *result); + +#endif + +/* + * private for pgsql database + */ +#ifndef _DOXYGEN_SKIP +typedef enum { + PQ_UNKNOWN, + PQ_WRITE, + PQ_READ, + PQ_PING, +} pgquery_t; + +typedef struct { + char *emsg; + PGconn *pgconn; + + PGresult *pgresult; + int rows; + int cols; + int cursor; +} pgsql_t; + +static inline pgsql_t* pgsql_init(void) +{ + pgsql_t* pgsql = (pgsql_t *)calloc(1, sizeof(pgsql_t)); + return pgsql; +} + +static inline const char* pgsql_set_emsg(pgsql_t* pgsql, const char* emsg) +{ + if (pgsql == NULL || emsg == NULL) { + return NULL; + } + + if (pgsql->emsg) { + free(pgsql->emsg); + } + + pgsql->emsg = strdup(emsg); + return pgsql->emsg; +} + +static inline void pgsql_close(pgsql_t* pgsql) +{ + if (pgsql == NULL) { + return; + } + + if (pgsql->emsg) { + free(pgsql->emsg); + pgsql->emsg = NULL; + } + + if (pgsql->pgresult) { + PQclear(pgsql->pgresult); + pgsql->pgresult = NULL; + } + + if (pgsql->pgconn) { + PQfinish(pgsql->pgconn); + pgsql->pgconn = NULL; + } + + free(pgsql); +} + +static inline void pgsql_query_result(pgsql_t* pgsql) +{ + if (pgsql->pgresult) { + PQclear(pgsql->pgresult); + pgsql->pgresult = NULL; + } +} + +static inline int pgsql_query(pgsql_t* pgsql, const char *query, pgquery_t type) +{ + if (pgsql == NULL || pgsql->pgconn == NULL) { + return -1; /* error */ + } + + /* free previous query results */ + pgsql_query_result(pgsql); + + if (type == PQ_PING) { + query = ""; + } + + PGresult *result = PQexec(pgsql->pgconn, query); + ExecStatusType status = PQresultStatus(result); + + switch (type) { + case PQ_PING: { + if (status != PGRES_EMPTY_QUERY) { + return -1; /* error */ + } + break; + } + + case PQ_READ: { + if (status != PGRES_TUPLES_OK) { + return -1; /* error */ + } + break; + } + + default: { + if (status != PGRES_COMMAND_OK) { + return -1; /* error */ + } + break; + } + } + + pgsql->pgresult = result; + + return 0; /* ok */ +} + +static inline int pgsql_affected_rows(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return atoi(PQcmdTuples(pgsql->pgresult)); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_rows(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return PQntuples(pgsql->pgresult); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_fields(pgsql_t* pgsql) +{ + if (pgsql && pgsql->pgresult) { + return PQnfields(pgsql->pgresult); + } + + /* error */ + return -1; +} + +#endif /* _DOXYGEN_SKIP */ + +/** + * Initialize internal connector structure + * + * @param dbtype database server type. currently "PGSQL" is only supported + * @param addr ip or fqdn address. + * @param port port number + * @param username database username + * @param password database password + * @param database database server type. currently "PGSQL" is only supported + * @param autocommit can only be set to true, when dbtype is "PGSQL" + * + * @return a pointer of qdb_t object in case of successful, + * otherwise returns NULL. + * + * @code + * qdb_t *db = qdb_pgsql("PGSQL", "127.0.0.1", 5432, + * "test", "secret", "sampledb", true); + * if (db == NULL) { + * printf("ERROR: Not supported database type.\n"); + * return -1; + * } + * @endcode + */ +qdb_t *qdb_pgsql(const char *dbtype, const char *addr, int port, const char *database, + const char *username, const char *password, bool autocommit) +{ + // check db type +#ifdef Q_ENABLE_PGSQL + if (strcmp(dbtype, "PGSQL")) return NULL; +#else + return NULL; +#endif + if (dbtype == NULL + || addr == NULL + || username == NULL + || password == NULL + || database == NULL + || autocommit == false) { + return NULL; + } + + // initialize + qdb_t *db; + if ((db = (qdb_t *)malloc(sizeof(qdb_t))) == NULL) return NULL; + memset((void *)db, 0, sizeof(qdb_t)); + db->connected = false; + + // set common structure + db->info.dbtype = strdup(dbtype); + db->info.addr = strdup(addr); + db->info.port = port; + db->info.username = strdup(username); + db->info.password = strdup(password); + db->info.database = strdup(database); + db->info.autocommit = autocommit; + db->info.fetchtype = false;// store mode + + // set db handler +#ifdef Q_ENABLE_PGSQL + db->pgsql = NULL; +#endif + + // assign methods + db->open = open_; + db->close = close_; + + db->execute_update = execute_update; + db->execute_updatef = execute_updatef; + db->execute_query = execute_query; + db->execute_queryf = execute_queryf; + + db->begin_tran = begin_tran; + db->commit = commit; + db->rollback = rollback; + + db->set_fetchtype = set_fetchtype; + db->get_conn_status = get_conn_status; + db->ping = ping; + db->get_error = get_error; + db->free = free_; + + // initialize recrusive mutex + Q_MUTEX_NEW(db->qmutex, true); + + return db; +} + +/** + * qdb->open(): Connect to database server + * + * @param db a pointer of qdb_t object + * + * @return true if successful, otherwise returns false. + */ +static bool open_(qdb_t *db) +{ + if (db == NULL) return false; + + // if connected, close first + if (db->connected == true) { + close_(db); + } + +#ifdef Q_ENABLE_PGSQL + Q_MUTEX_ENTER(db->qmutex); + + bool ret = true; + PGconn* pgconn = NULL; + + // initialize handler + if (db->pgsql != NULL) close_(db); + + if ((db->pgsql = pgsql_init()) == NULL) { + ret = false; + goto gt_quit; + } + + // set options + /* do nothing */ + + /* int to char* */ + char pgport[10]; + snprintf(pgport, sizeof(pgport), "%d", db->info.port); + + // try to connect + pgconn = PQsetdbLogin(db->info.addr, pgport, + NULL, NULL, + db->info.database, db->info.username, db->info.password); + if (pgconn == NULL) { + close_(db); // free pgsql handler + ret = false; + goto gt_quit; + } + + if (PQstatus(pgconn) != CONNECTION_OK) { + const char* emsg = PQerrorMessage(pgconn); + pgsql_set_emsg(db->pgsql, emsg); + // fprintf(stderr, "%s", emsg); + ret = false; + goto gt_quit; + } + + qtype_cast(pgsql_t*, db->pgsql)->pgconn = pgconn; + + // set auto-commit + /* do nothing */ + + // set flag + db->connected = true; + +gt_quit: + + if (ret == false) { + PQfinish(pgconn); + } + + Q_MUTEX_LEAVE(db->qmutex); + return ret; + +#else + return false; +#endif +} + +/** + * qdb->close(): Disconnect from database server + * + * @param db a pointer of qdb_t object + * + * @return true if successful, otherwise returns false. + * + * @note + * Unless you call qdb->free(), qdb_t object will keep the database + * information. So you can re-connect to database using qdb->open(). + */ +static bool close_(qdb_t *db) +{ + if (db == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + Q_MUTEX_ENTER(db->qmutex); + + if (db->pgsql != NULL) { + pgsql_close(db->pgsql); + db->pgsql = NULL; + } + db->connected = false; + + Q_MUTEX_LEAVE(db->qmutex); + return true; +#else + return false; +#endif +} + +/** + * qdb->execute_update(): Executes the update DML + * + * @param db a pointer of qdb_t object + * @param query query string + * + * @return a number of affected rows + */ +static int execute_update(qdb_t *db, const char *query) +{ + if (db == NULL || db->connected == false) return -1; + +#ifdef Q_ENABLE_PGSQL + Q_MUTEX_ENTER(db->qmutex); + + int affected = -1; + + // query + DEBUG("%s", query); + if (pgsql_query(db->pgsql, query, PQ_WRITE) == 0) { + /* get affected rows */ + if ((affected = pgsql_affected_rows(db->pgsql)) < 0) affected = -1; + } + + Q_MUTEX_LEAVE(db->qmutex); + return affected; +#else + return -1; +#endif +} + +/** + * qdb->execute_updatef(): Executes the formatted update DML + * + * @param db a pointer of qdb_t object + * @param format query string format + * + * @return a number of affected rows, otherwise returns -1 + */ +static int execute_updatef(qdb_t *db, const char *format, ...) +{ + char *query; + DYNAMIC_VSPRINTF(query, format); + if (query == NULL) return -1; + + int affected = execute_update(db, query); + free(query); + + return affected; +} + +/** + * qdb->execute_query(): Executes the query + * + * @param db a pointer of qdb_t object + * @param query query string + * + * @return a pointer of qdbresult_t if successful, otherwise returns NULL + */ +static qdbresult_t *execute_query(qdb_t *db, const char *query) +{ + if (db == NULL || db->connected == false) return NULL; + +#ifdef Q_ENABLE_PGSQL + // query + DEBUG("%s", query); + if (pgsql_query(db->pgsql, query, PQ_READ)) return NULL; + + // store + qdbresult_t *result = (qdbresult_t *)malloc(sizeof(qdbresult_t)); + if (result == NULL) return NULL; + result->pgsql = db->pgsql; + + /* get meta data */ + // result->row = NULL; + qtype_cast(pgsql_t*, result->pgsql)->rows = pgsql_num_rows(result->pgsql); + qtype_cast(pgsql_t*, result->pgsql)->cols = pgsql_num_fields(result->pgsql); + qtype_cast(pgsql_t*, result->pgsql)->cursor = 0; + + /* assign methods */ + result->getstr = _resultGetStr; + result->get_str_at = _resultGetStrAt; + result->getint = _resultGetInt; + result->get_int_at = _resultGetIntAt; + result->getnext = _resultGetNext; + + result->get_cols = result_get_cols; + result->get_rows = result_get_rows; + result->get_row = result_get_row; + + result->free = result_free; + + return result; +#else + return NULL; +#endif +} + +/** + * qdb->execute_queryf(): Executes the formatted query + * + * @param db a pointer of qdb_t object + * @param format query string format + * + * @return a pointer of qdbresult_t if successful, otherwise returns NULL + */ +static qdbresult_t *execute_queryf(qdb_t *db, const char *format, ...) +{ + char *query; + DYNAMIC_VSPRINTF(query, format); + if (query == NULL) return NULL; + + qdbresult_t *ret = db->execute_query(db, query); + free(query); + return ret; +} + +/** + * qdb->begin_tran(): Start transaction + * + * @param db a pointer of qdb_t object + * + * @return true if successful, otherwise returns false + * + * @code + * db->begin_tran(db); + * (... insert/update/delete ...) + * db->commit(db); + * @endcode + */ +static bool begin_tran(qdb_t *db) +{ + if (db == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + Q_MUTEX_ENTER(db->qmutex); + if (qtype_cast(qmutex_t*, db->qmutex)->count != 1) { + Q_MUTEX_LEAVE(db->qmutex); + return false; + } + + if (pgsql_query(db->pgsql, "START TRANSACTION", PQ_WRITE) < 0) { + Q_MUTEX_LEAVE(db->qmutex); + return false; + } + return true; +#else + return false; +#endif +} + +/** + * qdb->commit(): Commit transaction + * + * @param db a pointer of qdb_t object + * + * @return true if successful, otherwise returns false + */ +static bool commit(qdb_t *db) +{ + if (db == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + bool ret = false; + if (pgsql_query(db->pgsql, "COMMIT", PQ_WRITE) == 0) { + ret = true; + } + + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { + Q_MUTEX_LEAVE(db->qmutex); + } + return ret; +#else + return false; +#endif +} + +/** + * qdb->rellback(): Roll-back and abort transaction + * + * @param db a pointer of qdb_t object + * + * @return true if successful, otherwise returns false + */ +static bool rollback(qdb_t *db) +{ + if (db == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + bool ret = false; + if (pgsql_query(db->pgsql, "ROLLBACK", PQ_WRITE) == 0) { + ret = true; + } + + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { + Q_MUTEX_LEAVE(db->qmutex); + } + return ret; +#else + return 0; +#endif +} + +/** + * qdb->set_fetchtype(): Set result fetching type + * + * @param db a pointer of qdb_t object + * @param fromdb false for storing the results to client (default mode), + * true for fetching directly from server, + * + * @return true if successful otherwise returns false + * + * @note + * If qdb->set_fetchtype(db, true) is called, the results does not + * actually read into the client. Instead, each row must be retrieved + * individually by making calls to qdbresult->get_next(). + * This reads the result of a query directly from the server without storing + * it in local buffer, which is somewhat faster and uses much less memory than + * default behavior qdb->set_fetchtype(db, false). + */ +static bool set_fetchtype(qdb_t *db, bool fromdb) +{ + if (db == NULL || db->pgsql == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + pgsql_set_emsg(db->pgsql, "unsupported operation"); +#endif + + return false; +} + +/** + * qdb->get_conn_status(): Get last connection status + * + * @param db a pointer of qdb_t object + * + * @return true if the connection flag is set to alive, otherwise returns false + * + * @note + * This function just returns the the connection status flag. + */ +static bool get_conn_status(qdb_t *db) +{ + if (db == NULL) return false; + + return db->connected; +} + +/** + * qdb->ping(): Checks whether the connection to the server is working. + * + * @param db a pointer of qdb_t object + * + * @return true if connection is alive, false if connection is not available + * and failed to reconnect + * + * @note + * If the connection has gone down, an attempt to reconnect. + */ +static bool ping(qdb_t *db) +{ + if (db == NULL || db->pgsql == NULL) return false; + +#ifdef Q_ENABLE_PGSQL + if (db->connected == true && pgsql_query(db->pgsql, NULL, PQ_PING) == 0) { + return true; + } else { // ping test failed + if (open_(db) == true) { // try re-connect + DEBUG("Connection recovered."); + return true; + } + } + + return false; +#else + return false; +#endif +} + +/** + * qdb->get_error(): Get error number and message + * + * @param db a pointer of qdb_t object + * @param errorno if not NULL, error number will be stored + * + * @return a pointer of error message string + * + * @note + * Do not free returned error message + */ +static const char *get_error(qdb_t *db, unsigned int *errorno) +{ + if (db == NULL || db->connected == false || db->pgsql == NULL ) + return "(no opened db)"; + + unsigned int eno; + const char *emsg; + +#ifdef Q_ENABLE_PGSQL + emsg = qtype_cast(pgsql_t*, db->pgsql)->emsg; + if (emsg == NULL) { + eno = 0; + emsg = "(no error)"; + } else { + eno = 1; + } +#else + eno = 1; + emsg = "(not implemented)"; +#endif + + if (errorno != NULL) *errorno = eno; + return emsg; +} + +/** + * qdb->free(): De-allocate qdb_t structure + * + * @param db a pointer of qdb_t object + */ +static void free_(qdb_t *db) +{ + if (db == NULL) return; + + Q_MUTEX_ENTER(db->qmutex); + + close_(db); + + free(db->info.dbtype); + free(db->info.addr); + free(db->info.username); + free(db->info.password); + free(db->info.database); + free(db); + + Q_MUTEX_LEAVE(db->qmutex); + Q_MUTEX_DESTROY(db->qmutex); + + return; +} + +/** + * qdbresult->get_str(): Get the result as string by field name + * + * @param result a pointer of qdbresult_t + * @param field column name + * + * @return a string pointer if successful, otherwise returns NULL. + * + * @note + * Do not free returned string. + */ +static const char *_resultGetStr(qdbresult_t *result, const char *field) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->cols <= 0) { + return NULL; + } + + const char *val = NULL; + int rows = qtype_cast(pgsql_t*, result->pgsql)->rows; + int cols = qtype_cast(pgsql_t*, result->pgsql)->cols; + int cur = qtype_cast(pgsql_t*, result->pgsql)->cursor; + + /* get row num */ + int row = -1; + for (int i = 0; i < rows; i++) { + val = PQfname(qtype_cast(pgsql_t*, result->pgsql)->pgresult, i); + if (!strcasecmp(val, field)) { + row = i; + break; + } + } + + if (row == -1) { + return NULL; + } + + val = PQgetvalue(qtype_cast(pgsql_t*, result->pgsql)->pgresult, cur, row); + return val; +#else + return NULL; +#endif +} + +/** + * qdbresult->get_str_at(): Get the result as string by column number + * + * @param result a pointer of qdbresult_t + * @param idx column number (first column is 1) + * + * @return a string pointer if successful, otherwise returns NULL. + */ +static const char *_resultGetStrAt(qdbresult_t *result, int idx) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->rows == 0 + || qtype_cast(pgsql_t*, result->pgsql)->cols == 0 + || idx <= 0 + || idx > qtype_cast(pgsql_t*, result->pgsql)->cols) { + return NULL; + } + + int cur = qtype_cast(pgsql_t*, result->pgsql)->cursor; + const char *val = PQgetvalue(qtype_cast(pgsql_t*, result->pgsql)->pgresult, cur, idx - 1); + return val; +#else + return NULL; +#endif +} + +/** + * qdbresult->get_int(): Get the result as integer by field name + * + * @param result a pointer of qdbresult_t + * @param field column name + * + * @return a integer converted value + */ +static int _resultGetInt(qdbresult_t *result, const char *field) +{ + const char *val = result->getstr(result, field); + if (val == NULL) return 0; + return atoi(val); +} + +/** + * qdbresult->get_int_at(): Get the result as integer by column number + * + * @param result a pointer of qdbresult_t + * @param idx column number (first column is 1) + * + * @return a integer converted value + */ +static int _resultGetIntAt(qdbresult_t *result, int idx) +{ + const char *val = result->get_str_at(result, idx); + if (val == NULL) return 0; + return atoi(val); +} + +/** + * qdbresult->get_next(): Retrieves the next row of a result set + * + * @param result a pointer of qdbresult_t + * + * @return true if successful, false if no more rows are left + */ +static bool _resultGetNext(qdbresult_t *result) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL + || result->pgsql == NULL + || qtype_cast(pgsql_t*, result->pgsql)->pgresult == NULL) { + return false; + } + + int cursor = qtype_cast(pgsql_t*, result->pgsql)->cursor; + if (++cursor == qtype_cast(pgsql_t*, result->pgsql)->rows) { + return false; + } + + qtype_cast(pgsql_t*, result->pgsql)->cursor = cursor; + return true; +#else + return false; +#endif +} + +/** + * qdbresult->get_cols(): Get the number of columns in the result set + * + * @param result a pointer of qdbresult_t + * + * @return the number of columns in the result set + */ +static int result_get_cols(qdbresult_t *result) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->cols; +#else + return 0; +#endif +} + +/** + * qdbresult->get_rows(): Get the number of rows in the result set + * + * @param result a pointer of qdbresult_t + * + * @return the number of rows in the result set + */ +static int result_get_rows(qdbresult_t *result) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->rows; +#else + return 0; +#endif +} + +/** + * qdbresult->get_row(): Get the current row number + * + * @param result a pointer of qdbresult_t + * + * @return current fetching row number of the result set + * + * @note + * This number is sequencial counter which is started from 1. + */ +static int result_get_row(qdbresult_t *result) +{ +#ifdef Q_ENABLE_PGSQL + if (result == NULL || result->pgsql == NULL) return 0; + return qtype_cast(pgsql_t*, result->pgsql)->cursor; +#else + return 0; +#endif +} + +/** + * qdbresult->free(): De-allocate the result + * + * @param result a pointer of qdbresult_t + */ +static void result_free(qdbresult_t *result) +{ +#ifdef Q_ENABLE_PGSQL + if (result) { + result->pgsql = NULL; + free(result); + } + return; +#else + return; +#endif +} + +#endif + +#endif /* DISABLE_QDATABASE */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d7721b58..459c24df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,10 @@ SET(test_list test_qhash ) +IF(NOT "${WITH_PGSQL}" STREQUAL "") + LIST(APPEND test_list test_qdatabase_pgsql) +ENDIF() + SET(test_file_list test_qhash_data_1.bin test_qhash_data_2.bin diff --git a/tests/Makefile.in b/tests/Makefile.in index f5fd7ece..3cd2730b 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -52,13 +52,18 @@ TARGETS = \ test_qstack \ test_qhash +TARGETS_EXT = \ + test_qdatabase_pgsql + LIBQLIBC = ${QLIBC_LIBDIR}/libqlibc.a ${DEPLIBS} LIBQLIBCEXT = ${QLIBC_LIBDIR}/libqlibcext.a ## Main all: ${TARGETS} +ext: ${TARGETS_EXT} + +run: test test-ext -run: test test: all @./launcher.sh ${TARGETS} @@ -92,10 +97,20 @@ test_qvector: test_qvector.o test_qhash: test_qhash.o ${CC} ${CFLAGS} ${CPPFLAGS} -g -o $@ test_qhash.o ${LIBQLIBC} +## Test extensions +test-ext: ext + @./launcher.sh ${TARGETS_EXT} + +test_qdatabase_pgsql: test_qdatabase_pgsql.o + ${CC} -o $@ test_qdatabase_pgsql.o ${LIBQLIBCEXT} ${LIBQLIBC} ${CFLAGS} ${CPPFLAGS} + ## Clear Module clean: ${RM} -f *.o ${TARGETS} +clean-ext: + ${RM} -f *.o ${TARGETS_EXT} + ## Compile Module .c.o: ${CC} ${CFLAGS} ${CPPFLAGS} -c -o $@ $< diff --git a/tests/test_qdatabase_pgsql.c b/tests/test_qdatabase_pgsql.c new file mode 100644 index 00000000..16b7d62c --- /dev/null +++ b/tests/test_qdatabase_pgsql.c @@ -0,0 +1,327 @@ +/****************************************************************************** + * qLibc + * + * Copyright (c) 2024 SunBeau. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ +/* This code is written and updated by following people and released under + * the same license as above qLibc license. + * Copyright (c) 2015 Zhenjiang Xie - https://github.com/Charles0429 + *****************************************************************************/ + +#include "libpq-fe.h" + +#include "qunit.h" +#include "qlibc.h" +#include "qlibcext.h" + +const char *default_dbtype = "PGSQL"; +const char *default_addr = "127.0.0.1"; +const int default_port = 5432; +const char *default_database = "postgres"; +const char *default_username = "postgres"; +const char *default_password = "123456"; +const bool default_autocommit = true; + +QUNIT_START("Test qdatabase_pgsql.c"); + +qdb_t* test_connect_database(const char *database) { + if (database == NULL) { + database = default_database; + } + + qdb_t *db = qdb( + default_dbtype, + default_addr, + default_port, + database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(db); + ASSERT_FALSE(db->connected); + ASSERT_FALSE(db->get_conn_status(db)); + + ASSERT_TRUE(db->open(db)); + ASSERT_TRUE(db->connected); + ASSERT_TRUE(db->get_conn_status(db)); + + return db; +} + +qdb_t* test_connect_testdb(qdb_t* database) { + qdb_t *testdb = NULL; + char query[128]; + const char *test_database = "qdb_testdb"; + + /* test execute_update() */ + snprintf(query, sizeof(query), "DROP DATABASE IF EXISTS %s;", test_database); + ASSERT_EQUAL_INT(database->execute_update(database, query), 0); + + /* create new database */ + snprintf(query, sizeof(query), "CREATE DATABASE %s;", test_database); + ASSERT_EQUAL_INT(database->execute_update(database, query), 0); + + /* connect "qdb_testdb" database */ + testdb = qdb( + default_dbtype, + default_addr, + default_port, + test_database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(testdb); + ASSERT_FALSE(testdb->connected); + + ASSERT_TRUE(testdb->open(testdb)); + ASSERT_TRUE(testdb->connected); + + return testdb; +} + +/* + CREATE TABLE animals ( + animal_id INT PRIMARY KEY, + animal_name VARCHAR(255), + species VARCHAR(255), + gender VARCHAR(255), + age INT, + weight INT, + description VARCHAR(255) + ); + + INSERT INTO animals (animal_id, animal_name, species, gender, age, weight, description) + VALUES + (1, 'Dog', 'Canis lupus familiaris', 'Male', 2, 15, 'A friendly dog named Max'), + (2, 'Cat', 'Felis catus', 'Female', 5, 8, 'A lazy cat named Lucy'), + (3, 'Rabbit', 'Oryctolagus cuniculus', 'Male', 1, 2, 'A cute little rabbit named Bugs'), + (4, 'Hamster', 'Mesocricetus auratus', 'Female', 2, 50, 'A hyperactive hamster named Sparky'), + (5, 'Fish', 'Poecilia reticulata', 'Female', 1, 0.1, 'A peaceful betta fish named Blue'); +*/ +void test_create_table_animals(qdb_t* database) { + int rows; + + rows = database->execute_update(database, + "CREATE TABLE animals (" + "animal_id INT PRIMARY KEY," + "animal_name VARCHAR(255)," + "species VARCHAR(255)," + "gender VARCHAR(255)," + "age INT," + "weight INT," + "description VARCHAR(255)" + ");" + ); + ASSERT_EQUAL_INT(rows, 0); + + rows = database->execute_updatef(database, + "INSERT INTO animals VALUES" + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s)," + "(%s, %s, %s, %s, %s, %s, %s);", + "1", "'Dog'", "'Canis lupus familiaris'", "'Male'", "2", "15", "'A friendly dog named Max'", + "2", "'Cat'", "'Felis catus'", "'Female'", "5", "8", "'A lazy cat named Lucy'", + "3", "'Rabbit'", "'Oryctolagus cuniculus'", "'Male'", "1", "2", "'A cute little rabbit named Bugs'", + "4", "'Hamster'", "'Mesocricetus auratus'", "'Female'", "2", "50", "'A hyperactive hamster named Sparky'", + "5", "'Fish'", "'Poecilia reticulata'", "'Female'", "1", "0.1", "'A peaceful betta fish named Blue'"); + ASSERT_EQUAL_INT(rows, 5); +} + +TEST("Test1: qdb_pgsql()/qdb()/open()/close()/free()") { + /* test qdb_pgsql() */ + qdb_t *db = qdb_pgsql( + default_dbtype, + default_addr, + default_port, + default_database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(db); + ASSERT_FALSE(db->connected); + + /* test open() */ + ASSERT_TRUE(db->open(db)); + ASSERT_TRUE(db->connected); + + /* test close() */ + ASSERT_TRUE(db->close(db)); + ASSERT_FALSE(db->connected); + + /* test free() */ + db->free(db); + + /* test qdb() */ + db = qdb( + default_dbtype, + default_addr, + default_port, + default_database, + default_username, + default_password, + default_autocommit + ); + ASSERT_NOT_NULL(db); + ASSERT_FALSE(db->connected); + ASSERT_TRUE(db->open(db)); + ASSERT_TRUE(db->connected); + ASSERT_TRUE(db->close(db)); + ASSERT_FALSE(db->connected); + db->free(db); +} + +TEST("Test2: execute_updatef()") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + + /* test: execute_updatef */ + test_create_table_animals(test_db); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test3: execute_queryf()") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + int rows; + + /* test: execute_queryf */ + qdbresult_t *qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 5); + ASSERT_EQUAL_INT(qrst->get_cols(qrst), 7); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 0); + ASSERT_EQUAL_STR(qrst->getstr(qrst, "animal_name"), "Dog"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Canis lupus familiaris"); + ASSERT_TRUE(qrst->getnext(qrst)); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 1); + ASSERT_TRUE(qrst->getnext(qrst)); + ASSERT_EQUAL_INT(qrst->get_row(qrst), 2); + + ASSERT_EQUAL_INT(qrst->getint(qrst, "animal_id"), 3); + ASSERT_EQUAL_INT(qrst->get_int_at(qrst, 5), 1); + qrst->free(qrst); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test4: transaction") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + int rows; + qdbresult_t *qrst; + + /* test: rollback */ + ASSERT_TRUE(test_db->begin_tran(test_db)); + rows = test_db->execute_update(test_db, "DELETE FROM animals WHERE animal_id > 3;"); + ASSERT_EQUAL_INT(rows, 2); + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 3); + qrst->free(qrst); + ASSERT_TRUE(test_db->rollback(test_db)); /* rollback */ + + /* test: commit */ + ASSERT_TRUE(test_db->begin_tran(test_db)); + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 5); + rows = test_db->execute_update(test_db, "DELETE FROM animals WHERE animal_id > 3;"); + ASSERT_EQUAL_INT(rows, 2); + ASSERT_TRUE(test_db->commit(test_db)); /* commit */ + + qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 3); + qrst->free(qrst); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +TEST("Test5: autocommit") { + qdb_t *db = qdb( + default_dbtype, + default_addr, + default_port, + default_database, + default_username, + default_password, + false + ); + + /* Autocommit set to false is not supported in pgsql */ + ASSERT_NULL(db); +} + +TEST("Test6: other") { + qdb_t *default_db = test_connect_database(default_database); + ASSERT_TRUE(default_db->connected); + qdb_t *test_db = test_connect_testdb(default_db); + test_create_table_animals(test_db); + int rows; + qdbresult_t *qrst; + + /* test:get_error */ + ASSERT_EQUAL_STR(test_db->get_error(test_db, NULL), "(no error)"); + ASSERT_FALSE(test_db->set_fetchtype(test_db, true)); + ASSERT_EQUAL_STR(test_db->get_error(test_db, NULL), "unsupported operation"); + + /* test: ping */ + ASSERT_TRUE(test_db->ping(test_db)); + + /* disconnect */ + ASSERT_TRUE(test_db->close(test_db)); + ASSERT_TRUE(default_db->close(default_db)); + + /* free */ + test_db->free(test_db); + default_db->free(default_db); +} + +QUNIT_END(); \ No newline at end of file From cdd0c094fb0fc7b7abb17ab49c69b0bfaad0e415 Mon Sep 17 00:00:00 2001 From: SunBeau Date: Tue, 30 Apr 2024 09:36:12 +0800 Subject: [PATCH 3/3] update configure by autoconf --- .github/workflows/ci-run-test-extensions.yml | 2 +- configure | 103 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-run-test-extensions.yml b/.github/workflows/ci-run-test-extensions.yml index 8956692f..c01bd667 100644 --- a/.github/workflows/ci-run-test-extensions.yml +++ b/.github/workflows/ci-run-test-extensions.yml @@ -56,7 +56,7 @@ jobs: - name: Configure And make working-directory: ${{ github.workspace }} run: | - autoconf + # autoconf ./configure --with-pgsql \ --with-pgsql-incdir=/usr/include/postgresql \ --with-pgsql-libdir=/usr/lib/x86_64-linux-gnu diff --git a/configure b/configure index 64ab9529..49a1e077 100755 --- a/configure +++ b/configure @@ -732,6 +732,9 @@ enable_ext_qhttpclient enable_ext_qdatabase with_openssl with_mysql +with_pgsql +with_pgsql_incdir +with_pgsql_libdir ' ac_precious_vars='build_alias host_alias @@ -1372,6 +1375,11 @@ Optional Packages: extension API. When it's enabled, user applications need to link mysql client library. (ex: -lmysqlclient) + --with-pgsql This will enable PostgreSQL database support in + qdatabase extension API. When it's enabled, user + applications need to link libpq library. (ex: -lpq) + --with-pgsql-incdir=DIR site header files for PostgreSQL in DIR. + --with-pgsql-libdir=DIR site library files for PostgreSQL in DIR Some influential environment variables: CC C compiler command @@ -4843,6 +4851,101 @@ See \`config.log' for more details" "$LINENO" 5; } fi fi + +# Check whether --with-pgsql was given. +if test "${with_pgsql+set}" = set; then : + withval=$with_pgsql; +else + withval=no +fi + +if test "$withval" = yes; then + # check libpq-fe.h + as_ac_File=`$as_echo "ac_cv_file_$with_pgsql_incdir/libpq-fe.h" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $with_pgsql_incdir/libpq-fe.h" >&5 +$as_echo_n "checking for $with_pgsql_incdir/libpq-fe.h... " >&6; } +if eval \${$as_ac_File+:} false; then : + $as_echo_n "(cached) " >&6 +else + test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r "$with_pgsql_incdir/libpq-fe.h"; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi +fi +eval ac_res=\$$as_ac_File + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes"; then : + withval=yes +else + withval=no +fi + + if test "$withval" = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: find '$with_pgsql_incdir/libpq-fe.h'" >&5 +$as_echo "$as_me: find '$with_pgsql_incdir/libpq-fe.h'" >&6;} + CPPFLAGS="$CPPFLAGS -DENABLE_PGSQL -I$with_pgsql_incdir" + else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "Cannot find 'libpq-fe.h' header. Use --with-pgsql-incdir=DIR to specify the directory where 'libpq-fe.h' is located. +See \`config.log' for more details" "$LINENO" 5; } + fi + # check libpq.so + as_ac_File=`$as_echo "ac_cv_file_$with_pgsql_libdir/libpq.so" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $with_pgsql_libdir/libpq.so" >&5 +$as_echo_n "checking for $with_pgsql_libdir/libpq.so... " >&6; } +if eval \${$as_ac_File+:} false; then : + $as_echo_n "(cached) " >&6 +else + test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r "$with_pgsql_libdir/libpq.so"; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi +fi +eval ac_res=\$$as_ac_File + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes"; then : + withval=yes +else + withval=no +fi + + if test "$withval" = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: find '$with_pgsql_libdir/libpq.so'" >&5 +$as_echo "$as_me: find '$with_pgsql_libdir/libpq.so'" >&6;} + CPPFLAGS="$CPPFLAGS -Wl,-rpath,$with_pgsql_libdir -L$with_pgsql_libdir -lpq" + else + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "Cannot find 'libpq.so' library. Use --with-pgsql-libdir=DIR to specify the directory where 'libpq.so' is located. +See \`config.log' for more details" "$LINENO" 5; } + fi +fi + +# Check whether --with-pgsql_incdir was given. +if test "${with_pgsql_incdir+set}" = set; then : + withval=$with_pgsql_incdir; +else + withval=no +fi + + +# Check whether --with-pgsql_libdir was given. +if test "${with_pgsql_libdir+set}" = set; then : + withval=$with_pgsql_libdir; +else + withval=no +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: CFLAGS $CFLAGS" >&5 $as_echo "$as_me: CFLAGS $CFLAGS" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: CPPFLAGS $CPPFLAGS" >&5