diff --git a/.github/workflows/ci-run-test-extensions.yml b/.github/workflows/ci-run-test-extensions.yml new file mode 100644 index 00000000..c01bd667 --- /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 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 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..92e2a5c4 100644 --- a/include/qlibc/extensions/qdatabase.h +++ b/include/qlibc/extensions/qdatabase.h @@ -51,6 +51,14 @@ extern "C" { #define Q_ENABLE_MYSQL (1) #endif /* _mysql_h */ +#ifdef LIBPQ_FE_H +#define Q_ENABLE_PGSQL (1) +#endif /* LIBPQ_FE_H */ + +#if defined(Q_ENABLE_MYSQL) && defined(Q_ENABLE_PGSQL) +#error "only can enable one in the same time" +#endif + /* types */ typedef struct qdbresult_s qdbresult_t; typedef struct qdb_s qdb_t; @@ -65,11 +73,11 @@ extern qdb_t *qdb(const char *dbtype, */ struct qdbresult_s { /* encapsulated member functions */ - const char *(*getstr) (qdbresult_t *result, const char *field); + const char *(*get_str) (qdbresult_t *result, const char *field); const char *(*get_str_at) (qdbresult_t *result, int idx); - int (*getint) (qdbresult_t *result, const char *field); + int (*get_int) (qdbresult_t *result, const char *field); int (*get_int_at) (qdbresult_t *result, int idx); - bool (*getnext) (qdbresult_t *result); + bool (*get_next) (qdbresult_t *result); int (*get_cols) (qdbresult_t *result); int (*get_rows) (qdbresult_t *result); @@ -77,14 +85,21 @@ struct qdbresult_s { void (*free) (qdbresult_t *result); + bool fetchtype; + int cursor; + int cols; + #ifdef Q_ENABLE_MYSQL /* private variables for mysql database - do not access directly */ - bool fetchtype; MYSQL_RES *rs; MYSQL_FIELD *fields; MYSQL_ROW row; - int cols; - int cursor; +#endif + +#ifdef Q_ENABLE_PGSQL + /* private variables for pgsql database - do not access directly */ + void *rs; + const void *row; #endif }; @@ -131,6 +146,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/extensions/qdatabase.c b/src/extensions/qdatabase.c index ad238307..a8c33faf 100644 --- a/src/extensions/qdatabase.c +++ b/src/extensions/qdatabase.c @@ -77,7 +77,17 @@ #ifndef DISABLE_QDATABASE -#if defined(ENABLE_MYSQL) || defined( _DOXYGEN_SKIP) +#if defined(ENABLE_MYSQL) || defined(ENABLE_PGSQL) ||defined( _DOXYGEN_SKIP) + +#include +#include +#include +#include +#include +#include +#include + +#define qtype_cast(_type, _src) ((_type)_src) #ifdef ENABLE_MYSQL #include "mysql.h" @@ -86,13 +96,245 @@ #define Q_MYSQL_OPT_CONNECT_TIMEOUT (10) #define Q_MYSQL_OPT_READ_TIMEOUT (30) #define Q_MYSQL_OPT_WRITE_TIMEOUT (30) -#endif +#endif /* ENABLE_MYSQL */ + +#ifdef ENABLE_PGSQL +#include "libpq-fe.h" + +/* +Wrap the low-level functions of the libpq library. +Provide interfaces with naming convention similar to mysql_xyz(). +*/ + +typedef enum { + PQ_UNKNOWN, + PQ_PING, + PQ_QUERY, + PQ_UPDATE, + PQ_TRANSACTION, +} pgquery_t; + +typedef struct { + PGresult *pgresult_ref; + int idx; +} qpgrow_t; + +typedef struct { + PGconn *pgconn_ref; + PGresult *pgresult; + int cursor; + qpgrow_t qpgrow; +} qpgresult_t; + +typedef struct { + bool fetchtype; + PGconn *pgconn; + qpgresult_t qpgresult; +} qpgsql_t; + +static inline char* pgsql_row_get_str_at(const qpgrow_t *qpgrow, int field_idx) +{ + if (qpgrow == NULL || + qpgrow->idx < 0 || + qpgrow->pgresult_ref == NULL) { + return NULL; + } + + return PQgetvalue(qpgrow->pgresult_ref, qpgrow->idx, field_idx); +} + +static inline void pgsql_free_result(qpgresult_t* qpgresult) +{ + if (qpgresult == NULL) { + return; + } + + if (qpgresult->pgresult) { + PQclear(qpgresult->pgresult); + qpgresult->pgresult = NULL; + } + + qpgresult->pgconn_ref = NULL; + qpgresult->cursor = 0; + + qpgresult->qpgrow.pgresult_ref = NULL; + qpgresult->qpgrow.idx = 0; +} + +static inline qpgsql_t* pgsql_init(void) +{ + return calloc(1, sizeof(qpgsql_t)); +} + +static inline const char* pgsql_error(qpgsql_t* qpgsql) +{ + if (qpgsql == NULL || qpgsql->pgconn == NULL) { + return "(connection error)"; + } + + return PQerrorMessage(qpgsql->pgconn); +} + +static inline uint32_t pgsql_errno(qpgsql_t* qpgsql) +{ + const char* emsg = pgsql_error(qpgsql); + if (strlen(emsg) > 0) { + return 1; + } + + return 0; +} + +static inline void pgsql_close(qpgsql_t* qpgsql) +{ + if (qpgsql == NULL) { + return; + } + + pgsql_free_result(&qpgsql->qpgresult); + + if (qpgsql->pgconn) { + PQfinish(qpgsql->pgconn); + qpgsql->pgconn = NULL; + } + + free(qpgsql); +} + +static inline int pgsql_query(qpgsql_t* qpgsql, const char *query, pgquery_t type) +{ + if (qpgsql == NULL || qpgsql->pgconn == NULL) { + return -1; /* error */ + } + + PGresult *pgresult = NULL; + ExecStatusType status = -1; + PGconn *pgconn = qpgsql->pgconn; + + /* free previous query results */ + pgsql_free_result(&qpgsql->qpgresult); + + if (type == PQ_PING) { + query = ""; + } + + if (qpgsql->fetchtype && type == PQ_QUERY) { + if (PQsendQuery(pgconn, query) && PQsetSingleRowMode(pgconn)) { + pgresult = PQgetResult(pgconn); + } else { + return -1; /* error */ + } + } else { + pgresult = PQexec(pgconn, query); + } + + if (pgresult == NULL) { + return -1; /* error */ + } + + status = PQresultStatus(pgresult); + + switch (type) { + case PQ_PING: { + if (status != PGRES_EMPTY_QUERY) { + return -1; /* error */ + } + break; + } + + case PQ_QUERY: { + if (status != PGRES_TUPLES_OK && + status != PGRES_SINGLE_TUPLE) { + return -1; /* error */ + } + break; + } + + default: { + if (status != PGRES_COMMAND_OK) { + return -1; /* error */ + } + break; + } + } + + qpgsql->qpgresult.pgresult = pgresult; + qpgsql->qpgresult.pgconn_ref = pgconn; + + return 0; /* ok */ +} + +static inline qpgresult_t* pgsql_get_result(qpgsql_t* qpgsql) +{ + if (qpgsql == NULL || qpgsql->qpgresult.pgresult == NULL) { + return NULL; + } + + return &qpgsql->qpgresult; +} + +static inline int pgsql_affected_rows(qpgsql_t* qpgsql) +{ + if (qpgsql && qpgsql->qpgresult.pgresult) { + return atoi(PQcmdTuples(qpgsql->qpgresult.pgresult)); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_rows(qpgresult_t *qpgresult) +{ + if (qpgresult && qpgresult->pgresult) { + return PQntuples(qpgresult->pgresult); + } + + /* error */ + return -1; +} + +static inline int pgsql_num_fields(qpgresult_t *qpgresult) +{ + if (qpgresult && qpgresult->pgresult) { + return PQnfields(qpgresult->pgresult); + } + + /* error */ + return -1; +} + +static inline qpgrow_t* pgsql_fetch_row(qpgresult_t *qpgresult) +{ + if (qpgresult == NULL || qpgresult->pgresult == NULL) { + return NULL; + } + + PGresult *pgresult = qpgresult->pgresult; + qpgrow_t *qpgrow = &qpgresult->qpgrow; + + int cursor = qpgresult->cursor; + int rows = PQntuples(pgresult); + if (rows > cursor) { + qpgrow->idx = cursor; + qpgrow->pgresult_ref = pgresult; + qpgresult->cursor++; + return qpgrow; + } else { + PGconn *pgconn = qpgresult->pgconn_ref; + PGresult *pgresult = PQgetResult(pgconn); + if (pgresult) { + pgsql_free_result(qpgresult); + qpgresult->pgresult = pgresult; + qpgresult->pgconn_ref = pgconn; + return pgsql_fetch_row(qpgresult); + } else { + return NULL; + } + } +} + +#endif /* ENABLE_PGSQL */ -#include -#include -#include -#include -#include #include "qinternal.h" #include "extensions/qdatabase.h" @@ -120,11 +362,11 @@ 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 const char *result_get_str(qdbresult_t *result, const char *field); +static const char *result_get_str_at(qdbresult_t *result, int idx); +static int result_get_int(qdbresult_t *result, const char *field); +static int result_get_int_at(qdbresult_t *result, int idx); +static bool result_get_next(qdbresult_t *result); static int result_get_cols(qdbresult_t *result); static int result_get_rows(qdbresult_t *result); @@ -132,39 +374,67 @@ static int result_get_row(qdbresult_t *result); static void result_free(qdbresult_t *result); -#endif +#endif /* _DOXYGEN_SKIP */ /** * Initialize internal connector structure * - * @param dbtype database server type. currently "MYSQL" is only supported + * @param dbtype database server type. currently "MYSQL" and "PGSQL" are supported * @param addr ip or fqdn address. * @param port port number + * @param database database name. * @param username database username * @param password database password - * @param database database server type. currently "MYSQL" is only supported * @param autocommit sets autocommit mode on if autocommit is true, off if * autocommit is false + * (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("MYSQL", - * "dbhost.qdecoder.org", 3306, "test", "secret", - * "sampledb", true); + * qdb_t *db = qdb("MYSQL", "dbhost.qdecoder.org", 3306, + * "sampledb", "test", "secret", true); * if (db == NULL) { * printf("ERROR: Not supported database type.\n"); * return -1; * } * @endcode + * + * @code + * qdb_t *db = qdb("PGSQL", "127.0.0.1", 5432, + * "sampledb", "test", "secret", true); + * if (db == NULL) { + * printf("ERROR: Not supported database type.\n"); + * return -1; + * } + * @endcode + * + * @note + * The pgsql server and libpq library does not provide a setting for autocommit.
+ * The following instructions are from the official manual(https://www.postgresql.org/docs/15/sql-begin.html):
+ * BEGIN initiates a transaction block, that is, all statements after a BEGIN command + * will be executed in a single transaction until an explicit COMMIT or ROLLBACK is given. + * By default (without BEGIN), PostgreSQL executes transactions in “autocommit” mode, + * that is, each statement is executed in its own transaction + * and a commit is implicitly performed at the end of the statement + * (if execution was successful, otherwise a rollback is done). */ -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(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 - if (strcmp(dbtype, "MYSQL")) return NULL; + if (strcmp(dbtype, "MYSQL")) { + errno = ENOTSUP; + return NULL; + } +#elif defined(Q_ENABLE_PGSQL) + if (strcmp(dbtype, "PGSQL")) { + errno = ENOTSUP; + return NULL; + } #else return NULL; #endif @@ -176,6 +446,13 @@ qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, return NULL; } +#ifdef Q_ENABLE_PGSQL + if (autocommit == false) { + errno = EINVAL; + return NULL; + } +#endif + // initialize qdb_t *db; if ((db = (qdb_t *)malloc(sizeof(qdb_t))) == NULL) return NULL; @@ -195,6 +472,8 @@ qdb_t *qdb(const char *dbtype, const char *addr, int port, const char *username, // set db handler #ifdef Q_ENABLE_MYSQL db->mysql = NULL; +#elif defined(Q_ENABLE_PGSQL) + db->pgsql = NULL; #endif // assign methods @@ -304,6 +583,58 @@ static bool open_(qdb_t *db) db->connected = true; Q_MUTEX_LEAVE(db->qmutex); return true; +#elif defined(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 lable_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 lable_quit; + } + + if (PQstatus(pgconn) != CONNECTION_OK) { + // const char* emsg = PQerrorMessage(pgconn); + // fprintf(stderr, "%s", emsg); + errno = ECONNREFUSED; + ret = false; + goto lable_quit; + } + + qtype_cast(qpgsql_t*, db->pgsql)->pgconn = pgconn; + + // set flag + db->connected = true; + +lable_quit: + + if (ret == false) { + PQfinish(pgconn); + } + + Q_MUTEX_LEAVE(db->qmutex); + return ret; #else return false; #endif @@ -324,19 +655,28 @@ static bool close_(qdb_t *db) { if (db == NULL) return false; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) Q_MUTEX_ENTER(db->qmutex); +#if defined(Q_ENABLE_MYSQL) if (db->mysql != NULL) { mysql_close(db->mysql); db->mysql = NULL; mysql_library_end(); } - db->connected = false; +#endif /* Q_ENABLE_MYSQL */ +#if defined(Q_ENABLE_PGSQL) + if (db->pgsql != NULL) { + pgsql_close(db->pgsql); + db->pgsql = NULL; + } +#endif /* Q_ENABLE_PGSQL */ + + db->connected = false; Q_MUTEX_LEAVE(db->qmutex); return true; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return false; #endif } @@ -353,21 +693,30 @@ static int execute_update(qdb_t *db, const char *query) { if (db == NULL || db->connected == false) return -1; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) Q_MUTEX_ENTER(db->qmutex); int affected = -1; // query DEBUG("%s", query); +#if defined(Q_ENABLE_MYSQL) if (mysql_query(db->mysql, query) == 0) { /* get affected rows */ if ((affected = mysql_affected_rows(db->mysql)) < 0) affected = -1; } +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if (pgsql_query(db->pgsql, query, PQ_UPDATE) == 0) { + /* get affected rows */ + if ((affected = pgsql_affected_rows(db->pgsql)) < 0) affected = -1; + } +#endif /* Q_ENABLE_PGSQL */ Q_MUTEX_LEAVE(db->qmutex); return affected; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return -1; #endif } @@ -404,16 +753,26 @@ static qdbresult_t *execute_query(qdb_t *db, const char *query) { if (db == NULL || db->connected == false) return NULL; -#ifdef Q_ENABLE_MYSQL - // query +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + DEBUG("%s", query); - if (mysql_query(db->mysql, query)) return NULL; // store qdbresult_t *result = (qdbresult_t *)malloc(sizeof(qdbresult_t)); if (result == NULL) return NULL; + // init result->fetchtype = db->info.fetchtype; + result->cursor = 0; + result->row = NULL; + +#if defined(Q_ENABLE_MYSQL) + // query + if (mysql_query(db->mysql, query)) { + free(result); + return NULL; + } + if (result->fetchtype == false) { result->rs = mysql_store_result(db->mysql); } else { @@ -426,16 +785,31 @@ static qdbresult_t *execute_query(qdb_t *db, const char *query) /* get meta data */ result->fields = NULL; - result->row = NULL; result->cols = mysql_num_fields(result->rs); - result->cursor = 0; +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + qtype_cast(qpgsql_t*, db->pgsql)->fetchtype = result->fetchtype; + + // query + if (pgsql_query(db->pgsql, query, PQ_QUERY)) return NULL; + + result->rs = pgsql_get_result(db->pgsql); + if (result->rs == NULL) { + free(result); + return NULL; + } + + /* get meta data */ + result->cols = pgsql_num_fields(result->rs); +#endif /* Q_ENABLE_PGSQL */ /* assign methods */ - result->get_str = _resultGetStr; - result->get_str_at = _resultGetStrAt; - result->get_int = _resultGetInt; - result->get_int_at = _resultGetIntAt; - result->get_next = _resultGetNext; + result->get_str = result_get_str; + result->get_str_at = result_get_str_at; + result->get_int = result_get_int; + result->get_int_at = result_get_int_at; + result->get_next = result_get_next; result->get_cols = result_get_cols; result->get_rows = result_get_rows; @@ -444,7 +818,7 @@ static qdbresult_t *execute_query(qdb_t *db, const char *query) result->free = result_free; return result; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return NULL; #endif } @@ -485,13 +859,14 @@ static bool begin_tran(qdb_t *db) { if (db == NULL) return false; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) Q_MUTEX_ENTER(db->qmutex); - if (db->qmutex.count != 1) { + if (qtype_cast(qmutex_t*, db->qmutex)->count != 1) { Q_MUTEX_LEAVE(db->qmutex); return false; } +#if defined(Q_ENABLE_MYSQL) qdbresult_t *result; result = db->execute_query(db, "START TRANSACTION"); if (result == NULL) { @@ -500,7 +875,17 @@ static bool begin_tran(qdb_t *db) } result->free(result); return true; -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if (pgsql_query(db->pgsql, "START TRANSACTION", PQ_TRANSACTION) < 0) { + Q_MUTEX_LEAVE(db->qmutex); + return false; + } + return true; +#endif /* Q_ENABLE_PGSQL */ + +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return false; #endif } @@ -516,17 +901,26 @@ static bool commit(qdb_t *db) { if (db == NULL) return false; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) bool ret = false; + +#if defined(Q_ENABLE_MYSQL) if (mysql_commit(db->mysql) == 0) { ret = true; } +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if (pgsql_query(db->pgsql, "COMMIT", PQ_TRANSACTION) == 0) { + ret = true; + } +#endif /* Q_ENABLE_PGSQL */ - if (db->qmutex.count > 0) { + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { Q_MUTEX_LEAVE(db->qmutex); } return ret; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return false; #endif } @@ -542,17 +936,26 @@ static bool rollback(qdb_t *db) { if (db == NULL) return false; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) bool ret = false; + +#if defined(Q_ENABLE_MYSQL) if (mysql_rollback(db->mysql) == 0) { ret = true; } +#endif /* Q_ENABLE_MYSQL */ - if (db->qmutex.count > 0) { +#if defined(Q_ENABLE_PGSQL) + if (pgsql_query(db->pgsql, "ROLLBACK", PQ_TRANSACTION) == 0) { + ret = true; + } +#endif /* Q_ENABLE_PGSQL */ + + if (qtype_cast(qmutex_t*, db->qmutex)->count > 0) { Q_MUTEX_LEAVE(db->qmutex); } return ret; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return 0; #endif } @@ -577,8 +980,22 @@ static bool rollback(qdb_t *db) static bool set_fetchtype(qdb_t *db, bool fromdb) { if (db == NULL) return false; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + +#if defined(Q_ENABLE_MYSQL) + db->info.fetchtype = fromdb; + return true; +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) db->info.fetchtype = fromdb; return true; +#endif /* Q_ENABLE_PGSQL */ + +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ + return false; +#endif } /** @@ -613,10 +1030,24 @@ static bool ping(qdb_t *db) { if (db == NULL) return false; -#ifdef Q_ENABLE_MYSQL +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + bool ping_failed = false; + +#if defined(Q_ENABLE_MYSQL) if (db->connected == true && mysql_ping(db->mysql) == 0) { return true; - } else { // ping test failed + } + ping_failed = true; +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if (db->connected == true && pgsql_query(db->pgsql, NULL, PQ_PING) == 0) { + return true; + } + ping_failed = true; +#endif /* Q_ENABLE_PGSQL */ + + if (ping_failed) { // ping test failed if (open_(db) == true) { // try re-connect DEBUG("Connection recovered."); return true; @@ -624,7 +1055,7 @@ static bool ping(qdb_t *db) } return false; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return false; #endif } @@ -646,11 +1077,20 @@ static const char *get_error(qdb_t *db, unsigned int *errorno) unsigned int eno = 0; const char *emsg; -#ifdef Q_ENABLE_MYSQL + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) +#if defined(Q_ENABLE_MYSQL) eno = mysql_errno(db->mysql); if (eno == 0) emsg = "(no error)"; else emsg = mysql_error(db->mysql); -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + eno = pgsql_errno(db->pgsql); + if (eno == 0) emsg = "(no error)"; + else emsg = pgsql_error(db->pgsql); +#endif /* Q_ENABLE_PGSQL */ +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ emsg = "(not implemented)"; #endif @@ -695,11 +1135,13 @@ static void free_(qdb_t *db) * @note * Do not free returned string. */ -static const char *_resultGetStr(qdbresult_t *result, const char *field) +static const char *result_get_str(qdbresult_t *result, const char *field) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL || result->rs == NULL || result->cols <= 0) return NULL; + if (result == NULL) return NULL; +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL || result->cols <= 0) return NULL; +#if defined(Q_ENABLE_MYSQL) if (result->fields == NULL) result->fields = mysql_fetch_fields(result->rs); int i; @@ -710,7 +1152,19 @@ static const char *_resultGetStr(qdbresult_t *result, const char *field) } return NULL; -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + /* get field num */ + qpgresult_t *qpgresult = result->rs; + int field_num = PQfnumber(qpgresult->pgresult, field); + if (field_num == -1) { + return NULL; + } + + return result->get_str_at(result, field_num + 1); +#endif /* Q_ENABLE_PGSQL */ +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return NULL; #endif } @@ -723,18 +1177,27 @@ static const char *_resultGetStr(qdbresult_t *result, const char *field) * * @return a string pointer if successful, otherwise returns NULL. */ -static const char *_resultGetStrAt(qdbresult_t *result, int idx) +static const char *result_get_str_at(qdbresult_t *result, int idx) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL - || result->rs == NULL - || result->cursor == 0 - || idx <= 0 - || idx > result->cols ) { + if (result == NULL) return NULL; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL + || result->cursor == 0 + || idx <= 0 + || idx > result->cols) { return NULL; } + +#if defined(Q_ENABLE_MYSQL) return result->row[idx-1]; -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + return pgsql_row_get_str_at(result->row, idx - 1); +#endif /* Q_ENABLE_PGSQL */ + +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return NULL; #endif } @@ -747,7 +1210,7 @@ static const char *_resultGetStrAt(qdbresult_t *result, int idx) * * @return a integer converted value */ -static int _resultGetInt(qdbresult_t *result, const char *field) +static int result_get_int(qdbresult_t *result, const char *field) { const char *val = result->get_str(result, field); if (val == NULL) return 0; @@ -762,7 +1225,7 @@ static int _resultGetInt(qdbresult_t *result, const char *field) * * @return a integer converted value */ -static int _resultGetIntAt(qdbresult_t *result, int idx) +static int result_get_int_at(qdbresult_t *result, int idx) { const char *val = result->get_str_at(result, idx); if (val == NULL) return 0; @@ -776,16 +1239,27 @@ static int _resultGetIntAt(qdbresult_t *result, int idx) * * @return true if successful, false if no more rows are left */ -static bool _resultGetNext(qdbresult_t *result) +static bool result_get_next(qdbresult_t *result) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL || result->rs == NULL) return false; + if (result == NULL) return false; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL) return false; +#if defined(Q_ENABLE_MYSQL) if ((result->row = mysql_fetch_row(result->rs)) == NULL) return false; result->cursor++; return true; -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if ((result->row = pgsql_fetch_row(result->rs)) == NULL) return false; + result->cursor++; + + return true; +#endif /* Q_ENABLE_PGSQL */ +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return false; #endif } @@ -799,10 +1273,12 @@ static bool _resultGetNext(qdbresult_t *result) */ static int result_get_cols(qdbresult_t *result) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL || result->rs == NULL) return 0; + if (result == NULL) return 0; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL) return 0; return result->cols; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return 0; #endif } @@ -816,10 +1292,20 @@ static int result_get_cols(qdbresult_t *result) */ static int result_get_rows(qdbresult_t *result) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL || result->rs == NULL) return 0; + if (result == NULL) return 0; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL) return 0; + +#if defined(Q_ENABLE_MYSQL) return mysql_num_rows(result->rs); -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + return pgsql_num_rows(result->rs); +#endif /* Q_ENABLE_PGSQL */ + +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return 0; #endif } @@ -836,10 +1322,12 @@ static int result_get_rows(qdbresult_t *result) */ static int result_get_row(qdbresult_t *result) { -#ifdef Q_ENABLE_MYSQL - if (result == NULL || result->rs == NULL) return 0; + if (result == NULL) return 0; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) + if (result->rs == NULL) return 0; return result->cursor; -#else +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return 0; #endif } @@ -851,8 +1339,10 @@ static int result_get_row(qdbresult_t *result) */ static void result_free(qdbresult_t *result) { -#ifdef Q_ENABLE_MYSQL if (result == NULL) return; + +#if defined(Q_ENABLE_MYSQL) || defined(Q_ENABLE_PGSQL) +#if defined(Q_ENABLE_MYSQL) if (result->rs != NULL) { if (result->fetchtype == true) { while (mysql_fetch_row(result->rs) != NULL); @@ -862,7 +1352,17 @@ static void result_free(qdbresult_t *result) } free(result); return; -#else +#endif /* Q_ENABLE_MYSQL */ + +#if defined(Q_ENABLE_PGSQL) + if (result->rs != NULL) { + pgsql_free_result(result->rs); + result->rs = NULL; + } + free(result); + return; +#endif /* Q_ENABLE_PGSQL */ +#else /* Q_ENABLE_MYSQL || Q_ENABLE_PGSQL */ return; #endif } 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..81c6e353 --- /dev/null +++ b/tests/test_qdatabase_pgsql.c @@ -0,0 +1,368 @@ +/****************************************************************************** + * 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. + *****************************************************************************/ + +#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()/open()/close()/free()") { + qdb_t *db = NULL; + + /* 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); + + /* 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_NULL(qrst->get_str(qrst, "animal_name")); + ASSERT_NULL(qrst->get_str_at(qrst, 3)); + + int i = 1; + while (qrst->get_next(qrst)) { + ASSERT_EQUAL_INT(qrst->get_row(qrst), i); + if (i == 1) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Dog"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Canis lupus familiaris"); + } else if (i == 2) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Cat"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Felis catus"); + } else if (i == 3) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Rabbit"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Oryctolagus cuniculus"); + } else if (i == 4) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Hamster"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Mesocricetus auratus"); + } else if (i == 5) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Fish"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Poecilia reticulata"); + } + i++; + } + ASSERT_EQUAL_INT(i, 6); + 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: fetchtype") { + 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); + + /* test: set_fetchtype true */ + ASSERT_TRUE(test_db->set_fetchtype(test_db, true)); + + /* test: execute_queryf in fetchtype mode */ + qdbresult_t *qrst = test_db->execute_queryf(test_db, "SELECT * FROM %s;", "animals"); + ASSERT_EQUAL_INT(qrst->get_rows(qrst), 1); + ASSERT_EQUAL_INT(qrst->get_cols(qrst), 7); + + ASSERT_EQUAL_INT(qrst->get_row(qrst), 0); + ASSERT_NULL(qrst->get_str(qrst, "animal_name")); + ASSERT_NULL(qrst->get_str_at(qrst, 3)); + + int i = 1; + while (qrst->get_next(qrst)) { + ASSERT_EQUAL_INT(qrst->get_row(qrst), i); + if (i == 1) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Dog"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Canis lupus familiaris"); + } else if (i == 2) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Cat"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Felis catus"); + } else if (i == 3) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Rabbit"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Oryctolagus cuniculus"); + } else if (i == 4) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Hamster"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Mesocricetus auratus"); + } else if (i == 5) { + ASSERT_EQUAL_STR(qrst->get_str(qrst, "animal_name"), "Fish"); + ASSERT_EQUAL_STR(qrst->get_str_at(qrst, 3), "Poecilia reticulata"); + } + i++; + } + ASSERT_EQUAL_INT(i, 6); + qrst->free(qrst); + + /* test: set_fetchtype false */ + ASSERT_TRUE(test_db->set_fetchtype(test_db, false)); + + /* 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("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); + + /* test:get_error */ + ASSERT_EQUAL_STR(test_db->get_error(test_db, NULL), "(no error)"); + + /* 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();