diff options
Diffstat (limited to 'src/lib-sql')
-rw-r--r-- | src/lib-sql/Makefile.am | 144 | ||||
-rw-r--r-- | src/lib-sql/Makefile.in | 1159 | ||||
-rw-r--r-- | src/lib-sql/driver-cassandra.c | 2588 | ||||
-rw-r--r-- | src/lib-sql/driver-mysql.c | 844 | ||||
-rw-r--r-- | src/lib-sql/driver-pgsql.c | 1344 | ||||
-rw-r--r-- | src/lib-sql/driver-sqlite.c | 555 | ||||
-rw-r--r-- | src/lib-sql/driver-sqlpool.c | 934 | ||||
-rw-r--r-- | src/lib-sql/driver-test.c | 514 | ||||
-rw-r--r-- | src/lib-sql/driver-test.h | 28 | ||||
-rw-r--r-- | src/lib-sql/sql-api-private.h | 255 | ||||
-rw-r--r-- | src/lib-sql/sql-api.c | 846 | ||||
-rw-r--r-- | src/lib-sql/sql-api.h | 251 | ||||
-rw-r--r-- | src/lib-sql/sql-db-cache.c | 156 | ||||
-rw-r--r-- | src/lib-sql/sql-db-cache.h | 13 |
14 files changed, 9631 insertions, 0 deletions
diff --git a/src/lib-sql/Makefile.am b/src/lib-sql/Makefile.am new file mode 100644 index 0000000..89c3d2d --- /dev/null +++ b/src/lib-sql/Makefile.am @@ -0,0 +1,144 @@ +noinst_LTLIBRARIES = libsql.la libdriver_test.la + +SQL_DRIVER_PLUGINS = + +# automake seems to force making this unconditional.. +NOPLUGIN_LDFLAGS = + +if SQL_PLUGINS +if BUILD_MYSQL +MYSQL_LIB = libdriver_mysql.la +SQL_DRIVER_PLUGINS += mysql +endif +if BUILD_PGSQL +PGSQL_LIB = libdriver_pgsql.la +SQL_DRIVER_PLUGINS += pgsql +endif +if BUILD_SQLITE +SQLITE_LIB = libdriver_sqlite.la +SQL_DRIVER_PLUGINS += sqlite +endif +if BUILD_CASSANDRA +CASSANDRA_LIB = libdriver_cassandra.la +SQL_DRIVER_PLUGINS += cassandra +endif + +sql_module_LTLIBRARIES = \ + $(MYSQL_LIB) \ + $(PGSQL_LIB) \ + $(SQLITE_LIB) \ + $(CASSANDRA_LIB) + +sql_moduledir = $(moduledir) +endif + +sql_drivers = @sql_drivers@ + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-settings \ + $(SQL_CFLAGS) + +dist_sources = \ + sql-api.c \ + sql-db-cache.c + +if ! SQL_PLUGINS +driver_sources = \ + driver-mysql.c \ + driver-pgsql.c \ + driver-sqlite.c \ + driver-cassandra.c +endif + +libsql_la_SOURCES = \ + $(dist_sources) \ + $(driver_sources) \ + driver-sqlpool.c +libsql_la_LIBADD = $(SQL_LIBS) + +nodist_libsql_la_SOURCES = sql-drivers-register.c + +deplibs = \ + ../lib-dovecot/libdovecot.la + +if SQL_PLUGINS +libdriver_mysql_la_LDFLAGS = -module -avoid-version +libdriver_mysql_la_LIBADD = $(MYSQL_LIBS) +libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS) +libdriver_mysql_la_SOURCES = driver-mysql.c + +libdriver_pgsql_la_LDFLAGS = -module -avoid-version +libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS) +libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS) +libdriver_pgsql_la_SOURCES = driver-pgsql.c + +libdriver_sqlite_la_LDFLAGS = -module -avoid-version +libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS) +libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS) +libdriver_sqlite_la_SOURCES = driver-sqlite.c + +libdriver_cassandra_la_LDFLAGS = -module -avoid-version +libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS) +libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS) +libdriver_cassandra_la_SOURCES = driver-cassandra.c +else +endif + +libdriver_test_la_LDFLAGS = -avoid-version +libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \ + -I$(top_srcdir)/src/lib-test +libdriver_test_la_SOURCES = driver-test.c + +noinst_HEADERS = driver-test.h + +pkglib_LTLIBRARIES = libdovecot-sql.la +libdovecot_sql_la_SOURCES = +libdovecot_sql_la_LIBADD = libsql.la $(deplibs) +libdovecot_sql_la_DEPENDENCIES = libsql.la +libdovecot_sql_la_LDFLAGS = -export-dynamic + +headers = \ + sql-api.h \ + sql-api-private.h \ + sql-db-cache.h + +pkginc_libdir=$(pkgincludedir) +pkginc_lib_HEADERS = $(headers) + +sql-drivers-register.c: Makefile + rm -f $@ + echo '/* this file automatically generated by Makefile */' >$@ + echo '#include "lib.h"' >>$@ + echo '#include "sql-api.h"' >>$@ +if ! SQL_PLUGINS + for i in $(sql_drivers) null; do \ + if [ "$${i}" != "null" ]; then \ + echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \ + fi; \ + done +endif + echo 'void sql_drivers_register_all(void) {' >>$@ +if ! SQL_PLUGINS + for i in $(sql_drivers) null; do \ + if [ "$${i}" != "null" ]; then \ + echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \ + fi; \ + done +endif + echo '}' >>$@ + +if SQL_PLUGINS +install-exec-local: + for d in auth dict; do \ + $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \ + for driver in $(SQL_DRIVER_PLUGINS); do \ + rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \ + $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \ + done; \ + done +endif + + +distclean-generic: + rm -f Makefile sql-drivers-register.c diff --git a/src/lib-sql/Makefile.in b/src/lib-sql/Makefile.in new file mode 100644 index 0000000..e7d8964 --- /dev/null +++ b/src/lib-sql/Makefile.in @@ -0,0 +1,1159 @@ +# Makefile.in generated by automake 1.16.3 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2020 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + + +VPATH = @srcdir@ +am__is_gnu_make = { \ + if test -z '$(MAKELEVEL)'; then \ + false; \ + elif test -n '$(MAKE_HOST)'; then \ + true; \ + elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ + true; \ + else \ + false; \ + fi; \ +} +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_1 = mysql +@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_2 = pgsql +@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am__append_3 = sqlite +@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am__append_4 = cassandra +subdir = src/lib-sql +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \ + $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \ + $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \ + $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \ + $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \ + $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \ + $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \ + $(top_srcdir)/m4/flexible_array_member.m4 \ + $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \ + $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \ + $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \ + $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \ + $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \ + $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \ + $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \ + $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/pr_set_dumpable.m4 \ + $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \ + $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \ + $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \ + $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \ + $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \ + $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \ + $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \ + $(top_srcdir)/m4/typeof_dev_t.m4 \ + $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \ + $(top_srcdir)/m4/want_apparmor.m4 \ + $(top_srcdir)/m4/want_bsdauth.m4 \ + $(top_srcdir)/m4/want_bzlib.m4 \ + $(top_srcdir)/m4/want_cassandra.m4 \ + $(top_srcdir)/m4/want_cdb.m4 \ + $(top_srcdir)/m4/want_checkpassword.m4 \ + $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \ + $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \ + $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \ + $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \ + $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \ + $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \ + $(top_srcdir)/m4/want_prefetch.m4 \ + $(top_srcdir)/m4/want_shadow.m4 \ + $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \ + $(top_srcdir)/m4/want_sqlite.m4 \ + $(top_srcdir)/m4/want_stemmer.m4 \ + $(top_srcdir)/m4/want_systemd.m4 \ + $(top_srcdir)/m4/want_textcat.m4 \ + $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \ + $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \ + $(pkginc_lib_HEADERS) $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__installdirs = "$(DESTDIR)$(pkglibdir)" \ + "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)" +LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES) \ + $(sql_module_LTLIBRARIES) +am_libdovecot_sql_la_OBJECTS = +libdovecot_sql_la_OBJECTS = $(am_libdovecot_sql_la_OBJECTS) +AM_V_lt = $(am__v_lt_@AM_V@) +am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) +am__v_lt_0 = --silent +am__v_lt_1 = +libdovecot_sql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdovecot_sql_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +am__DEPENDENCIES_1 = +@SQL_PLUGINS_TRUE@libdriver_cassandra_la_DEPENDENCIES = \ +@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1) +am__libdriver_cassandra_la_SOURCES_DIST = driver-cassandra.c +@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_OBJECTS = \ +@SQL_PLUGINS_TRUE@ libdriver_cassandra_la-driver-cassandra.lo +libdriver_cassandra_la_OBJECTS = $(am_libdriver_cassandra_la_OBJECTS) +libdriver_cassandra_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdriver_cassandra_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_rpath = \ +@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ -rpath \ +@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ $(sql_moduledir) +@SQL_PLUGINS_TRUE@libdriver_mysql_la_DEPENDENCIES = \ +@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1) +am__libdriver_mysql_la_SOURCES_DIST = driver-mysql.c +@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_OBJECTS = \ +@SQL_PLUGINS_TRUE@ libdriver_mysql_la-driver-mysql.lo +libdriver_mysql_la_OBJECTS = $(am_libdriver_mysql_la_OBJECTS) +libdriver_mysql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdriver_mysql_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_rpath = \ +@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir) +@SQL_PLUGINS_TRUE@libdriver_pgsql_la_DEPENDENCIES = \ +@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1) +am__libdriver_pgsql_la_SOURCES_DIST = driver-pgsql.c +@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_OBJECTS = \ +@SQL_PLUGINS_TRUE@ libdriver_pgsql_la-driver-pgsql.lo +libdriver_pgsql_la_OBJECTS = $(am_libdriver_pgsql_la_OBJECTS) +libdriver_pgsql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdriver_pgsql_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_rpath = \ +@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir) +@SQL_PLUGINS_TRUE@libdriver_sqlite_la_DEPENDENCIES = \ +@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1) +am__libdriver_sqlite_la_SOURCES_DIST = driver-sqlite.c +@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_OBJECTS = \ +@SQL_PLUGINS_TRUE@ libdriver_sqlite_la-driver-sqlite.lo +libdriver_sqlite_la_OBJECTS = $(am_libdriver_sqlite_la_OBJECTS) +libdriver_sqlite_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdriver_sqlite_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_rpath = \ +@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir) +libdriver_test_la_LIBADD = +am_libdriver_test_la_OBJECTS = libdriver_test_la-driver-test.lo +libdriver_test_la_OBJECTS = $(am_libdriver_test_la_OBJECTS) +libdriver_test_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdriver_test_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +libsql_la_DEPENDENCIES = $(am__DEPENDENCIES_1) +am__libsql_la_SOURCES_DIST = sql-api.c sql-db-cache.c driver-mysql.c \ + driver-pgsql.c driver-sqlite.c driver-cassandra.c \ + driver-sqlpool.c +am__objects_1 = sql-api.lo sql-db-cache.lo +@SQL_PLUGINS_FALSE@am__objects_2 = driver-mysql.lo driver-pgsql.lo \ +@SQL_PLUGINS_FALSE@ driver-sqlite.lo driver-cassandra.lo +am_libsql_la_OBJECTS = $(am__objects_1) $(am__objects_2) \ + driver-sqlpool.lo +nodist_libsql_la_OBJECTS = sql-drivers-register.lo +libsql_la_OBJECTS = $(am_libsql_la_OBJECTS) \ + $(nodist_libsql_la_OBJECTS) +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +depcomp = $(SHELL) $(top_srcdir)/depcomp +am__maybe_remake_depfiles = depfiles +am__depfiles_remade = ./$(DEPDIR)/driver-cassandra.Plo \ + ./$(DEPDIR)/driver-mysql.Plo ./$(DEPDIR)/driver-pgsql.Plo \ + ./$(DEPDIR)/driver-sqlite.Plo ./$(DEPDIR)/driver-sqlpool.Plo \ + ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo \ + ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo \ + ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo \ + ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo \ + ./$(DEPDIR)/libdriver_test_la-driver-test.Plo \ + ./$(DEPDIR)/sql-api.Plo ./$(DEPDIR)/sql-db-cache.Plo \ + ./$(DEPDIR)/sql-drivers-register.Plo +am__mv = mv -f +COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ + $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) +LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \ + $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(AM_CFLAGS) $(CFLAGS) +AM_V_CC = $(am__v_CC_@AM_V@) +am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@) +am__v_CC_0 = @echo " CC " $@; +am__v_CC_1 = +CCLD = $(CC) +LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(AM_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_CCLD = $(am__v_CCLD_@AM_V@) +am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@) +am__v_CCLD_0 = @echo " CCLD " $@; +am__v_CCLD_1 = +SOURCES = $(libdovecot_sql_la_SOURCES) \ + $(libdriver_cassandra_la_SOURCES) \ + $(libdriver_mysql_la_SOURCES) $(libdriver_pgsql_la_SOURCES) \ + $(libdriver_sqlite_la_SOURCES) $(libdriver_test_la_SOURCES) \ + $(libsql_la_SOURCES) $(nodist_libsql_la_SOURCES) +DIST_SOURCES = $(libdovecot_sql_la_SOURCES) \ + $(am__libdriver_cassandra_la_SOURCES_DIST) \ + $(am__libdriver_mysql_la_SOURCES_DIST) \ + $(am__libdriver_pgsql_la_SOURCES_DIST) \ + $(am__libdriver_sqlite_la_SOURCES_DIST) \ + $(libdriver_test_la_SOURCES) $(am__libsql_la_SOURCES_DIST) +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS) +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +APPARMOR_LIBS = @APPARMOR_LIBS@ +AR = @AR@ +AUTH_CFLAGS = @AUTH_CFLAGS@ +AUTH_LIBS = @AUTH_LIBS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BINARY_CFLAGS = @BINARY_CFLAGS@ +BINARY_LDFLAGS = @BINARY_LDFLAGS@ +BISON = @BISON@ +CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@ +CASSANDRA_LIBS = @CASSANDRA_LIBS@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CDB_LIBS = @CDB_LIBS@ +CFLAGS = @CFLAGS@ +CLUCENE_CFLAGS = @CLUCENE_CFLAGS@ +CLUCENE_LIBS = @CLUCENE_LIBS@ +COMPRESS_LIBS = @COMPRESS_LIBS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CRYPT_LIBS = @CRYPT_LIBS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DICT_LIBS = @DICT_LIBS@ +DLLIB = @DLLIB@ +DLLTOOL = @DLLTOOL@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +FLEX = @FLEX@ +FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@ +FUZZER_LDFLAGS = @FUZZER_LDFLAGS@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +KRB5CONFIG = @KRB5CONFIG@ +KRB5_CFLAGS = @KRB5_CFLAGS@ +KRB5_LIBS = @KRB5_LIBS@ +LD = @LD@ +LDAP_LIBS = @LDAP_LIBS@ +LDFLAGS = @LDFLAGS@ +LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@ +LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@ +LIBCAP = @LIBCAP@ +LIBDOVECOT = @LIBDOVECOT@ +LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@ +LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@ +LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@ +LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@ +LIBDOVECOT_LDA = @LIBDOVECOT_LDA@ +LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@ +LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@ +LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@ +LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@ +LIBDOVECOT_LUA = @LIBDOVECOT_LUA@ +LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@ +LIBDOVECOT_SQL = @LIBDOVECOT_SQL@ +LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@ +LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@ +LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@ +LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@ +LIBICONV = @LIBICONV@ +LIBICU_CFLAGS = @LIBICU_CFLAGS@ +LIBICU_LIBS = @LIBICU_LIBS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@ +LIBSODIUM_LIBS = @LIBSODIUM_LIBS@ +LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@ +LIBTIRPC_LIBS = @LIBTIRPC_LIBS@ +LIBTOOL = @LIBTOOL@ +LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@ +LIBUNWIND_LIBS = @LIBUNWIND_LIBS@ +LIBWRAP_LIBS = @LIBWRAP_LIBS@ +LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBICONV = @LTLIBICONV@ +LTLIBOBJS = @LTLIBOBJS@ +LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ +LUA_CFLAGS = @LUA_CFLAGS@ +LUA_LIBS = @LUA_LIBS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MODULE_LIBS = @MODULE_LIBS@ +MODULE_SUFFIX = @MODULE_SUFFIX@ +MYSQL_CFLAGS = @MYSQL_CFLAGS@ +MYSQL_CONFIG = @MYSQL_CONFIG@ +MYSQL_LIBS = @MYSQL_LIBS@ +NM = @NM@ +NMEDIT = @NMEDIT@ + +# automake seems to force making this unconditional.. +NOPLUGIN_LDFLAGS = +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PANDOC = @PANDOC@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PGSQL_CFLAGS = @PGSQL_CFLAGS@ +PGSQL_LIBS = @PGSQL_LIBS@ +PG_CONFIG = @PG_CONFIG@ +PIE_CFLAGS = @PIE_CFLAGS@ +PIE_LDFLAGS = @PIE_LDFLAGS@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +QUOTA_LIBS = @QUOTA_LIBS@ +RANLIB = @RANLIB@ +RELRO_LDFLAGS = @RELRO_LDFLAGS@ +RPCGEN = @RPCGEN@ +RUN_TEST = @RUN_TEST@ +SED = @SED@ +SETTING_FILES = @SETTING_FILES@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SQLITE_CFLAGS = @SQLITE_CFLAGS@ +SQLITE_LIBS = @SQLITE_LIBS@ +SQL_CFLAGS = @SQL_CFLAGS@ +SQL_LIBS = @SQL_LIBS@ +SSL_CFLAGS = @SSL_CFLAGS@ +SSL_LIBS = @SSL_LIBS@ +STRIP = @STRIP@ +SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@ +SYSTEMD_LIBS = @SYSTEMD_LIBS@ +VALGRIND = @VALGRIND@ +VERSION = @VERSION@ +ZSTD_CFLAGS = @ZSTD_CFLAGS@ +ZSTD_LIBS = @ZSTD_LIBS@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +dict_drivers = @dict_drivers@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +moduledir = @moduledir@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +rundir = @rundir@ +runstatedir = @runstatedir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +sql_drivers = @sql_drivers@ +srcdir = @srcdir@ +ssldir = @ssldir@ +statedir = @statedir@ +sysconfdir = @sysconfdir@ +systemdservicetype = @systemdservicetype@ +systemdsystemunitdir = @systemdsystemunitdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +noinst_LTLIBRARIES = libsql.la libdriver_test.la +SQL_DRIVER_PLUGINS = $(am__append_1) $(am__append_2) $(am__append_3) \ + $(am__append_4) +@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@MYSQL_LIB = libdriver_mysql.la +@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@PGSQL_LIB = libdriver_pgsql.la +@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@SQLITE_LIB = libdriver_sqlite.la +@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@CASSANDRA_LIB = libdriver_cassandra.la +@SQL_PLUGINS_TRUE@sql_module_LTLIBRARIES = \ +@SQL_PLUGINS_TRUE@ $(MYSQL_LIB) \ +@SQL_PLUGINS_TRUE@ $(PGSQL_LIB) \ +@SQL_PLUGINS_TRUE@ $(SQLITE_LIB) \ +@SQL_PLUGINS_TRUE@ $(CASSANDRA_LIB) + +@SQL_PLUGINS_TRUE@sql_moduledir = $(moduledir) +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-settings \ + $(SQL_CFLAGS) + +dist_sources = \ + sql-api.c \ + sql-db-cache.c + +@SQL_PLUGINS_FALSE@driver_sources = \ +@SQL_PLUGINS_FALSE@ driver-mysql.c \ +@SQL_PLUGINS_FALSE@ driver-pgsql.c \ +@SQL_PLUGINS_FALSE@ driver-sqlite.c \ +@SQL_PLUGINS_FALSE@ driver-cassandra.c + +libsql_la_SOURCES = \ + $(dist_sources) \ + $(driver_sources) \ + driver-sqlpool.c + +libsql_la_LIBADD = $(SQL_LIBS) +nodist_libsql_la_SOURCES = sql-drivers-register.c +deplibs = \ + ../lib-dovecot/libdovecot.la + +@SQL_PLUGINS_TRUE@libdriver_mysql_la_LDFLAGS = -module -avoid-version +@SQL_PLUGINS_TRUE@libdriver_mysql_la_LIBADD = $(MYSQL_LIBS) +@SQL_PLUGINS_TRUE@libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS) +@SQL_PLUGINS_TRUE@libdriver_mysql_la_SOURCES = driver-mysql.c +@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LDFLAGS = -module -avoid-version +@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS) +@SQL_PLUGINS_TRUE@libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS) +@SQL_PLUGINS_TRUE@libdriver_pgsql_la_SOURCES = driver-pgsql.c +@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LDFLAGS = -module -avoid-version +@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS) +@SQL_PLUGINS_TRUE@libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS) +@SQL_PLUGINS_TRUE@libdriver_sqlite_la_SOURCES = driver-sqlite.c +@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LDFLAGS = -module -avoid-version +@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS) +@SQL_PLUGINS_TRUE@libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS) +@SQL_PLUGINS_TRUE@libdriver_cassandra_la_SOURCES = driver-cassandra.c +libdriver_test_la_LDFLAGS = -avoid-version +libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \ + -I$(top_srcdir)/src/lib-test + +libdriver_test_la_SOURCES = driver-test.c +noinst_HEADERS = driver-test.h +pkglib_LTLIBRARIES = libdovecot-sql.la +libdovecot_sql_la_SOURCES = +libdovecot_sql_la_LIBADD = libsql.la $(deplibs) +libdovecot_sql_la_DEPENDENCIES = libsql.la +libdovecot_sql_la_LDFLAGS = -export-dynamic +headers = \ + sql-api.h \ + sql-api-private.h \ + sql-db-cache.h + +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = $(headers) +all: all-am + +.SUFFIXES: +.SUFFIXES: .c .lo .o .obj +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-sql/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/lib-sql/Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +clean-noinstLTLIBRARIES: + -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES) + @list='$(noinst_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \ + } + +uninstall-pkglibLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \ + done + +clean-pkglibLTLIBRARIES: + -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES) + @list='$(pkglib_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +install-sql_moduleLTLIBRARIES: $(sql_module_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(sql_moduledir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(sql_moduledir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(sql_moduledir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(sql_moduledir)"; \ + } + +uninstall-sql_moduleLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(sql_moduledir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(sql_moduledir)/$$f"; \ + done + +clean-sql_moduleLTLIBRARIES: + -test -z "$(sql_module_LTLIBRARIES)" || rm -f $(sql_module_LTLIBRARIES) + @list='$(sql_module_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +libdovecot-sql.la: $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_DEPENDENCIES) $(EXTRA_libdovecot_sql_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdovecot_sql_la_LINK) -rpath $(pkglibdir) $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_LIBADD) $(LIBS) + +libdriver_cassandra.la: $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_DEPENDENCIES) $(EXTRA_libdriver_cassandra_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdriver_cassandra_la_LINK) $(am_libdriver_cassandra_la_rpath) $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_LIBADD) $(LIBS) + +libdriver_mysql.la: $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_DEPENDENCIES) $(EXTRA_libdriver_mysql_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdriver_mysql_la_LINK) $(am_libdriver_mysql_la_rpath) $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_LIBADD) $(LIBS) + +libdriver_pgsql.la: $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_DEPENDENCIES) $(EXTRA_libdriver_pgsql_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdriver_pgsql_la_LINK) $(am_libdriver_pgsql_la_rpath) $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_LIBADD) $(LIBS) + +libdriver_sqlite.la: $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_DEPENDENCIES) $(EXTRA_libdriver_sqlite_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdriver_sqlite_la_LINK) $(am_libdriver_sqlite_la_rpath) $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_LIBADD) $(LIBS) + +libdriver_test.la: $(libdriver_test_la_OBJECTS) $(libdriver_test_la_DEPENDENCIES) $(EXTRA_libdriver_test_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdriver_test_la_LINK) $(libdriver_test_la_OBJECTS) $(libdriver_test_la_LIBADD) $(LIBS) + +libsql.la: $(libsql_la_OBJECTS) $(libsql_la_DEPENDENCIES) $(EXTRA_libsql_la_DEPENDENCIES) + $(AM_V_CCLD)$(LINK) $(libsql_la_OBJECTS) $(libsql_la_LIBADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-cassandra.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-mysql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-pgsql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlite.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlpool.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_test_la-driver-test.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-api.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-db-cache.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-drivers-register.Plo@am__quote@ # am--include-marker + +$(am__depfiles_remade): + @$(MKDIR_P) $(@D) + @echo '# dummy' >$@-t && $(am__mv) $@-t $@ + +am--depfiles: $(am__depfiles_remade) + +.c.o: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $< + +.c.obj: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'` + +.c.lo: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $< + +libdriver_cassandra_la-driver-cassandra.lo: driver-cassandra.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_cassandra_la-driver-cassandra.lo -MD -MP -MF $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-cassandra.c' object='libdriver_cassandra_la-driver-cassandra.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c + +libdriver_mysql_la-driver-mysql.lo: driver-mysql.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_mysql_la-driver-mysql.lo -MD -MP -MF $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo $(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-mysql.c' object='libdriver_mysql_la-driver-mysql.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c + +libdriver_pgsql_la-driver-pgsql.lo: driver-pgsql.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_pgsql_la-driver-pgsql.lo -MD -MP -MF $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-pgsql.c' object='libdriver_pgsql_la-driver-pgsql.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c + +libdriver_sqlite_la-driver-sqlite.lo: driver-sqlite.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_sqlite_la-driver-sqlite.lo -MD -MP -MF $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-sqlite.c' object='libdriver_sqlite_la-driver-sqlite.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c + +libdriver_test_la-driver-test.lo: driver-test.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_test_la-driver-test.lo -MD -MP -MF $(DEPDIR)/libdriver_test_la-driver-test.Tpo -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_test_la-driver-test.Tpo $(DEPDIR)/libdriver_test_la-driver-test.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-test.c' object='libdriver_test_la-driver-test.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-pkginc_libHEADERS: $(pkginc_lib_HEADERS) + @$(NORMAL_INSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \ + $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \ + done + +uninstall-pkginc_libHEADERS: + @$(NORMAL_UNINSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir) + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-am +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-am + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-am + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +distdir: $(BUILT_SOURCES) + $(MAKE) $(AM_MAKEFLAGS) distdir-am + +distdir-am: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile $(LTLIBRARIES) $(HEADERS) +installdirs: + for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +@SQL_PLUGINS_FALSE@install-exec-local: +clean: clean-am + +clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \ + clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \ + mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/driver-cassandra.Plo + -rm -f ./$(DEPDIR)/driver-mysql.Plo + -rm -f ./$(DEPDIR)/driver-pgsql.Plo + -rm -f ./$(DEPDIR)/driver-sqlite.Plo + -rm -f ./$(DEPDIR)/driver-sqlpool.Plo + -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo + -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo + -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo + -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo + -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo + -rm -f ./$(DEPDIR)/sql-api.Plo + -rm -f ./$(DEPDIR)/sql-db-cache.Plo + -rm -f ./$(DEPDIR)/sql-drivers-register.Plo + -rm -f Makefile +distclean-am: clean-am distclean-compile distclean-generic \ + distclean-tags + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-pkginc_libHEADERS \ + install-sql_moduleLTLIBRARIES + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: install-exec-local install-pkglibLTLIBRARIES + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f ./$(DEPDIR)/driver-cassandra.Plo + -rm -f ./$(DEPDIR)/driver-mysql.Plo + -rm -f ./$(DEPDIR)/driver-pgsql.Plo + -rm -f ./$(DEPDIR)/driver-sqlite.Plo + -rm -f ./$(DEPDIR)/driver-sqlpool.Plo + -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo + -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo + -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo + -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo + -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo + -rm -f ./$(DEPDIR)/sql-api.Plo + -rm -f ./$(DEPDIR)/sql-db-cache.Plo + -rm -f ./$(DEPDIR)/sql-drivers-register.Plo + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \ + uninstall-sql_moduleLTLIBRARIES + +.MAKE: install-am install-strip + +.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \ + clean-generic clean-libtool clean-noinstLTLIBRARIES \ + clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \ + cscopelist-am ctags ctags-am distclean distclean-compile \ + distclean-generic distclean-libtool distclean-tags distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-data install-data-am install-dvi install-dvi-am \ + install-exec install-exec-am install-exec-local install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-pkginc_libHEADERS \ + install-pkglibLTLIBRARIES install-ps install-ps-am \ + install-sql_moduleLTLIBRARIES install-strip installcheck \ + installcheck-am installdirs maintainer-clean \ + maintainer-clean-generic mostlyclean mostlyclean-compile \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags tags-am uninstall uninstall-am \ + uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \ + uninstall-sql_moduleLTLIBRARIES + +.PRECIOUS: Makefile + + +sql-drivers-register.c: Makefile + rm -f $@ + echo '/* this file automatically generated by Makefile */' >$@ + echo '#include "lib.h"' >>$@ + echo '#include "sql-api.h"' >>$@ +@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \ +@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \ +@SQL_PLUGINS_FALSE@ echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \ +@SQL_PLUGINS_FALSE@ fi; \ +@SQL_PLUGINS_FALSE@ done + echo 'void sql_drivers_register_all(void) {' >>$@ +@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \ +@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \ +@SQL_PLUGINS_FALSE@ echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \ +@SQL_PLUGINS_FALSE@ fi; \ +@SQL_PLUGINS_FALSE@ done + echo '}' >>$@ + +@SQL_PLUGINS_TRUE@install-exec-local: +@SQL_PLUGINS_TRUE@ for d in auth dict; do \ +@SQL_PLUGINS_TRUE@ $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \ +@SQL_PLUGINS_TRUE@ for driver in $(SQL_DRIVER_PLUGINS); do \ +@SQL_PLUGINS_TRUE@ rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \ +@SQL_PLUGINS_TRUE@ $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \ +@SQL_PLUGINS_TRUE@ done; \ +@SQL_PLUGINS_TRUE@ done + +distclean-generic: + rm -f Makefile sql-drivers-register.c + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/lib-sql/driver-cassandra.c b/src/lib-sql/driver-cassandra.c new file mode 100644 index 0000000..2b86a12 --- /dev/null +++ b/src/lib-sql/driver-cassandra.c @@ -0,0 +1,2588 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream.h" +#include "array.h" +#include "hostpid.h" +#include "hex-binary.h" +#include "str.h" +#include "ioloop.h" +#include "net.h" +#include "write-full.h" +#include "time-util.h" +#include "var-expand.h" +#include "safe-memset.h" +#include "settings-parser.h" +#include "sql-api-private.h" + +#ifdef BUILD_CASSANDRA +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <cassandra.h> +#include <pthread.h> + +#define IS_CONNECTED(db) \ + ((db)->api.state != SQL_DB_STATE_DISCONNECTED && \ + (db)->api.state != SQL_DB_STATE_CONNECTING) + +#define CASSANDRA_FALLBACK_WARN_INTERVAL_SECS 60 +#define CASSANDRA_FALLBACK_FIRST_RETRY_MSECS 50 +#define CASSANDRA_FALLBACK_MAX_RETRY_MSECS (1000*60) + +#define CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS (5*1000) + +typedef void driver_cassandra_callback_t(CassFuture *future, void *context); + +enum cassandra_counter_type { + CASSANDRA_COUNTER_TYPE_QUERY_SENT, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE, + CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER, + CASSANDRA_COUNTER_TYPE_QUERY_SLOW, + + CASSANDRA_COUNTER_COUNT +}; +static const char *counter_names[CASSANDRA_COUNTER_COUNT] = { + "sent", + "recv_ok", + "recv_err_no_hosts", + "recv_err_queue_full", + "recv_err_client_timeout", + "recv_err_server_timeout", + "recv_err_server_unavailable", + "recv_err_other", + "slow", +}; + +enum cassandra_query_type { + CASSANDRA_QUERY_TYPE_READ, + CASSANDRA_QUERY_TYPE_READ_MORE, + CASSANDRA_QUERY_TYPE_WRITE, + CASSANDRA_QUERY_TYPE_DELETE, + + CASSANDRA_QUERY_TYPE_COUNT +}; + +static const char *cassandra_query_type_names[CASSANDRA_QUERY_TYPE_COUNT] = { + "read", "read-more", "write", "delete" +}; + +struct cassandra_callback { + unsigned int id; + struct timeout *to; + CassFuture *future; + struct cassandra_db *db; + driver_cassandra_callback_t *callback; + void *context; +}; + +struct cassandra_db { + struct sql_db api; + + char *hosts, *keyspace, *user, *password; + CassConsistency read_consistency, write_consistency, delete_consistency; + CassConsistency read_fallback_consistency, write_fallback_consistency; + CassConsistency delete_fallback_consistency; + CassLogLevel log_level; + bool debug_queries; + bool latency_aware_routing; + bool init_ssl; + unsigned int protocol_version; + unsigned int num_threads; + unsigned int connect_timeout_msecs, request_timeout_msecs; + unsigned int warn_timeout_msecs; + unsigned int heartbeat_interval_secs, idle_timeout_secs; + unsigned int execution_retry_interval_msecs, execution_retry_times; + unsigned int page_size; + in_port_t port; + + CassCluster *cluster; + CassSession *session; + CassTimestampGen *timestamp_gen; + CassSsl *ssl; + + int fd_pipe[2]; + struct io *io_pipe; + ARRAY(struct cassandra_sql_prepared_statement *) pending_prepares; + ARRAY(struct cassandra_callback *) callbacks; + ARRAY(struct cassandra_result *) results; + unsigned int callback_ids; + + char *metrics_path; + char *ssl_ca_file; + char *ssl_cert_file; + char *ssl_private_key_file; + char *ssl_private_key_password; + CassSslVerifyFlags ssl_verify_flags; + + struct timeout *to_metrics; + uint64_t counters[CASSANDRA_COUNTER_COUNT]; + + struct timeval primary_query_last_sent[CASSANDRA_QUERY_TYPE_COUNT]; + time_t last_fallback_warning[CASSANDRA_QUERY_TYPE_COUNT]; + unsigned int fallback_failures[CASSANDRA_QUERY_TYPE_COUNT]; + + /* for synchronous queries: */ + struct ioloop *ioloop, *orig_ioloop; + struct sql_result *sync_result; + + char *error; +}; + +struct cassandra_result { + struct sql_result api; + CassStatement *statement; + const CassResult *result; + CassIterator *iterator; + char *log_query; + char *error; + CassConsistency consistency, fallback_consistency; + enum cassandra_query_type query_type; + struct timeval page0_start_time, start_time, finish_time; + unsigned int row_count, total_row_count, page_num; + cass_int64_t timestamp; + + pool_t row_pool; + ARRAY_TYPE(const_string) fields; + ARRAY(size_t) field_sizes; + + sql_query_callback_t *callback; + void *context; + + bool is_prepared:1; + bool query_sent:1; + bool finished:1; + bool paging_continues:1; +}; + +struct cassandra_transaction_context { + struct sql_transaction_context ctx; + int refcount; + + sql_commit_callback_t *callback; + void *context; + + struct cassandra_sql_statement *stmt; + char *query; + char *log_query; + cass_int64_t query_timestamp; + char *error; + + bool begin_succeeded:1; + bool begin_failed:1; + bool failed:1; +}; + +struct cassandra_sql_arg { + unsigned int column_idx; + + char *value_str; + const unsigned char *value_binary; + size_t value_binary_size; + int64_t value_int64; +}; + +struct cassandra_sql_statement { + struct sql_statement stmt; + + struct cassandra_sql_prepared_statement *prep; + CassStatement *cass_stmt; + + ARRAY(struct cassandra_sql_arg) pending_args; + cass_int64_t timestamp; + + struct cassandra_result *result; +}; + +struct cassandra_sql_prepared_statement { + struct sql_prepared_statement prep_stmt; + + /* NULL, until the prepare is asynchronously finished */ + const CassPrepared *prepared; + /* statements waiting for prepare to finish */ + ARRAY(struct cassandra_sql_statement *) pending_statements; + /* an error here will cause the prepare to be retried on the next + execution attempt. */ + char *error; + + bool pending; +}; + +extern const struct sql_db driver_cassandra_db; +extern const struct sql_result driver_cassandra_result; + +static struct { + CassConsistency consistency; + const char *name; +} cass_consistency_names[] = { + { CASS_CONSISTENCY_ANY, "any" }, + { CASS_CONSISTENCY_ONE, "one" }, + { CASS_CONSISTENCY_TWO, "two" }, + { CASS_CONSISTENCY_THREE, "three" }, + { CASS_CONSISTENCY_QUORUM, "quorum" }, + { CASS_CONSISTENCY_ALL, "all" }, + { CASS_CONSISTENCY_LOCAL_QUORUM, "local-quorum" }, + { CASS_CONSISTENCY_EACH_QUORUM, "each-quorum" }, + { CASS_CONSISTENCY_SERIAL, "serial" }, + { CASS_CONSISTENCY_LOCAL_SERIAL, "local-serial" }, + { CASS_CONSISTENCY_LOCAL_ONE, "local-one" } +}; + +static struct { + CassLogLevel log_level; + const char *name; +} cass_log_level_names[] = { + { CASS_LOG_CRITICAL, "critical" }, + { CASS_LOG_ERROR, "error" }, + { CASS_LOG_WARN, "warn" }, + { CASS_LOG_INFO, "info" }, + { CASS_LOG_DEBUG, "debug" }, + { CASS_LOG_TRACE, "trace" } +}; + +static struct event_category event_category_cassandra = { + .parent = &event_category_sql, + .name = "cassandra" +}; + +static pthread_t main_thread_id; +static bool main_thread_id_set; + +static void driver_cassandra_prepare_pending(struct cassandra_db *db); +static void +prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt); +static void driver_cassandra_result_send_query(struct cassandra_result *result); +static void driver_cassandra_send_queries(struct cassandra_db *db); +static void result_finish(struct cassandra_result *result); + +static void log_one_line(const CassLogMessage *message, + enum log_type log_type, const char *log_level_str, + const char *text, size_t text_len) +{ + /* NOTE: We may not be in the main thread. We can't use the + standard Dovecot functions that may use data stack. That's why + we can't use i_log_type() in here, but have to re-implement the + internal logging protocol. Otherwise preserve Cassandra's own + logging format. */ + fprintf(stderr, "\001%c%s %u.%03u %s(%s:%d:%s): %.*s\n", + log_type+1, my_pid, + (unsigned int)(message->time_ms / 1000), + (unsigned int)(message->time_ms % 1000), + log_level_str, + message->file, message->line, message->function, + (int)text_len, text); +} + +static void +driver_cassandra_log_handler(const CassLogMessage* message, + void *data ATTR_UNUSED) +{ + enum log_type log_type = LOG_TYPE_ERROR; + const char *log_level_str = ""; + + switch (message->severity) { + case CASS_LOG_DISABLED: + case CASS_LOG_LAST_ENTRY: + i_unreached(); + case CASS_LOG_CRITICAL: + log_type = LOG_TYPE_PANIC; + break; + case CASS_LOG_ERROR: + log_type = LOG_TYPE_ERROR; + break; + case CASS_LOG_WARN: + log_type = LOG_TYPE_WARNING; + break; + case CASS_LOG_INFO: + log_type = LOG_TYPE_INFO; + break; + case CASS_LOG_TRACE: + log_level_str = "[TRACE] "; + /* fall through */ + case CASS_LOG_DEBUG: + log_type = LOG_TYPE_DEBUG; + break; + } + + /* Log message may contain LFs, so log each line separately. */ + const char *p, *line = message->message; + while ((p = strchr(line, '\n')) != NULL) { + log_one_line(message, log_type, log_level_str, line, p - line); + line = p+1; + } + log_one_line(message, log_type, log_level_str, line, strlen(line)); +} + +static void driver_cassandra_init_log(void) +{ + failure_callback_t *fatal_callback, *error_callback; + failure_callback_t *info_callback, *debug_callback; + + i_get_failure_handlers(&fatal_callback, &error_callback, + &info_callback, &debug_callback); + if (i_failure_handler_is_internal(debug_callback)) { + /* Using internal logging protocol. Use it ourself to set log + levels correctly. */ + cass_log_set_callback(driver_cassandra_log_handler, NULL); + } +} + +static int consistency_parse(const char *str, CassConsistency *consistency_r) +{ + unsigned int i; + + for (i = 0; i < N_ELEMENTS(cass_consistency_names); i++) { + if (strcmp(cass_consistency_names[i].name, str) == 0) { + *consistency_r = cass_consistency_names[i].consistency; + return 0; + } + } + return -1; +} + +static int log_level_parse(const char *str, CassLogLevel *log_level_r) +{ + unsigned int i; + + for (i = 0; i < N_ELEMENTS(cass_log_level_names); i++) { + if (strcmp(cass_log_level_names[i].name, str) == 0) { + *log_level_r = cass_log_level_names[i].log_level; + return 0; + } + } + return -1; +} + +static void driver_cassandra_set_state(struct cassandra_db *db, + enum sql_db_state state) +{ + /* switch back to original ioloop in case the caller wants to + add/remove timeouts */ + if (db->ioloop != NULL) + io_loop_set_current(db->orig_ioloop); + sql_db_set_state(&db->api, state); + if (db->ioloop != NULL) + io_loop_set_current(db->ioloop); +} + +static void driver_cassandra_close(struct cassandra_db *db, const char *error) +{ + struct cassandra_sql_prepared_statement *prep_stmt; + struct cassandra_result *const *resultp; + + io_remove(&db->io_pipe); + if (db->fd_pipe[0] != -1) { + i_close_fd(&db->fd_pipe[0]); + i_close_fd(&db->fd_pipe[1]); + } + driver_cassandra_set_state(db, SQL_DB_STATE_DISCONNECTED); + + array_foreach_elem(&db->pending_prepares, prep_stmt) { + prep_stmt->pending = FALSE; + prep_stmt->error = i_strdup(error); + prepare_finish_pending_statements(prep_stmt); + } + array_clear(&db->pending_prepares); + + while (array_count(&db->results) > 0) { + resultp = array_front(&db->results); + if ((*resultp)->error == NULL) + (*resultp)->error = i_strdup(error); + result_finish(*resultp); + } + + if (db->ioloop != NULL) { + /* running a sync query, stop it */ + io_loop_stop(db->ioloop); + } +} + +static void driver_cassandra_log_error(struct cassandra_db *db, + CassFuture *future, const char *str) +{ + const char *message; + size_t size; + + cass_future_error_message(future, &message, &size); + e_error(db->api.event, "%s: %.*s", str, (int)size, message); +} + +static struct cassandra_callback * +cassandra_callback_detach(struct cassandra_db *db, unsigned int id) +{ + struct cassandra_callback *cb, *const *cbp; + + /* usually there are only a few callbacks, so don't bother with using + a hash table */ + array_foreach(&db->callbacks, cbp) { + cb = *cbp; + if (cb->id == id) { + array_delete(&db->callbacks, + array_foreach_idx(&db->callbacks, cbp), 1); + return cb; + } + } + return NULL; +} + +static void cassandra_callback_run(struct cassandra_callback *cb) +{ + timeout_remove(&cb->to); + cb->callback(cb->future, cb->context); + cass_future_free(cb->future); + i_free(cb); +} + +static void driver_cassandra_future_callback(CassFuture *future ATTR_UNUSED, + void *context) +{ + struct cassandra_callback *cb = context; + + if (pthread_equal(pthread_self(), main_thread_id) != 0) { + /* called immediately from the main thread. */ + cassandra_callback_detach(cb->db, cb->id); + cb->to = timeout_add_short(0, cassandra_callback_run, cb); + return; + } + + /* this isn't the main thread - communicate with main thread by + writing the callback id to the pipe. note that we must not use + almost any dovecot functions here because most of them are using + data-stack, which isn't thread-safe. especially don't use + i_error() here. */ + if (write_full(cb->db->fd_pipe[1], &cb->id, sizeof(cb->id)) < 0) { + const char *str = t_strdup_printf( + "cassandra: write(pipe) failed: %s\n", + strerror(errno)); + (void)write_full(STDERR_FILENO, str, strlen(str)); + } +} + +static void driver_cassandra_input_id(struct cassandra_db *db, unsigned int id) +{ + struct cassandra_callback *cb; + + cb = cassandra_callback_detach(db, id); + if (cb == NULL) + i_panic("cassandra: Received unknown ID %u", id); + cassandra_callback_run(cb); +} + +static void driver_cassandra_input(struct cassandra_db *db) +{ + unsigned int ids[1024]; + ssize_t ret; + + ret = read(db->fd_pipe[0], ids, sizeof(ids)); + if (ret < 0) + e_error(db->api.event, "read(pipe) failed: %m"); + else if (ret == 0) + e_error(db->api.event, "read(pipe) failed: EOF"); + else if (ret % sizeof(ids[0]) != 0) + e_error(db->api.event, "read(pipe) returned wrong amount of data"); + else { + /* success */ + unsigned int i, count = ret / sizeof(ids[0]); + + for (i = 0; i < count && + db->api.state != SQL_DB_STATE_DISCONNECTED; i++) + driver_cassandra_input_id(db, ids[i]); + return; + } + driver_cassandra_close(db, "IPC pipe closed"); +} + +static void +driver_cassandra_set_callback(CassFuture *future, struct cassandra_db *db, + driver_cassandra_callback_t *callback, + void *context) +{ + struct cassandra_callback *cb; + + i_assert(callback != NULL); + + cb = i_new(struct cassandra_callback, 1); + cb->future = future; + cb->callback = callback; + cb->context = context; + cb->db = db; + + array_push_back(&db->callbacks, &cb); + cb->id = ++db->callback_ids; + if (cb->id == 0) + cb->id = ++db->callback_ids; + + /* NOTE: The callback may be called immediately by this same thread. + This is checked within the callback. It may also be called at any + time after this call by another thread. So we must not access "cb" + again after this call. */ + cass_future_set_callback(future, driver_cassandra_future_callback, cb); +} + +static void connect_callback(CassFuture *future, void *context) +{ + struct cassandra_db *db = context; + + if (cass_future_error_code(future) != CASS_OK) { + driver_cassandra_log_error(db, future, + "Couldn't connect to Cassandra"); + driver_cassandra_close(db, "Couldn't connect to Cassandra"); + return; + } + driver_cassandra_set_state(db, SQL_DB_STATE_IDLE); + if (db->ioloop != NULL) { + /* driver_cassandra_sync_init() waiting for connection to + finish */ + io_loop_stop(db->ioloop); + } + driver_cassandra_prepare_pending(db); + driver_cassandra_send_queries(db); +} + +static int driver_cassandra_connect(struct sql_db *_db) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + CassFuture *future; + + i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED); + + if (pipe(db->fd_pipe) < 0) { + e_error(_db->event, "pipe() failed: %m"); + return -1; + } + db->io_pipe = io_add(db->fd_pipe[0], IO_READ, + driver_cassandra_input, db); + driver_cassandra_set_state(db, SQL_DB_STATE_CONNECTING); + + future = cass_session_connect_keyspace(db->session, db->cluster, + db->keyspace); + driver_cassandra_set_callback(future, db, connect_callback, db); + return 0; +} + +static void driver_cassandra_disconnect(struct sql_db *_db) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + + driver_cassandra_close(db, "Disconnected"); +} + +static const char * +driver_cassandra_escape_string(struct sql_db *db ATTR_UNUSED, + const char *string) +{ + string_t *escaped; + unsigned int i; + + if (strchr(string, '\'') == NULL) + return string; + escaped = t_str_new(strlen(string)+10); + for (i = 0; string[i] != '\0'; i++) { + if (string[i] == '\'') + str_append_c(escaped, '\''); + str_append_c(escaped, string[i]); + } + return str_c(escaped); +} + +static int driver_cassandra_parse_connect_string(struct cassandra_db *db, + const char *connect_string, + const char **error_r) +{ + const char *const *args, *key, *value, *error; + string_t *hosts = t_str_new(64); + bool read_fallback_set = FALSE, write_fallback_set = FALSE; + bool delete_fallback_set = FALSE; + + db->log_level = CASS_LOG_WARN; + db->read_consistency = CASS_CONSISTENCY_LOCAL_QUORUM; + db->write_consistency = CASS_CONSISTENCY_LOCAL_QUORUM; + db->delete_consistency = CASS_CONSISTENCY_LOCAL_QUORUM; + db->connect_timeout_msecs = SQL_CONNECT_TIMEOUT_SECS*1000; + db->request_timeout_msecs = SQL_QUERY_TIMEOUT_SECS*1000; + db->warn_timeout_msecs = CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS; + + args = t_strsplit_spaces(connect_string, " "); + for (; *args != NULL; args++) { + value = strchr(*args, '='); + if (value == NULL) { + *error_r = t_strdup_printf( + "Missing value in connect string: %s", *args); + return -1; + } + key = t_strdup_until(*args, value++); + + if (str_begins(key, "ssl_")) + db->init_ssl = TRUE; + + if (strcmp(key, "host") == 0) { + if (str_len(hosts) > 0) + str_append_c(hosts, ','); + str_append(hosts, value); + } else if (strcmp(key, "port") == 0) { + if (net_str2port(value, &db->port) < 0) { + *error_r = t_strdup_printf( + "Invalid port: %s", value); + return -1; + } + } else if (strcmp(key, "dbname") == 0 || + strcmp(key, "keyspace") == 0) { + i_free(db->keyspace); + db->keyspace = i_strdup(value); + } else if (strcmp(key, "user") == 0) { + i_free(db->user); + db->user = i_strdup(value); + } else if (strcmp(key, "password") == 0) { + i_free(db->password); + db->password = i_strdup(value); + } else if (strcmp(key, "read_consistency") == 0) { + if (consistency_parse(value, &db->read_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown read_consistency: %s", value); + return -1; + } + } else if (strcmp(key, "read_fallback_consistency") == 0) { + if (consistency_parse(value, &db->read_fallback_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown read_fallback_consistency: %s", value); + return -1; + } + read_fallback_set = TRUE; + } else if (strcmp(key, "write_consistency") == 0) { + if (consistency_parse(value, + &db->write_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown write_consistency: %s", value); + return -1; + } + } else if (strcmp(key, "write_fallback_consistency") == 0) { + if (consistency_parse(value, + &db->write_fallback_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown write_fallback_consistency: %s", + value); + return -1; + } + write_fallback_set = TRUE; + } else if (strcmp(key, "delete_consistency") == 0) { + if (consistency_parse(value, + &db->delete_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown delete_consistency: %s", value); + return -1; + } + } else if (strcmp(key, "delete_fallback_consistency") == 0) { + if (consistency_parse(value, + &db->delete_fallback_consistency) < 0) { + *error_r = t_strdup_printf( + "Unknown delete_fallback_consistency: %s", + value); + return -1; + } + delete_fallback_set = TRUE; + } else if (strcmp(key, "log_level") == 0) { + if (log_level_parse(value, &db->log_level) < 0) { + *error_r = t_strdup_printf( + "Unknown log_level: %s", value); + return -1; + } + } else if (strcmp(key, "debug_queries") == 0) { + db->debug_queries = TRUE; + } else if (strcmp(key, "latency_aware_routing") == 0) { + db->latency_aware_routing = TRUE; + } else if (strcmp(key, "version") == 0) { + if (str_to_uint(value, &db->protocol_version) < 0) { + *error_r = t_strdup_printf( + "Invalid version: %s", value); + return -1; + } + } else if (strcmp(key, "num_threads") == 0) { + if (str_to_uint(value, &db->num_threads) < 0) { + *error_r = t_strdup_printf( + "Invalid num_threads: %s", value); + return -1; + } + } else if (strcmp(key, "heartbeat_interval") == 0) { + if (settings_get_time(value, &db->heartbeat_interval_secs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid heartbeat_interval '%s': %s", + value, error); + return -1; + } + } else if (strcmp(key, "idle_timeout") == 0) { + if (settings_get_time(value, &db->idle_timeout_secs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid idle_timeout '%s': %s", + value, error); + return -1; + } + } else if (strcmp(key, "connect_timeout") == 0) { + if (settings_get_time_msecs(value, + &db->connect_timeout_msecs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid connect_timeout '%s': %s", + value, error); + return -1; + } + } else if (strcmp(key, "request_timeout") == 0) { + if (settings_get_time_msecs(value, + &db->request_timeout_msecs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid request_timeout '%s': %s", + value, error); + return -1; + } + } else if (strcmp(key, "warn_timeout") == 0) { + if (settings_get_time_msecs(value, + &db->warn_timeout_msecs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid warn_timeout '%s': %s", + value, error); + return -1; + } + } else if (strcmp(key, "metrics") == 0) { + i_free(db->metrics_path); + db->metrics_path = i_strdup(value); + } else if (strcmp(key, "execution_retry_interval") == 0) { + if (settings_get_time_msecs(value, + &db->execution_retry_interval_msecs, + &error) < 0) { + *error_r = t_strdup_printf( + "Invalid execution_retry_interval '%s': %s", + value, error); + return -1; + } +#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY + *error_r = t_strdup_printf( + "This cassandra version does not support execution_retry_interval"); + return -1; +#endif + } else if (strcmp(key, "execution_retry_times") == 0) { + if (str_to_uint(value, &db->execution_retry_times) < 0) { + *error_r = t_strdup_printf( + "Invalid execution_retry_times %s", + value); + return -1; + } +#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY + *error_r = t_strdup_printf( + "This cassandra version does not support execution_retry_times"); + return -1; +#endif + } else if (strcmp(key, "page_size") == 0) { + if (str_to_uint(value, &db->page_size) < 0) { + *error_r = t_strdup_printf( + "Invalid page_size: %s", + value); + return -1; + } + } else if (strcmp(key, "ssl_ca") == 0) { + db->ssl_ca_file = i_strdup(value); + } else if (strcmp(key, "ssl_cert_file") == 0) { + db->ssl_cert_file = i_strdup(value); + } else if (strcmp(key, "ssl_private_key_file") == 0) { + db->ssl_private_key_file = i_strdup(value); + } else if (strcmp(key, "ssl_private_key_password") == 0) { + db->ssl_private_key_password = i_strdup(value); + } else if (strcmp(key, "ssl_verify") == 0) { + if (strcmp(value, "none") == 0) { + db->ssl_verify_flags = CASS_SSL_VERIFY_NONE; + } else if (strcmp(value, "cert") == 0) { + db->ssl_verify_flags = CASS_SSL_VERIFY_PEER_CERT; + } else if (strcmp(value, "cert-ip") == 0) { + db->ssl_verify_flags = + CASS_SSL_VERIFY_PEER_CERT | + CASS_SSL_VERIFY_PEER_IDENTITY; +#if HAVE_DECL_CASS_SSL_VERIFY_PEER_IDENTITY_DNS == 1 + } else if (strcmp(value, "cert-dns") == 0) { + db->ssl_verify_flags = + CASS_SSL_VERIFY_PEER_CERT | + CASS_SSL_VERIFY_PEER_IDENTITY_DNS; +#endif + } else { + *error_r = t_strdup_printf( + "Unsupported ssl_verify flags: '%s'", + value); + return -1; + } + } else { + *error_r = t_strdup_printf( + "Unknown connect string: %s", key); + return -1; + } + } + + if (!read_fallback_set) + db->read_fallback_consistency = db->read_consistency; + if (!write_fallback_set) + db->write_fallback_consistency = db->write_consistency; + if (!delete_fallback_set) + db->delete_fallback_consistency = db->delete_consistency; + + if (str_len(hosts) == 0) { + *error_r = t_strdup_printf("No hosts given in connect string"); + return -1; + } + if (db->keyspace == NULL) { + *error_r = t_strdup_printf("No dbname given in connect string"); + return -1; + } + + if ((db->ssl_cert_file != NULL && db->ssl_private_key_file == NULL) || + (db->ssl_cert_file == NULL && db->ssl_private_key_file != NULL)) { + *error_r = "ssl_cert_file and ssl_private_key_file need to be both set"; + return -1; + } + + db->hosts = i_strdup(str_c(hosts)); + return 0; +} + +static void +driver_cassandra_get_metrics_json(struct cassandra_db *db, string_t *dest) +{ +#define ADD_UINT64(_struct, _field) \ + str_printfa(dest, "\""#_field"\": %llu,", \ + (unsigned long long)metrics._struct._field); +#define ADD_DOUBLE(_struct, _field) \ + str_printfa(dest, "\""#_field"\": %02lf,", metrics._struct._field); + CassMetrics metrics; + + cass_session_get_metrics(db->session, &metrics); + str_append(dest, "{ \"requests\": {"); + ADD_UINT64(requests, min); + ADD_UINT64(requests, max); + ADD_UINT64(requests, mean); + ADD_UINT64(requests, stddev); + ADD_UINT64(requests, median); + ADD_UINT64(requests, percentile_75th); + ADD_UINT64(requests, percentile_95th); + ADD_UINT64(requests, percentile_98th); + ADD_UINT64(requests, percentile_99th); + ADD_UINT64(requests, percentile_999th); + ADD_DOUBLE(requests, mean_rate); + ADD_DOUBLE(requests, one_minute_rate); + ADD_DOUBLE(requests, five_minute_rate); + ADD_DOUBLE(requests, fifteen_minute_rate); + str_truncate(dest, str_len(dest)-1); + + str_append(dest, "}, \"stats\": {"); + ADD_UINT64(stats, total_connections); + ADD_UINT64(stats, available_connections); + ADD_UINT64(stats, exceeded_pending_requests_water_mark); + ADD_UINT64(stats, exceeded_write_bytes_water_mark); + str_truncate(dest, str_len(dest)-1); + + str_append(dest, "}, \"errors\": {"); + ADD_UINT64(errors, connection_timeouts); + ADD_UINT64(errors, pending_request_timeouts); + ADD_UINT64(errors, request_timeouts); + str_truncate(dest, str_len(dest)-1); + + str_append(dest, "}, \"queries\": {"); + for (unsigned int i = 0; i < CASSANDRA_COUNTER_COUNT; i++) { + str_printfa(dest, "\"%s\": %"PRIu64",", counter_names[i], + db->counters[i]); + } + str_truncate(dest, str_len(dest)-1); + str_append(dest, "}}"); +} + +static void driver_cassandra_metrics_write(struct cassandra_db *db) +{ + struct var_expand_table tab[] = { + { '\0', NULL, NULL } + }; + string_t *path = t_str_new(64); + string_t *data; + const char *error; + int fd; + + if (var_expand(path, db->metrics_path, tab, &error) <= 0) { + e_error(db->api.event, "Failed to expand metrics_path=%s: %s", + db->metrics_path, error); + return; + } + + fd = open(str_c(path), O_WRONLY | O_CREAT | O_TRUNC | O_NONBLOCK, 0600); + if (fd == -1) { + e_error(db->api.event, "creat(%s) failed: %m", str_c(path)); + return; + } + data = t_str_new(1024); + driver_cassandra_get_metrics_json(db, data); + if (write_full(fd, str_data(data), str_len(data)) < 0) + e_error(db->api.event, "write(%s) failed: %m", str_c(path)); + i_close_fd(&fd); +} + +static void driver_cassandra_free(struct cassandra_db **_db) +{ + struct cassandra_db *db = *_db; + *_db = NULL; + + event_unref(&db->api.event); + i_free(db->metrics_path); + i_free(db->hosts); + i_free(db->error); + i_free(db->keyspace); + i_free(db->user); + i_free(db->password); + i_free(db->ssl_ca_file); + i_free(db->ssl_cert_file); + i_free(db->ssl_private_key_file); + i_free_and_null(db->ssl_private_key_password); + array_free(&db->api.module_contexts); + if (db->ssl != NULL) + cass_ssl_free(db->ssl); + i_free(db); +} + +static int driver_cassandra_init_ssl(struct cassandra_db *db, const char **error_r) +{ + buffer_t *buf = t_buffer_create(512); + CassError c_err; + + db->ssl = cass_ssl_new(); + i_assert(db->ssl != NULL); + + if (db->ssl_ca_file != NULL) { + if (buffer_append_full_file(buf, db->ssl_ca_file, SIZE_MAX, + error_r) < 0) + return -1; + if ((c_err = cass_ssl_add_trusted_cert(db->ssl, str_c(buf))) != CASS_OK) { + *error_r = cass_error_desc(c_err); + return -1; + } + } + + if (db->ssl_private_key_file != NULL && db->ssl_cert_file != NULL) { + buffer_set_used_size(buf, 0); + if (buffer_append_full_file(buf, db->ssl_private_key_file, + SIZE_MAX, error_r) < 0) + return -1; + c_err = cass_ssl_set_private_key(db->ssl, str_c(buf), + db->ssl_private_key_password); + safe_memset(buffer_get_modifiable_data(buf, NULL), 0, buf->used); + if (c_err != CASS_OK) { + *error_r = cass_error_desc(c_err); + return -1; + } + + buffer_set_used_size(buf, 0); + if (buffer_append_full_file(buf, db->ssl_cert_file, SIZE_MAX, error_r) < 0) + return -1; + if ((c_err = cass_ssl_set_cert(db->ssl, str_c(buf))) != CASS_OK) { + *error_r = cass_error_desc(c_err); + return -1; + } + } + + cass_ssl_set_verify_flags(db->ssl, db->ssl_verify_flags); + + return 0; +} + +static int driver_cassandra_init_full_v(const struct sql_settings *set, + struct sql_db **db_r, + const char **error_r) +{ + struct cassandra_db *db; + int ret; + + db = i_new(struct cassandra_db, 1); + db->api = driver_cassandra_db; + db->fd_pipe[0] = db->fd_pipe[1] = -1; + db->api.event = event_create(set->event_parent); + event_add_category(db->api.event, &event_category_cassandra); + event_set_append_log_prefix(db->api.event, "cassandra: "); + + T_BEGIN { + ret = driver_cassandra_parse_connect_string(db, + set->connect_string, error_r); + } T_END_PASS_STR_IF(ret < 0, error_r); + + if (ret < 0) { + driver_cassandra_free(&db); + return -1; + } + + if (db->init_ssl && driver_cassandra_init_ssl(db, error_r) < 0) { + driver_cassandra_free(&db); + return -1; + } + + driver_cassandra_init_log(); + cass_log_set_level(db->log_level); + if (db->log_level >= CASS_LOG_DEBUG) + event_set_forced_debug(db->api.event, TRUE); + + if (db->protocol_version > 0 && db->protocol_version < 4) { + /* binding with column indexes requires v4 */ + db->api.v.prepared_statement_init = NULL; + db->api.v.prepared_statement_deinit = NULL; + db->api.v.statement_init_prepared = NULL; + } + + db->timestamp_gen = cass_timestamp_gen_monotonic_new(); + db->cluster = cass_cluster_new(); + +#ifdef HAVE_CASS_CLUSTER_SET_USE_HOSTNAME_RESOLUTION + if ((db->ssl_verify_flags & CASS_SSL_VERIFY_PEER_IDENTITY_DNS) != 0) { + CassError c_err; + if ((c_err = cass_cluster_set_use_hostname_resolution( + db->cluster, cass_true)) != CASS_OK) { + *error_r = cass_error_desc(c_err); + driver_cassandra_free(&db); + return -1; + } + } +#endif + cass_cluster_set_ssl(db->cluster, db->ssl); + cass_cluster_set_timestamp_gen(db->cluster, db->timestamp_gen); + cass_cluster_set_connect_timeout(db->cluster, db->connect_timeout_msecs); + cass_cluster_set_request_timeout(db->cluster, db->request_timeout_msecs); + cass_cluster_set_contact_points(db->cluster, db->hosts); + if (db->user != NULL && db->password != NULL) + cass_cluster_set_credentials(db->cluster, db->user, db->password); + if (db->port != 0) + cass_cluster_set_port(db->cluster, db->port); + if (db->protocol_version != 0) + cass_cluster_set_protocol_version(db->cluster, db->protocol_version); + if (db->num_threads != 0) + cass_cluster_set_num_threads_io(db->cluster, db->num_threads); + if (db->latency_aware_routing) + cass_cluster_set_latency_aware_routing(db->cluster, cass_true); + if (db->heartbeat_interval_secs != 0) + cass_cluster_set_connection_heartbeat_interval(db->cluster, + db->heartbeat_interval_secs); + if (db->idle_timeout_secs != 0) + cass_cluster_set_connection_idle_timeout(db->cluster, + db->idle_timeout_secs); +#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY + if (db->execution_retry_times > 0 && db->execution_retry_interval_msecs > 0) + cass_cluster_set_constant_speculative_execution_policy( + db->cluster, db->execution_retry_interval_msecs, + db->execution_retry_times); +#endif + if (db->ssl != NULL) { + e_debug(db->api.event, "Enabling TLS for cluster"); + cass_cluster_set_ssl(db->cluster, db->ssl); + } + db->session = cass_session_new(); + if (db->metrics_path != NULL) + db->to_metrics = timeout_add(1000, driver_cassandra_metrics_write, + db); + i_array_init(&db->results, 16); + i_array_init(&db->callbacks, 16); + i_array_init(&db->pending_prepares, 16); + if (!main_thread_id_set) { + main_thread_id = pthread_self(); + main_thread_id_set = TRUE; + } + + *db_r = &db->api; + return 0; +} + +static void driver_cassandra_deinit_v(struct sql_db *_db) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + + driver_cassandra_close(db, "Deinitialized"); + + i_assert(array_count(&db->callbacks) == 0); + array_free(&db->callbacks); + i_assert(array_count(&db->results) == 0); + array_free(&db->results); + i_assert(array_count(&db->pending_prepares) == 0); + array_free(&db->pending_prepares); + + cass_session_free(db->session); + cass_cluster_free(db->cluster); + cass_timestamp_gen_free(db->timestamp_gen); + timeout_remove(&db->to_metrics); + sql_connection_log_finished(_db); + driver_cassandra_free(&db); +} + +static void driver_cassandra_result_unlink(struct cassandra_db *db, + struct cassandra_result *result) +{ + struct cassandra_result *const *results; + unsigned int i, count; + + results = array_get(&db->results, &count); + for (i = 0; i < count; i++) { + if (results[i] == result) { + array_delete(&db->results, i, 1); + return; + } + } + i_unreached(); +} + +static void driver_cassandra_log_result(struct cassandra_result *result, + bool all_pages, long long reply_usecs) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + struct timeval now; + unsigned int row_count; + + i_gettimeofday(&now); + + string_t *str = t_str_new(128); + str_printfa(str, "Finished %squery '%s' (", + result->is_prepared ? "prepared " : "", result->log_query); + if (result->timestamp != 0) + str_printfa(str, "timestamp=%"PRId64", ", result->timestamp); + if (all_pages) { + str_printfa(str, "%u pages in total, ", result->page_num); + row_count = result->total_row_count; + } else { + if (result->page_num > 0 || result->paging_continues) + str_printfa(str, "page %u, ", result->page_num); + row_count = result->row_count; + } + str_printfa(str, "%u rows, %lld+%lld us): %s", row_count, reply_usecs, + timeval_diff_usecs(&now, &result->finish_time), + result->error != NULL ? result->error : "success"); + + struct event_passthrough *e = + sql_query_finished_event(&db->api, result->api.event, + result->log_query, result->error == NULL, + NULL); + if (result->error != NULL) + e->add_str("error", result->error); + + struct event *event = e->event(); + if (db->debug_queries) + event_set_forced_debug(event, TRUE); + if (reply_usecs/1000 >= db->warn_timeout_msecs) { + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SLOW]++; + e_warning(event, "%s", str_c(str)); + } else { + e_debug(event, "%s", str_c(str)); + } +} + +static void driver_cassandra_result_free(struct sql_result *_result) +{ + struct cassandra_db *db = (struct cassandra_db *)_result->db; + struct cassandra_result *result = (struct cassandra_result *)_result; + long long reply_usecs; + + i_assert(!result->api.callback); + i_assert(result->callback == NULL); + + if (_result == db->sync_result) + db->sync_result = NULL; + + reply_usecs = timeval_diff_usecs(&result->finish_time, + &result->start_time); + driver_cassandra_log_result(result, FALSE, reply_usecs); + + if (result->page_num > 0 && !result->paging_continues) { + /* Multi-page query finishes now. Log a debug/warning summary + message about it separate from the per-page messages. */ + reply_usecs = timeval_diff_usecs(&result->finish_time, + &result->page0_start_time); + driver_cassandra_log_result(result, TRUE, reply_usecs); + } + + if (result->result != NULL) + cass_result_free(result->result); + if (result->iterator != NULL) + cass_iterator_free(result->iterator); + if (result->statement != NULL) + cass_statement_free(result->statement); + pool_unref(&result->row_pool); + event_unref(&result->api.event); + i_free(result->log_query); + i_free(result->error); + i_free(result); +} + +static void result_finish(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + bool free_result = TRUE; + + result->finished = TRUE; + result->finish_time = ioloop_timeval; + driver_cassandra_result_unlink(db, result); + + i_assert((result->error != NULL) == (result->iterator == NULL)); + + result->api.callback = TRUE; + T_BEGIN { + result->callback(&result->api, result->context); + } T_END; + result->api.callback = FALSE; + + free_result = db->sync_result != &result->api; + if (db->ioloop != NULL) + io_loop_stop(db->ioloop); + + i_assert(!free_result || result->api.refcount > 0); + result->callback = NULL; + if (free_result) + sql_result_unref(&result->api); +} + +static void query_resend_with_fallback(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + time_t last_warning = + ioloop_time - db->last_fallback_warning[result->query_type]; + + if (last_warning >= CASSANDRA_FALLBACK_WARN_INTERVAL_SECS) { + e_warning(db->api.event, + "%s - retrying future %s queries with consistency %s (instead of %s)", + result->error, cassandra_query_type_names[result->query_type], + cass_consistency_string(result->fallback_consistency), + cass_consistency_string(result->consistency)); + db->last_fallback_warning[result->query_type] = ioloop_time; + } + i_free_and_null(result->error); + db->fallback_failures[result->query_type]++; + + result->consistency = result->fallback_consistency; + driver_cassandra_result_send_query(result); +} + +static void counters_inc_error(struct cassandra_db *db, CassError error) +{ + switch (error) { + case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS]++; + break; + case CASS_ERROR_LIB_REQUEST_QUEUE_FULL: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL]++; + break; + case CASS_ERROR_LIB_REQUEST_TIMED_OUT: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT]++; + break; + case CASS_ERROR_SERVER_WRITE_TIMEOUT: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT]++; + break; + case CASS_ERROR_SERVER_UNAVAILABLE: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE]++; + break; + default: + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER]++; + break; + } +} + +static bool query_error_want_fallback(CassError error) +{ + switch (error) { + case CASS_ERROR_LIB_WRITE_ERROR: + case CASS_ERROR_LIB_REQUEST_TIMED_OUT: + /* Communication problems on client side. Maybe it will work + with fallback consistency? */ + return TRUE; + case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE: + /* The client library couldn't connect to enough Cassandra + nodes. The error message text is the same as for + CASS_ERROR_SERVER_UNAVAILABLE. */ + return TRUE; + case CASS_ERROR_SERVER_SERVER_ERROR: + case CASS_ERROR_SERVER_OVERLOADED: + case CASS_ERROR_SERVER_IS_BOOTSTRAPPING: + case CASS_ERROR_SERVER_READ_TIMEOUT: + case CASS_ERROR_SERVER_READ_FAILURE: + case CASS_ERROR_SERVER_WRITE_FAILURE: + /* Servers are having trouble. Maybe with fallback consistency + we can reach non-troubled servers? */ + return TRUE; + case CASS_ERROR_SERVER_UNAVAILABLE: + /* Cassandra server knows that there aren't enough nodes + available. "All hosts in current policy attempted and were + either unavailable or failed". */ + return TRUE; + case CASS_ERROR_SERVER_WRITE_TIMEOUT: + /* Cassandra server couldn't reach all the needed nodes. + This may be because it hasn't yet detected that the servers + are down, or because the servers are just too busy. We'll + try the fallback consistency to avoid unnecessary temporary + errors. */ + return TRUE; + default: + return FALSE; + } +} + +static enum sql_result_error_type +driver_cassandra_error_is_uncertain(CassError error) +{ + switch (error) { + case CASS_ERROR_SERVER_WRITE_FAILURE: + /* This happens when some of the replicas that were contacted + * by the coordinator replied with an error. */ + case CASS_ERROR_SERVER_WRITE_TIMEOUT: + /* A Cassandra timeout during a write query. */ + case CASS_ERROR_SERVER_UNAVAILABLE: + /* The coordinator knows there are not enough replicas alive + * to perform a query with the requested consistency level. */ + case CASS_ERROR_LIB_REQUEST_TIMED_OUT: + /* A request sent from the driver has timed out. */ + case CASS_ERROR_LIB_WRITE_ERROR: + /* A write error occured. */ + return SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN; + default: + return SQL_RESULT_ERROR_TYPE_UNKNOWN; + } +} + +static void query_callback(CassFuture *future, void *context) +{ + struct cassandra_result *result = context; + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + CassError error = cass_future_error_code(future); + + if (error != CASS_OK) { + const char *errmsg; + size_t errsize; + int msecs; + + cass_future_error_message(future, &errmsg, &errsize); + i_free(result->error); + + msecs = timeval_diff_msecs(&ioloop_timeval, &result->start_time); + counters_inc_error(db, error); + /* Timeouts bring uncertainty whether the query succeeded or + not. Also _SERVER_UNAVAILABLE could have actually written + enough copies of the data for the query to succeed. */ + result->api.error_type = driver_cassandra_error_is_uncertain(error); + result->error = i_strdup_printf( + "Query '%s' failed: %.*s (in %u.%03u secs%s)", + result->log_query, (int)errsize, errmsg, msecs/1000, msecs%1000, + result->page_num == 0 ? + "" : + t_strdup_printf(", page %u", result->page_num)); + + if (query_error_want_fallback(error) && + result->fallback_consistency != result->consistency) { + /* retry with fallback consistency */ + query_resend_with_fallback(result); + return; + } + result_finish(result); + return; + } + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK]++; + + if (result->fallback_consistency != result->consistency) { + /* non-fallback query finished successfully. if there had been + any fallbacks, reset them. */ + db->fallback_failures[result->query_type] = 0; + } + + result->result = cass_future_get_result(future); + result->iterator = cass_iterator_from_result(result->result); + result_finish(result); +} + +static void driver_cassandra_init_statement(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + + cass_statement_set_consistency(result->statement, result->consistency); + +#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY + cass_statement_set_is_idempotent(result->statement, cass_true); +#endif + if (db->page_size > 0) + cass_statement_set_paging_size(result->statement, db->page_size); +} + +static void driver_cassandra_result_send_query(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + CassFuture *future; + + i_assert(result->statement != NULL); + + db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SENT]++; + if (result->query_type != CASSANDRA_QUERY_TYPE_READ_MORE) + driver_cassandra_init_statement(result); + + future = cass_session_execute(db->session, result->statement); + driver_cassandra_set_callback(future, db, query_callback, result); +} + +static bool +driver_cassandra_want_fallback_query(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + unsigned int failure_count = db->fallback_failures[result->query_type]; + unsigned int i, msecs = CASSANDRA_FALLBACK_FIRST_RETRY_MSECS; + struct timeval tv; + + if (failure_count == 0) + return FALSE; + /* double the retries every time. */ + for (i = 1; i < failure_count; i++) { + msecs *= 2; + if (msecs >= CASSANDRA_FALLBACK_MAX_RETRY_MSECS) { + msecs = CASSANDRA_FALLBACK_MAX_RETRY_MSECS; + break; + } + } + /* If last primary query sent timestamp + msecs is older than current + time, we need to retry the primary query. Note that this practically + prevents multiple primary queries from being attempted + simultaneously, because the caller updates primary_query_last_sent + immediately when returning. + + The only time when multiple primary queries can be running in + parallel is when the earlier query is being slow and hasn't finished + early enough. This could even be a wanted feature, since while the + first query might have to wait for a timeout, Cassandra could have + been fixed in the meantime and the second query finishes + successfully. */ + tv = db->primary_query_last_sent[result->query_type]; + timeval_add_msecs(&tv, msecs); + return timeval_cmp(&ioloop_timeval, &tv) < 0; +} + +static int driver_cassandra_send_query(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + int ret; + + if (!SQL_DB_IS_READY(&db->api)) { + if ((ret = sql_connect(&db->api)) <= 0) { + if (ret < 0) + driver_cassandra_close(db, + "Couldn't connect to Cassandra"); + return ret; + } + } + + if (result->page0_start_time.tv_sec == 0) + result->page0_start_time = ioloop_timeval; + result->start_time = ioloop_timeval; + result->row_pool = pool_alloconly_create("cassandra result", 512); + switch (result->query_type) { + case CASSANDRA_QUERY_TYPE_READ: + result->consistency = db->read_consistency; + result->fallback_consistency = db->read_fallback_consistency; + break; + case CASSANDRA_QUERY_TYPE_READ_MORE: + /* consistency is already set and we don't want to fallback + at this point anymore. */ + result->fallback_consistency = result->consistency; + break; + case CASSANDRA_QUERY_TYPE_WRITE: + result->consistency = db->write_consistency; + result->fallback_consistency = db->write_fallback_consistency; + break; + case CASSANDRA_QUERY_TYPE_DELETE: + result->consistency = db->delete_consistency; + result->fallback_consistency = db->delete_fallback_consistency; + break; + case CASSANDRA_QUERY_TYPE_COUNT: + i_unreached(); + } + + if (driver_cassandra_want_fallback_query(result)) + result->consistency = result->fallback_consistency; + else + db->primary_query_last_sent[result->query_type] = ioloop_timeval; + + driver_cassandra_result_send_query(result); + result->query_sent = TRUE; + return 1; +} + +static void driver_cassandra_send_queries(struct cassandra_db *db) +{ + struct cassandra_result *const *results; + unsigned int i, count; + + results = array_get(&db->results, &count); + for (i = 0; i < count; i++) { + if (!results[i]->query_sent && results[i]->statement != NULL) { + if (driver_cassandra_send_query(results[i]) <= 0) + break; + } + } +} + +static void exec_callback(struct sql_result *_result ATTR_UNUSED, + void *context ATTR_UNUSED) +{ +} + +static struct cassandra_result * +driver_cassandra_query_init(struct cassandra_db *db, const char *log_query, + enum cassandra_query_type query_type, + bool is_prepared, + sql_query_callback_t *callback, void *context) +{ + struct cassandra_result *result; + + result = i_new(struct cassandra_result, 1); + result->api = driver_cassandra_result; + result->api.db = &db->api; + result->api.refcount = 1; + result->callback = callback; + result->context = context; + result->query_type = query_type; + result->log_query = i_strdup(log_query); + result->is_prepared = is_prepared; + result->api.event = event_create(db->api.event); + array_push_back(&db->results, &result); + return result; +} + +static void +driver_cassandra_query_full(struct sql_db *_db, const char *query, + enum cassandra_query_type query_type, + sql_query_callback_t *callback, void *context) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + struct cassandra_result *result; + + result = driver_cassandra_query_init(db, query, query_type, FALSE, + callback, context); + result->statement = cass_statement_new(query, 0); + (void)driver_cassandra_send_query(result); +} + +static void driver_cassandra_exec(struct sql_db *db, const char *query) +{ + driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_WRITE, + exec_callback, NULL); +} + +static void driver_cassandra_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) +{ + driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_READ, + callback, context); +} + +static void cassandra_query_s_callback(struct sql_result *result, void *context) +{ + struct cassandra_db *db = context; + + db->sync_result = result; +} + +static void driver_cassandra_sync_init(struct cassandra_db *db) +{ + if (sql_connect(&db->api) < 0) + return; + db->orig_ioloop = current_ioloop; + db->ioloop = io_loop_create(); + if (IS_CONNECTED(db)) + return; + i_assert(db->api.state == SQL_DB_STATE_CONNECTING); + + db->io_pipe = io_loop_move_io(&db->io_pipe); + /* wait for connecting to finish */ + io_loop_run(db->ioloop); +} + +static void driver_cassandra_sync_deinit(struct cassandra_db *db) +{ + if (db->orig_ioloop == NULL) + return; + if (db->io_pipe != NULL) { + io_loop_set_current(db->orig_ioloop); + db->io_pipe = io_loop_move_io(&db->io_pipe); + io_loop_set_current(db->ioloop); + } + io_loop_destroy(&db->ioloop); +} + +static struct sql_result * +driver_cassandra_sync_query(struct cassandra_db *db, const char *query, + enum cassandra_query_type query_type) +{ + struct sql_result *result; + + i_assert(db->sync_result == NULL); + + switch (db->api.state) { + case SQL_DB_STATE_CONNECTING: + case SQL_DB_STATE_BUSY: + i_unreached(); + case SQL_DB_STATE_DISCONNECTED: + sql_not_connected_result.refcount++; + return &sql_not_connected_result; + case SQL_DB_STATE_IDLE: + break; + } + + driver_cassandra_query_full(&db->api, query, query_type, + cassandra_query_s_callback, db); + if (db->sync_result == NULL) { + db->io_pipe = io_loop_move_io(&db->io_pipe); + io_loop_run(db->ioloop); + } + + result = db->sync_result; + if (result == &sql_not_connected_result) { + /* we don't end up in cassandra's free function, so sync_result + won't be set to NULL if we don't do it here. */ + db->sync_result = NULL; + } else if (result == NULL) { + result = &sql_not_connected_result; + result->refcount++; + } + return result; +} + +static struct sql_result * +driver_cassandra_query_s(struct sql_db *_db, const char *query) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + struct sql_result *result; + + driver_cassandra_sync_init(db); + result = driver_cassandra_sync_query(db, query, + CASSANDRA_QUERY_TYPE_READ); + driver_cassandra_sync_deinit(db); + return result; +} + +static int +driver_cassandra_get_value(struct cassandra_result *result, + const CassValue *value, const char **str_r, + size_t *len_r) +{ + const unsigned char *output; + void *output_dup; + size_t output_size; + CassError rc; + const char *type; + + if (cass_value_is_null(value) != 0) { + *str_r = NULL; + *len_r = 0; + return 0; + } + + switch (cass_data_type_type(cass_value_data_type(value))) { + case CASS_VALUE_TYPE_INT: { + cass_int32_t num; + + rc = cass_value_get_int32(value, &num); + if (rc == CASS_OK) { + const char *str = t_strdup_printf("%d", num); + output_size = strlen(str); + output = (const void *)str; + } + type = "int32"; + break; + } + case CASS_VALUE_TYPE_TIMESTAMP: + case CASS_VALUE_TYPE_BIGINT: { + cass_int64_t num; + + rc = cass_value_get_int64(value, &num); + if (rc == CASS_OK) { + const char *str = t_strdup_printf("%lld", (long long)num); + output_size = strlen(str); + output = (const void *)str; + } + type = "int64"; + break; + } + default: + rc = cass_value_get_bytes(value, &output, &output_size); + type = "bytes"; + break; + } + if (rc != CASS_OK) { + i_free(result->error); + result->error = i_strdup_printf("Couldn't get value as %s: %s", + type, cass_error_desc(rc)); + return -1; + } + output_dup = p_malloc(result->row_pool, output_size + 1); + memcpy(output_dup, output, output_size); + *str_r = output_dup; + *len_r = output_size; + return 0; +} + +static int driver_cassandra_result_next_page(struct cassandra_result *result) +{ + struct cassandra_db *db = (struct cassandra_db *)result->api.db; + + if (db->page_size == 0) { + /* no paging */ + return 0; + } + if (cass_result_has_more_pages(result->result) == cass_false) + return 0; + + /* callers that don't support sql_query_more() will still get a useful + error message. */ + i_free(result->error); + result->error = i_strdup( + "Paged query has more results, but not supported by the caller"); + return SQL_RESULT_NEXT_MORE; +} + +static int driver_cassandra_result_next_row(struct sql_result *_result) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + const CassRow *row; + const CassValue *value; + const char *str; + size_t size; + unsigned int i; + int ret = 1; + + if (result->iterator == NULL) + return -1; + + if (cass_iterator_next(result->iterator) == 0) + return driver_cassandra_result_next_page(result); + result->row_count++; + result->total_row_count++; + + p_clear(result->row_pool); + p_array_init(&result->fields, result->row_pool, 8); + p_array_init(&result->field_sizes, result->row_pool, 8); + + row = cass_iterator_get_row(result->iterator); + for (i = 0; (value = cass_row_get_column(row, i)) != NULL; i++) { + if (driver_cassandra_get_value(result, value, &str, &size) < 0) { + ret = -1; + break; + } + array_push_back(&result->fields, &str); + array_push_back(&result->field_sizes, &size); + } + return ret; +} + +static void +driver_cassandra_result_more(struct sql_result **_result, bool async, + sql_query_callback_t *callback, void *context) +{ + struct cassandra_db *db = (struct cassandra_db *)(*_result)->db; + struct cassandra_result *new_result; + struct cassandra_result *old_result = + (struct cassandra_result *)*_result; + + /* Initialize the next page as a new sql_result */ + new_result = driver_cassandra_query_init(db, old_result->log_query, + CASSANDRA_QUERY_TYPE_READ_MORE, + old_result->is_prepared, + callback, context); + + /* Preserve the statement and update its paging state */ + new_result->statement = old_result->statement; + old_result->statement = NULL; + cass_statement_set_paging_state(new_result->statement, + old_result->result); + old_result->paging_continues = TRUE; + /* The caller did support paging. Clear out the "...not supported by + the caller" error text, so it won't be in the debug log output. */ + i_free_and_null(old_result->error); + + new_result->timestamp = old_result->timestamp; + new_result->consistency = old_result->consistency; + new_result->page_num = old_result->page_num + 1; + new_result->page0_start_time = old_result->page0_start_time; + new_result->total_row_count = old_result->total_row_count; + + sql_result_unref(*_result); + *_result = NULL; + + if (async) + (void)driver_cassandra_send_query(new_result); + else { + i_assert(db->api.state == SQL_DB_STATE_IDLE); + driver_cassandra_sync_init(db); + (void)driver_cassandra_send_query(new_result); + if (new_result->result == NULL) { + db->io_pipe = io_loop_move_io(&db->io_pipe); + io_loop_run(db->ioloop); + } + driver_cassandra_sync_deinit(db); + + callback(&new_result->api, context); + } +} + +static unsigned int +driver_cassandra_result_get_fields_count(struct sql_result *_result) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + + return array_count(&result->fields); +} + +static const char * +driver_cassandra_result_get_field_name(struct sql_result *_result ATTR_UNUSED, + unsigned int idx ATTR_UNUSED) +{ + i_unreached(); +} + +static int +driver_cassandra_result_find_field(struct sql_result *_result ATTR_UNUSED, + const char *field_name ATTR_UNUSED) +{ + i_unreached(); +} + +static const char * +driver_cassandra_result_get_field_value(struct sql_result *_result, + unsigned int idx) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + + return array_idx_elem(&result->fields, idx); +} + +static const unsigned char * +driver_cassandra_result_get_field_value_binary(struct sql_result *_result ATTR_UNUSED, + unsigned int idx ATTR_UNUSED, + size_t *size_r ATTR_UNUSED) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + const char *str; + const size_t *sizep; + + str = array_idx_elem(&result->fields, idx); + sizep = array_idx(&result->field_sizes, idx); + *size_r = *sizep; + return (const void *)str; +} + +static const char * +driver_cassandra_result_find_field_value(struct sql_result *result ATTR_UNUSED, + const char *field_name ATTR_UNUSED) +{ + i_unreached(); +} + +static const char *const * +driver_cassandra_result_get_values(struct sql_result *_result) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + + return array_front(&result->fields); +} + +static const char *driver_cassandra_result_get_error(struct sql_result *_result) +{ + struct cassandra_result *result = (struct cassandra_result *)_result; + + if (result->error != NULL) + return result->error; + return "FIXME"; +} + +static struct sql_transaction_context * +driver_cassandra_transaction_begin(struct sql_db *db) +{ + struct cassandra_transaction_context *ctx; + + ctx = i_new(struct cassandra_transaction_context, 1); + ctx->ctx.db = db; + ctx->ctx.event = event_create(db->event); + ctx->refcount = 1; + return &ctx->ctx; +} + +static void +driver_cassandra_transaction_unref(struct cassandra_transaction_context **_ctx) +{ + struct cassandra_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + i_assert(ctx->refcount > 0); + if (--ctx->refcount > 0) + return; + + event_unref(&ctx->ctx.event); + i_free(ctx->log_query); + i_free(ctx->query); + i_free(ctx->error); + i_free(ctx); +} + +static void +transaction_set_failed(struct cassandra_transaction_context *ctx, + const char *error) +{ + if (ctx->failed) { + i_assert(ctx->error != NULL); + } else { + i_assert(ctx->error == NULL); + ctx->failed = TRUE; + ctx->error = i_strdup(error); + } +} + +static void +transaction_commit_callback(struct sql_result *result, void *context) +{ + struct cassandra_transaction_context *ctx = context; + struct sql_commit_result commit_result; + + i_zero(&commit_result); + if (sql_result_next_row(result) < 0) { + commit_result.error = sql_result_get_error(result); + commit_result.error_type = sql_result_get_error_type(result); + e_debug(sql_transaction_finished_event(&ctx->ctx)-> + add_str("error", commit_result.error)->event(), + "Transaction failed"); + } else { + e_debug(sql_transaction_finished_event(&ctx->ctx)->event(), + "Transaction committed"); + } + ctx->callback(&commit_result, ctx->context); + driver_cassandra_transaction_unref(&ctx); +} + +static void +driver_cassandra_transaction_commit(struct sql_transaction_context *_ctx, + sql_commit_callback_t *callback, void *context) +{ + struct cassandra_transaction_context *ctx = + (struct cassandra_transaction_context *)_ctx; + struct cassandra_db *db = (struct cassandra_db *)_ctx->db; + enum cassandra_query_type query_type; + struct sql_commit_result result; + + i_zero(&result); + ctx->callback = callback; + ctx->context = context; + + if (ctx->failed || (ctx->query == NULL && ctx->stmt == NULL)) { + if (ctx->failed) + result.error = ctx->error; + + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", "Rolled back")->event(), + "Transaction rolled back"); + callback(&result, context); + driver_cassandra_transaction_unref(&ctx); + return; + } + + /* just a single query, send it */ + const char *query = ctx->query != NULL ? + ctx->query : sql_statement_get_query(&ctx->stmt->stmt); + if (strncasecmp(query, "DELETE ", 7) == 0) + query_type = CASSANDRA_QUERY_TYPE_DELETE; + else + query_type = CASSANDRA_QUERY_TYPE_WRITE; + + if (ctx->query != NULL) { + struct cassandra_result *cass_result; + + cass_result = driver_cassandra_query_init(db, ctx->log_query, + query_type, FALSE, transaction_commit_callback, ctx); + cass_result->statement = cass_statement_new(query, 0); + if (ctx->query_timestamp != 0) { + cass_result->timestamp = ctx->query_timestamp; + cass_statement_set_timestamp(cass_result->statement, + ctx->query_timestamp); + } + (void)driver_cassandra_send_query(cass_result); + } else { + ctx->stmt->result = + driver_cassandra_query_init(db, + sql_statement_get_log_query(&ctx->stmt->stmt), + query_type, TRUE, transaction_commit_callback, + ctx); + if (ctx->stmt->cass_stmt == NULL) { + /* wait for prepare to finish */ + } else { + ctx->stmt->result->statement = ctx->stmt->cass_stmt; + ctx->stmt->result->timestamp = ctx->stmt->timestamp; + (void)driver_cassandra_send_query(ctx->stmt->result); + pool_unref(&ctx->stmt->stmt.pool); + } + } +} + +static void +driver_cassandra_try_commit_s(struct cassandra_transaction_context *ctx) +{ + struct sql_transaction_context *_ctx = &ctx->ctx; + struct cassandra_db *db = (struct cassandra_db *)_ctx->db; + struct sql_result *result = NULL; + enum cassandra_query_type query_type; + + /* just a single query, send it */ + if (strncasecmp(ctx->query, "DELETE ", 7) == 0) + query_type = CASSANDRA_QUERY_TYPE_DELETE; + else + query_type = CASSANDRA_QUERY_TYPE_WRITE; + driver_cassandra_sync_init(db); + result = driver_cassandra_sync_query(db, ctx->query, query_type); + driver_cassandra_sync_deinit(db); + + if (sql_result_next_row(result) < 0) + transaction_set_failed(ctx, sql_result_get_error(result)); + sql_result_unref(result); +} + +static int +driver_cassandra_transaction_commit_s(struct sql_transaction_context *_ctx, + const char **error_r) +{ + struct cassandra_transaction_context *ctx = + (struct cassandra_transaction_context *)_ctx; + + if (ctx->stmt != NULL) { + /* nothing should be using this - don't bother implementing */ + i_panic("cassandra: sql_transaction_commit_s() not supported for prepared statements"); + } + + if (ctx->query != NULL && !ctx->failed) + driver_cassandra_try_commit_s(ctx); + *error_r = t_strdup(ctx->error); + + i_assert(ctx->refcount == 1); + i_assert((*error_r != NULL) == ctx->failed); + driver_cassandra_transaction_unref(&ctx); + return *error_r == NULL ? 0 : -1; +} + +static void +driver_cassandra_transaction_rollback(struct sql_transaction_context *_ctx) +{ + struct cassandra_transaction_context *ctx = + (struct cassandra_transaction_context *)_ctx; + + i_assert(ctx->refcount == 1); + driver_cassandra_transaction_unref(&ctx); +} + +static void +driver_cassandra_update(struct sql_transaction_context *_ctx, const char *query, + unsigned int *affected_rows) +{ + struct cassandra_transaction_context *ctx = + (struct cassandra_transaction_context *)_ctx; + + i_assert(affected_rows == NULL); + + if (ctx->query != NULL || ctx->stmt != NULL) { + transaction_set_failed(ctx, "Multiple changes in transaction not supported"); + return; + } + ctx->query = i_strdup(query); + /* When log_query is set here it can contain expanded values even + if stmt->no_log_expanded_values is set. */ + ctx->log_query = i_strdup(query); +} + +static const char * +driver_cassandra_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + string_t *str = t_str_new(128); + + str_append(str, "0x"); + binary_to_hex_append(str, data, size); + return str_c(str); +} + +static CassError +driver_cassandra_bind_int(struct cassandra_sql_statement *stmt, + unsigned int column_idx, int64_t value) +{ + const CassDataType *data_type; + CassValueType value_type; + + i_assert(stmt->prep != NULL); + + /* statements require exactly correct value type */ + data_type = cass_prepared_parameter_data_type(stmt->prep->prepared, + column_idx); + value_type = cass_data_type_type(data_type); + + switch (value_type) { + case CASS_VALUE_TYPE_INT: + if (value < INT32_MIN || value > INT32_MAX) + return CASS_ERROR_LIB_INVALID_VALUE_TYPE; + return cass_statement_bind_int32(stmt->cass_stmt, column_idx, + value); + case CASS_VALUE_TYPE_TIMESTAMP: + case CASS_VALUE_TYPE_BIGINT: + return cass_statement_bind_int64(stmt->cass_stmt, column_idx, + value); + case CASS_VALUE_TYPE_SMALL_INT: + if (value < INT16_MIN || value > INT16_MAX) + return CASS_ERROR_LIB_INVALID_VALUE_TYPE; + return cass_statement_bind_int16(stmt->cass_stmt, column_idx, + value); + case CASS_VALUE_TYPE_TINY_INT: + if (value < INT8_MIN || value > INT8_MAX) + return CASS_ERROR_LIB_INVALID_VALUE_TYPE; + return cass_statement_bind_int8(stmt->cass_stmt, column_idx, + value); + default: + return CASS_ERROR_LIB_INVALID_VALUE_TYPE; + } +} + +static void prepare_finish_arg(struct cassandra_sql_statement *stmt, + const struct cassandra_sql_arg *arg) +{ + CassError rc; + + if (arg->value_str != NULL) { + rc = cass_statement_bind_string(stmt->cass_stmt, arg->column_idx, + arg->value_str); + } else if (arg->value_binary != NULL) { + rc = cass_statement_bind_bytes(stmt->cass_stmt, arg->column_idx, + arg->value_binary, + arg->value_binary_size); + } else { + rc = driver_cassandra_bind_int(stmt, arg->column_idx, + arg->value_int64); + } + if (rc != CASS_OK) { + e_error(stmt->stmt.db->event, + "Statement '%s': Failed to bind column %u: %s", + stmt->stmt.query_template, arg->column_idx, + cass_error_desc(rc)); + } +} + +static void prepare_finish_statement(struct cassandra_sql_statement *stmt) +{ + const struct cassandra_sql_arg *arg; + + if (stmt->prep->prepared == NULL) { + i_assert(stmt->prep->error != NULL); + + if (stmt->result != NULL) { + stmt->result->error = i_strdup(stmt->prep->error); + result_finish(stmt->result); + } + pool_unref(&stmt->stmt.pool); + return; + } + stmt->cass_stmt = cass_prepared_bind(stmt->prep->prepared); + + if (stmt->timestamp != 0) + cass_statement_set_timestamp(stmt->cass_stmt, stmt->timestamp); + + if (array_is_created(&stmt->pending_args)) { + array_foreach(&stmt->pending_args, arg) + prepare_finish_arg(stmt, arg); + } + if (stmt->result != NULL) { + stmt->result->statement = stmt->cass_stmt; + stmt->result->timestamp = stmt->timestamp; + (void)driver_cassandra_send_query(stmt->result); + pool_unref(&stmt->stmt.pool); + } +} + +static void +prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt) +{ + struct cassandra_sql_statement *stmt; + + array_foreach_elem(&prep_stmt->pending_statements, stmt) + prepare_finish_statement(stmt); + array_clear(&prep_stmt->pending_statements); +} + +static void prepare_callback(CassFuture *future, void *context) +{ + struct cassandra_sql_prepared_statement *prep_stmt = context; + CassError error = cass_future_error_code(future); + + if (error != CASS_OK) { + const char *errmsg; + size_t errsize; + + cass_future_error_message(future, &errmsg, &errsize); + i_free(prep_stmt->error); + prep_stmt->error = i_strndup(errmsg, errsize); + } else { + prep_stmt->prepared = cass_future_get_prepared(future); + } + + prepare_finish_pending_statements(prep_stmt); +} + +static void prepare_start(struct cassandra_sql_prepared_statement *prep_stmt) +{ + struct cassandra_db *db = (struct cassandra_db *)prep_stmt->prep_stmt.db; + CassFuture *future; + + if (!SQL_DB_IS_READY(&db->api)) { + if (!prep_stmt->pending) { + prep_stmt->pending = TRUE; + array_push_back(&db->pending_prepares, &prep_stmt); + + if (sql_connect(&db->api) < 0) + i_unreached(); + } + return; + } + + /* clear the current error in case we're retrying */ + i_free_and_null(prep_stmt->error); + + future = cass_session_prepare(db->session, + prep_stmt->prep_stmt.query_template); + driver_cassandra_set_callback(future, db, prepare_callback, prep_stmt); +} + +static void driver_cassandra_prepare_pending(struct cassandra_db *db) +{ + struct cassandra_sql_prepared_statement *prep_stmt; + + i_assert(SQL_DB_IS_READY(&db->api)); + + array_foreach_elem(&db->pending_prepares, prep_stmt) { + prep_stmt->pending = FALSE; + prepare_start(prep_stmt); + } + array_clear(&db->pending_prepares); +} + +static struct sql_prepared_statement * +driver_cassandra_prepared_statement_init(struct sql_db *db, + const char *query_template) +{ + struct cassandra_sql_prepared_statement *prep_stmt = + i_new(struct cassandra_sql_prepared_statement, 1); + prep_stmt->prep_stmt.db = db; + prep_stmt->prep_stmt.refcount = 1; + prep_stmt->prep_stmt.query_template = i_strdup(query_template); + i_array_init(&prep_stmt->pending_statements, 4); + prepare_start(prep_stmt); + return &prep_stmt->prep_stmt; +} + +static void +driver_cassandra_prepared_statement_deinit(struct sql_prepared_statement *_prep_stmt) +{ + struct cassandra_sql_prepared_statement *prep_stmt = + (struct cassandra_sql_prepared_statement *)_prep_stmt; + + i_assert(array_count(&prep_stmt->pending_statements) == 0); + if (prep_stmt->prepared != NULL) + cass_prepared_free(prep_stmt->prepared); + array_free(&prep_stmt->pending_statements); + i_free(prep_stmt->error); + i_free(prep_stmt->prep_stmt.query_template); + i_free(prep_stmt); +} + +static struct sql_statement * +driver_cassandra_statement_init(struct sql_db *db ATTR_UNUSED, + const char *query_template ATTR_UNUSED) +{ + pool_t pool = pool_alloconly_create("cassandra sql statement", 1024); + struct cassandra_sql_statement *stmt = + p_new(pool, struct cassandra_sql_statement, 1); + stmt->stmt.pool = pool; + return &stmt->stmt; +} + +static struct sql_statement * +driver_cassandra_statement_init_prepared(struct sql_prepared_statement *_prep_stmt) +{ + struct cassandra_sql_prepared_statement *prep_stmt = + (struct cassandra_sql_prepared_statement *)_prep_stmt; + pool_t pool = pool_alloconly_create("cassandra prepared sql statement", 1024); + struct cassandra_sql_statement *stmt = + p_new(pool, struct cassandra_sql_statement, 1); + + stmt->stmt.pool = pool; + stmt->stmt.query_template = + p_strdup(stmt->stmt.pool, prep_stmt->prep_stmt.query_template); + stmt->prep = prep_stmt; + + if (prep_stmt->prepared != NULL) { + /* statement is already prepared. we can use it immediately. */ + stmt->cass_stmt = cass_prepared_bind(prep_stmt->prepared); + } else { + if (prep_stmt->error != NULL) + prepare_start(prep_stmt); + /* need to wait until prepare is finished */ + array_push_back(&prep_stmt->pending_statements, &stmt); + } + return &stmt->stmt; +} + +static void +driver_cassandra_statement_abort(struct sql_statement *_stmt) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + + if (stmt->cass_stmt != NULL) + cass_statement_free(stmt->cass_stmt); +} + +static void +driver_cassandra_statement_set_timestamp(struct sql_statement *_stmt, + const struct timespec *ts) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + cass_int64_t ts_usecs = + (cass_int64_t)ts->tv_sec * 1000000ULL + + ts->tv_nsec / 1000; + + i_assert(stmt->result == NULL); + + if (stmt->cass_stmt != NULL) + cass_statement_set_timestamp(stmt->cass_stmt, ts_usecs); + stmt->timestamp = ts_usecs; +} + +static struct cassandra_sql_arg * +driver_cassandra_add_pending_arg(struct cassandra_sql_statement *stmt, + unsigned int column_idx) +{ + struct cassandra_sql_arg *arg; + + if (!array_is_created(&stmt->pending_args)) + p_array_init(&stmt->pending_args, stmt->stmt.pool, 8); + arg = array_append_space(&stmt->pending_args); + arg->column_idx = column_idx; + return arg; +} + +static void +driver_cassandra_statement_bind_str(struct sql_statement *_stmt, + unsigned int column_idx, + const char *value) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + if (stmt->cass_stmt != NULL) + cass_statement_bind_string(stmt->cass_stmt, column_idx, value); + else if (stmt->prep != NULL) { + struct cassandra_sql_arg *arg = + driver_cassandra_add_pending_arg(stmt, column_idx); + arg->value_str = p_strdup(_stmt->pool, value); + } +} + +static void +driver_cassandra_statement_bind_binary(struct sql_statement *_stmt, + unsigned int column_idx, + const void *value, size_t value_size) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + + if (stmt->cass_stmt != NULL) { + cass_statement_bind_bytes(stmt->cass_stmt, column_idx, + value, value_size); + } else if (stmt->prep != NULL) { + struct cassandra_sql_arg *arg = + driver_cassandra_add_pending_arg(stmt, column_idx); + arg->value_binary = value_size == 0 ? &uchar_nul : + p_memdup(_stmt->pool, value, value_size); + arg->value_binary_size = value_size; + } +} + +static void +driver_cassandra_statement_bind_int64(struct sql_statement *_stmt, + unsigned int column_idx, int64_t value) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + + if (stmt->cass_stmt != NULL) + driver_cassandra_bind_int(stmt, column_idx, value); + else if (stmt->prep != NULL) { + struct cassandra_sql_arg *arg = + driver_cassandra_add_pending_arg(stmt, column_idx); + arg->value_int64 = value; + } +} + +static void +driver_cassandra_statement_query(struct sql_statement *_stmt, + sql_query_callback_t *callback, void *context) +{ + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + struct cassandra_db *db = (struct cassandra_db *)_stmt->db; + const char *query = sql_statement_get_query(_stmt); + bool is_prepared = stmt->cass_stmt != NULL || stmt->prep != NULL; + + stmt->result = driver_cassandra_query_init(db, + sql_statement_get_log_query(_stmt), + CASSANDRA_QUERY_TYPE_READ, + is_prepared, + callback, context); + if (stmt->cass_stmt != NULL) { + stmt->result->statement = stmt->cass_stmt; + stmt->result->timestamp = stmt->timestamp; + } else if (stmt->prep != NULL) { + /* wait for prepare to finish */ + return; + } else { + stmt->result->statement = cass_statement_new(query, 0); + stmt->result->timestamp = stmt->timestamp; + if (stmt->timestamp != 0) { + cass_statement_set_timestamp(stmt->result->statement, + stmt->timestamp); + } + } + (void)driver_cassandra_send_query(stmt->result); + pool_unref(&_stmt->pool); +} + +static struct sql_result * +driver_cassandra_statement_query_s(struct sql_statement *_stmt ATTR_UNUSED) +{ + i_panic("cassandra: sql_statement_query_s() not supported"); +} + +static void +driver_cassandra_update_stmt(struct sql_transaction_context *_ctx, + struct sql_statement *_stmt, + unsigned int *affected_rows) +{ + struct cassandra_transaction_context *ctx = + (struct cassandra_transaction_context *)_ctx; + struct cassandra_sql_statement *stmt = + (struct cassandra_sql_statement *)_stmt; + + i_assert(affected_rows == NULL); + + if (ctx->query != NULL || ctx->stmt != NULL) { + transaction_set_failed(ctx, + "Multiple changes in transaction not supported"); + return; + } + if (stmt->prep != NULL) + ctx->stmt = stmt; + else { + ctx->query = i_strdup(sql_statement_get_query(_stmt)); + ctx->log_query = i_strdup(sql_statement_get_log_query(_stmt)); + ctx->query_timestamp = stmt->timestamp; + pool_unref(&_stmt->pool); + } +} + +static bool driver_cassandra_have_work(struct cassandra_db *db) +{ + return array_not_empty(&db->pending_prepares) || + array_not_empty(&db->callbacks) || + array_not_empty(&db->results); +} + +static void driver_cassandra_wait(struct sql_db *_db) +{ + struct cassandra_db *db = (struct cassandra_db *)_db; + + if (!driver_cassandra_have_work(db)) + return; + + struct ioloop *prev_ioloop = current_ioloop; + db->ioloop = io_loop_create(); + db->io_pipe = io_loop_move_io(&db->io_pipe); + while (driver_cassandra_have_work(db)) + io_loop_run(db->ioloop); + + io_loop_set_current(prev_ioloop); + db->io_pipe = io_loop_move_io(&db->io_pipe); + io_loop_set_current(db->ioloop); + io_loop_destroy(&db->ioloop); +} + +const struct sql_db driver_cassandra_db = { + .name = "cassandra", + .flags = SQL_DB_FLAG_PREP_STATEMENTS, + + .v = { + .init_full = driver_cassandra_init_full_v, + .deinit = driver_cassandra_deinit_v, + .connect = driver_cassandra_connect, + .disconnect = driver_cassandra_disconnect, + .escape_string = driver_cassandra_escape_string, + .exec = driver_cassandra_exec, + .query = driver_cassandra_query, + .query_s = driver_cassandra_query_s, + .wait = driver_cassandra_wait, + + .transaction_begin = driver_cassandra_transaction_begin, + .transaction_commit = driver_cassandra_transaction_commit, + .transaction_commit_s = driver_cassandra_transaction_commit_s, + .transaction_rollback = driver_cassandra_transaction_rollback, + + .update = driver_cassandra_update, + + .escape_blob = driver_cassandra_escape_blob, + + .prepared_statement_init = driver_cassandra_prepared_statement_init, + .prepared_statement_deinit = driver_cassandra_prepared_statement_deinit, + .statement_init = driver_cassandra_statement_init, + .statement_init_prepared = driver_cassandra_statement_init_prepared, + .statement_abort = driver_cassandra_statement_abort, + .statement_set_timestamp = driver_cassandra_statement_set_timestamp, + .statement_bind_str = driver_cassandra_statement_bind_str, + .statement_bind_binary = driver_cassandra_statement_bind_binary, + .statement_bind_int64 = driver_cassandra_statement_bind_int64, + .statement_query = driver_cassandra_statement_query, + .statement_query_s = driver_cassandra_statement_query_s, + .update_stmt = driver_cassandra_update_stmt, + } +}; + +const struct sql_result driver_cassandra_result = { + .v = { + driver_cassandra_result_free, + driver_cassandra_result_next_row, + driver_cassandra_result_get_fields_count, + driver_cassandra_result_get_field_name, + driver_cassandra_result_find_field, + driver_cassandra_result_get_field_value, + driver_cassandra_result_get_field_value_binary, + driver_cassandra_result_find_field_value, + driver_cassandra_result_get_values, + driver_cassandra_result_get_error, + driver_cassandra_result_more, + } +}; + +const char *driver_cassandra_version = DOVECOT_ABI_VERSION; + +void driver_cassandra_init(void); +void driver_cassandra_deinit(void); + +void driver_cassandra_init(void) +{ + sql_driver_register(&driver_cassandra_db); +} + +void driver_cassandra_deinit(void) +{ + sql_driver_unregister(&driver_cassandra_db); +} + +#endif diff --git a/src/lib-sql/driver-mysql.c b/src/lib-sql/driver-mysql.c new file mode 100644 index 0000000..2693a07 --- /dev/null +++ b/src/lib-sql/driver-mysql.c @@ -0,0 +1,844 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "array.h" +#include "hex-binary.h" +#include "str.h" +#include "net.h" +#include "time-util.h" +#include "sql-api-private.h" + +#ifdef BUILD_MYSQL +#include <unistd.h> +#include <time.h> +#ifdef HAVE_ATTR_NULL +/* ugly way to tell clang that mysql.h is a system header and we don't want + to enable nonnull attributes for it by default.. */ +# 4 "driver-mysql.c" 3 +#endif +#include <mysql.h> +#ifdef HAVE_ATTR_NULL +# 4 "driver-mysql.c" 3 +# line 20 +#endif +#include <errmsg.h> + +#define MYSQL_DEFAULT_READ_TIMEOUT_SECS 30 +#define MYSQL_DEFAULT_WRITE_TIMEOUT_SECS 30 + +struct mysql_db { + struct sql_db api; + + pool_t pool; + const char *user, *password, *dbname, *host, *unix_socket; + const char *ssl_cert, *ssl_key, *ssl_ca, *ssl_ca_path, *ssl_cipher; + int ssl_verify_server_cert; + const char *option_file, *option_group; + in_port_t port; + unsigned int client_flags; + unsigned int connect_timeout, read_timeout, write_timeout; + time_t last_success; + + MYSQL *mysql; + unsigned int next_query_connection; + + bool ssl_set:1; +}; + +struct mysql_result { + struct sql_result api; + + MYSQL_RES *result; + MYSQL_ROW row; + + MYSQL_FIELD *fields; + unsigned int fields_count; + + my_ulonglong affected_rows; +}; + +struct mysql_transaction_context { + struct sql_transaction_context ctx; + + pool_t query_pool; + const char *error; + + bool failed:1; + bool committed:1; + bool commit_started:1; +}; + +extern const struct sql_db driver_mysql_db; +extern const struct sql_result driver_mysql_result; +extern const struct sql_result driver_mysql_error_result; + +static struct event_category event_category_mysql = { + .parent = &event_category_sql, + .name = "mysql" +}; + +static int driver_mysql_connect(struct sql_db *_db) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + const char *unix_socket, *host; + unsigned long client_flags = db->client_flags; + unsigned int secs_used; + time_t start_time; + bool failed; + + i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED); + + sql_db_set_state(&db->api, SQL_DB_STATE_CONNECTING); + + if (db->host == NULL) { + /* assume option_file overrides the host, or if not we'll just + connect to localhost */ + unix_socket = NULL; + host = NULL; + } else if (*db->host == '/') { + unix_socket = db->host; + host = NULL; + } else { + unix_socket = NULL; + host = db->host; + } + + if (db->option_file != NULL) { + mysql_options(db->mysql, MYSQL_READ_DEFAULT_FILE, + db->option_file); + } + + if (db->host != NULL) + event_set_append_log_prefix(_db->event, t_strdup_printf("mysql(%s): ", db->host)); + + e_debug(_db->event, "Connecting"); + + mysql_options(db->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &db->connect_timeout); + mysql_options(db->mysql, MYSQL_OPT_READ_TIMEOUT, &db->read_timeout); + mysql_options(db->mysql, MYSQL_OPT_WRITE_TIMEOUT, &db->write_timeout); + mysql_options(db->mysql, MYSQL_READ_DEFAULT_GROUP, + db->option_group != NULL ? db->option_group : "client"); + + if (!db->ssl_set && (db->ssl_ca != NULL || db->ssl_ca_path != NULL)) { +#ifdef HAVE_MYSQL_SSL + mysql_ssl_set(db->mysql, db->ssl_key, db->ssl_cert, + db->ssl_ca, db->ssl_ca_path +#ifdef HAVE_MYSQL_SSL_CIPHER + , db->ssl_cipher +#endif + ); +#ifdef HAVE_MYSQL_SSL_VERIFY_SERVER_CERT + mysql_options(db->mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, + (void *)&db->ssl_verify_server_cert); +#endif + db->ssl_set = TRUE; +#else + i_fatal("mysql: SSL support not compiled in " + "(remove ssl_ca and ssl_ca_path settings)"); +#endif + } + +#ifdef CLIENT_MULTI_RESULTS + client_flags |= CLIENT_MULTI_RESULTS; +#endif + /* CLIENT_MULTI_RESULTS allows the use of stored procedures */ + start_time = time(NULL); + failed = mysql_real_connect(db->mysql, host, db->user, db->password, + db->dbname, db->port, unix_socket, + client_flags) == NULL; + secs_used = time(NULL) - start_time; + if (failed) { + /* connecting could have taken a while. make sure that any + timeouts that get added soon will get a refreshed + timestamp. */ + io_loop_time_refresh(); + + if (db->api.connect_delay < secs_used) + db->api.connect_delay = secs_used; + sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED); + e_error(_db->event, "Connect failed to database (%s): %s - " + "waiting for %u seconds before retry", + db->dbname, mysql_error(db->mysql), db->api.connect_delay); + sql_disconnect(&db->api); + return -1; + } else { + db->last_success = ioloop_time; + sql_db_set_state(&db->api, SQL_DB_STATE_IDLE); + return 1; + } +} + +static void driver_mysql_disconnect(struct sql_db *_db) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + if (db->mysql != NULL) + mysql_close(db->mysql); +} + +static int driver_mysql_parse_connect_string(struct mysql_db *db, + const char *connect_string, + const char **error_r) +{ + const char *const *args, *name, *value; + const char **field; + + db->ssl_cipher = "HIGH"; + db->ssl_verify_server_cert = 1; + db->connect_timeout = SQL_CONNECT_TIMEOUT_SECS; + db->read_timeout = MYSQL_DEFAULT_READ_TIMEOUT_SECS; + db->write_timeout = MYSQL_DEFAULT_WRITE_TIMEOUT_SECS; + + args = t_strsplit_spaces(connect_string, " "); + for (; *args != NULL; args++) { + value = strchr(*args, '='); + if (value == NULL) { + *error_r = t_strdup_printf("Missing value in connect string: %s", + *args); + return -1; + } + name = t_strdup_until(*args, value); + value++; + + field = NULL; + if (strcmp(name, "host") == 0 || + strcmp(name, "hostaddr") == 0) + field = &db->host; + else if (strcmp(name, "user") == 0) + field = &db->user; + else if (strcmp(name, "password") == 0) + field = &db->password; + else if (strcmp(name, "dbname") == 0) + field = &db->dbname; + else if (strcmp(name, "port") == 0) { + if (net_str2port(value, &db->port) < 0) { + *error_r = t_strdup_printf("Invalid port number: %s", value); + return -1; + } + } else if (strcmp(name, "client_flags") == 0) { + if (str_to_uint(value, &db->client_flags) < 0) { + *error_r = t_strdup_printf("Invalid client flags: %s", value); + return -1; + } + } else if (strcmp(name, "connect_timeout") == 0) { + if (str_to_uint(value, &db->connect_timeout) < 0) { + *error_r = t_strdup_printf("Invalid read_timeout: %s", value); + return -1; + } + } else if (strcmp(name, "read_timeout") == 0) { + if (str_to_uint(value, &db->read_timeout) < 0) { + *error_r = t_strdup_printf("Invalid read_timeout: %s", value); + return -1; + } + } else if (strcmp(name, "write_timeout") == 0) { + if (str_to_uint(value, &db->write_timeout) < 0) { + *error_r = t_strdup_printf("Invalid read_timeout: %s", value); + return -1; + } + } else if (strcmp(name, "ssl_cert") == 0) + field = &db->ssl_cert; + else if (strcmp(name, "ssl_key") == 0) + field = &db->ssl_key; + else if (strcmp(name, "ssl_ca") == 0) + field = &db->ssl_ca; + else if (strcmp(name, "ssl_ca_path") == 0) + field = &db->ssl_ca_path; + else if (strcmp(name, "ssl_cipher") == 0) + field = &db->ssl_cipher; + else if (strcmp(name, "ssl_verify_server_cert") == 0) { + if (strcmp(value, "yes") == 0) + db->ssl_verify_server_cert = 1; + else if (strcmp(value, "no") == 0) + db->ssl_verify_server_cert = 0; + else { + *error_r = t_strdup_printf("Invalid boolean: %s", value); + return -1; + } + } else if (strcmp(name, "option_file") == 0) + field = &db->option_file; + else if (strcmp(name, "option_group") == 0) + field = &db->option_group; + else { + *error_r = t_strdup_printf("Unknown connect string: %s", name); + return -1; + } + if (field != NULL) + *field = p_strdup(db->pool, value); + } + + if (db->host == NULL && db->option_file == NULL) { + *error_r = "No hosts given in connect string"; + return -1; + } + if (db->mysql == NULL) { + db->mysql = p_new(db->pool, MYSQL, 1); + MYSQL *ptr = mysql_init(db->mysql); + if (ptr == NULL) + i_fatal_status(FATAL_OUTOFMEM, "mysql_init() failed"); + } + return 0; +} + +static int driver_mysql_init_full_v(const struct sql_settings *set, + struct sql_db **db_r, const char **error_r) +{ + struct mysql_db *db; + const char *error = NULL; + pool_t pool; + int ret; + + pool = pool_alloconly_create("mysql driver", 1024); + db = p_new(pool, struct mysql_db, 1); + db->pool = pool; + db->api = driver_mysql_db; + db->api.event = event_create(set->event_parent); + event_add_category(db->api.event, &event_category_mysql); + event_set_append_log_prefix(db->api.event, "mysql: "); + T_BEGIN { + ret = driver_mysql_parse_connect_string(db, set->connect_string, &error); + error = p_strdup(db->pool, error); + } T_END; + + if (ret < 0) { + *error_r = t_strdup(error); + pool_unref(&db->pool); + return ret; + } + + *db_r = &db->api; + return 0; +} + +static void driver_mysql_deinit_v(struct sql_db *_db) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + + _db->no_reconnect = TRUE; + sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED); + + driver_mysql_disconnect(_db); + + sql_connection_log_finished(_db); + event_unref(&_db->event); + array_free(&_db->module_contexts); + pool_unref(&db->pool); +} + +static int driver_mysql_do_query(struct mysql_db *db, const char *query, + struct event *event) +{ + int ret, diff; + struct event_passthrough *e; + + ret = mysql_query(db->mysql, query); + io_loop_time_refresh(); + e = sql_query_finished_event(&db->api, event, query, ret == 0, &diff); + + if (ret != 0) { + e->add_int("error_code", mysql_errno(db->mysql)); + e->add_str("error", mysql_error(db->mysql)); + e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", query, + diff, mysql_error(db->mysql)); + } else + e_debug(e->event(), SQL_QUERY_FINISHED_FMT, query, diff); + + if (ret == 0) + return 0; + + /* failed */ + switch (mysql_errno(db->mysql)) { + case CR_SERVER_GONE_ERROR: + case CR_SERVER_LOST: + sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED); + break; + default: + break; + } + return -1; +} + +static const char * +driver_mysql_escape_string(struct sql_db *_db, const char *string) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + size_t len = strlen(string); + char *to; + + if (_db->state == SQL_DB_STATE_DISCONNECTED) { + /* try connecting */ + (void)sql_connect(&db->api); + } + + if (_db->state == SQL_DB_STATE_DISCONNECTED) { + /* FIXME: we don't have a valid connection, so fallback + to using default escaping. the next query will most + likely fail anyway so it shouldn't matter that much + what we return here.. Anyway, this API needs + changing so that the escaping function could already + fail the query reliably. */ + to = t_buffer_get(len * 2 + 1); + len = mysql_escape_string(to, string, len); + t_buffer_alloc(len + 1); + return to; + } + + to = t_buffer_get(len * 2 + 1); + len = mysql_real_escape_string(db->mysql, to, string, len); + t_buffer_alloc(len + 1); + return to; +} + +static void driver_mysql_exec(struct sql_db *_db, const char *query) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + struct event *event = event_create(_db->event); + + (void)driver_mysql_do_query(db, query, event); + + event_unref(&event); +} + +static void driver_mysql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) +{ + struct sql_result *result; + + result = sql_query_s(db, query); + result->callback = TRUE; + callback(result, context); + result->callback = FALSE; + sql_result_unref(result); +} + +static struct sql_result * +driver_mysql_query_s(struct sql_db *_db, const char *query) +{ + struct mysql_db *db = container_of(_db, struct mysql_db, api); + struct mysql_result *result; + struct event *event; + int ret; + + result = i_new(struct mysql_result, 1); + result->api = driver_mysql_result; + event = event_create(_db->event); + + if (driver_mysql_do_query(db, query, event) < 0) + result->api = driver_mysql_error_result; + else { + /* query ok */ + result->affected_rows = mysql_affected_rows(db->mysql); + result->result = mysql_store_result(db->mysql); +#ifdef CLIENT_MULTI_RESULTS + /* Because we've enabled CLIENT_MULTI_RESULTS, we need to read + (ignore) extra results - there should not be any. + ret is: -1 = done, >0 = error, 0 = more results. */ + while ((ret = mysql_next_result(db->mysql)) == 0) ; +#else + ret = -1; +#endif + + if (ret < 0 && + (result->result != NULL || mysql_errno(db->mysql) == 0)) { + /* ok */ + } else { + /* failed */ + if (result->result != NULL) + mysql_free_result(result->result); + result->api = driver_mysql_error_result; + } + } + + result->api.db = _db; + result->api.refcount = 1; + result->api.event = event; + return &result->api; +} + +static void driver_mysql_result_free(struct sql_result *_result) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + i_assert(_result != &sql_not_connected_result); + if (_result->callback) + return; + + if (result->result != NULL) + mysql_free_result(result->result); + event_unref(&_result->event); + i_free(result); +} + +static int driver_mysql_result_next_row(struct sql_result *_result) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + struct mysql_db *db = container_of(_result->db, struct mysql_db, api); + int ret; + + if (result->result == NULL) { + /* no results */ + return 0; + } + + result->row = mysql_fetch_row(result->result); + if (result->row != NULL) + ret = 1; + else { + if (mysql_errno(db->mysql) != 0) + return -1; + ret = 0; + } + db->last_success = ioloop_time; + return ret; +} + +static void driver_mysql_result_fetch_fields(struct mysql_result *result) +{ + if (result->fields != NULL) + return; + + result->fields_count = mysql_num_fields(result->result); + result->fields = mysql_fetch_fields(result->result); +} + +static unsigned int +driver_mysql_result_get_fields_count(struct sql_result *_result) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + driver_mysql_result_fetch_fields(result); + return result->fields_count; +} + +static const char * +driver_mysql_result_get_field_name(struct sql_result *_result, unsigned int idx) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + driver_mysql_result_fetch_fields(result); + i_assert(idx < result->fields_count); + return result->fields[idx].name; +} + +static int driver_mysql_result_find_field(struct sql_result *_result, + const char *field_name) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + unsigned int i; + + driver_mysql_result_fetch_fields(result); + for (i = 0; i < result->fields_count; i++) { + if (strcmp(result->fields[i].name, field_name) == 0) + return i; + } + return -1; +} + +static const char * +driver_mysql_result_get_field_value(struct sql_result *_result, + unsigned int idx) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + return (const char *)result->row[idx]; +} + +static const unsigned char * +driver_mysql_result_get_field_value_binary(struct sql_result *_result, + unsigned int idx, size_t *size_r) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + unsigned long *lengths; + + lengths = mysql_fetch_lengths(result->result); + + *size_r = lengths[idx]; + return (const void *)result->row[idx]; +} + +static const char * +driver_mysql_result_find_field_value(struct sql_result *result, + const char *field_name) +{ + int idx; + + idx = driver_mysql_result_find_field(result, field_name); + if (idx < 0) + return NULL; + return driver_mysql_result_get_field_value(result, idx); +} + +static const char *const * +driver_mysql_result_get_values(struct sql_result *_result) +{ + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + return (const char *const *)result->row; +} + +static const char *driver_mysql_result_get_error(struct sql_result *_result) +{ + struct mysql_db *db = container_of(_result->db, struct mysql_db, api); + const char *errstr; + unsigned int idle_time; + int err; + + err = mysql_errno(db->mysql); + errstr = mysql_error(db->mysql); + if ((err == CR_SERVER_GONE_ERROR || err == CR_SERVER_LOST) && + db->last_success != 0) { + idle_time = ioloop_time - db->last_success; + errstr = t_strdup_printf("%s (idled for %u secs)", + errstr, idle_time); + } + return errstr; +} + +static struct sql_transaction_context * +driver_mysql_transaction_begin(struct sql_db *db) +{ + struct mysql_transaction_context *ctx; + + ctx = i_new(struct mysql_transaction_context, 1); + ctx->ctx.db = db; + ctx->query_pool = pool_alloconly_create("mysql transaction", 1024); + ctx->ctx.event = event_create(db->event); + return &ctx->ctx; +} + +static void +driver_mysql_transaction_commit(struct sql_transaction_context *ctx, + sql_commit_callback_t *callback, void *context) +{ + struct sql_commit_result result; + const char *error; + + i_zero(&result); + if (sql_transaction_commit_s(&ctx, &error) < 0) + result.error = error; + callback(&result, context); +} + +static int ATTR_NULL(3) +transaction_send_query(struct mysql_transaction_context *ctx, const char *query, + unsigned int *affected_rows_r) +{ + struct sql_result *_result; + int ret = 0; + + if (ctx->failed) + return -1; + + _result = sql_query_s(ctx->ctx.db, query); + if (sql_result_next_row(_result) < 0) { + ctx->error = sql_result_get_error(_result); + ctx->failed = TRUE; + ret = -1; + } else if (affected_rows_r != NULL) { + struct mysql_result *result = + container_of(_result, struct mysql_result, api); + + i_assert(result->affected_rows != (my_ulonglong)-1); + *affected_rows_r = result->affected_rows; + } + sql_result_unref(_result); + return ret; +} + +static int driver_mysql_try_commit_s(struct mysql_transaction_context *ctx) +{ + struct sql_transaction_context *_ctx = &ctx->ctx; + bool multi = _ctx->head != NULL && _ctx->head->next != NULL; + + /* wrap in BEGIN/COMMIT only if transaction has mutiple statements. */ + if (multi && transaction_send_query(ctx, "BEGIN", NULL) < 0) { + if (_ctx->db->state != SQL_DB_STATE_DISCONNECTED) + return -1; + /* we got disconnected, retry */ + return 0; + } else if (multi) { + ctx->commit_started = TRUE; + } + + while (_ctx->head != NULL) { + if (transaction_send_query(ctx, _ctx->head->query, + _ctx->head->affected_rows) < 0) + return -1; + _ctx->head = _ctx->head->next; + } + if (multi && transaction_send_query(ctx, "COMMIT", NULL) < 0) + return -1; + return 1; +} + +static int +driver_mysql_transaction_commit_s(struct sql_transaction_context *_ctx, + const char **error_r) +{ + struct mysql_transaction_context *ctx = + container_of(_ctx, struct mysql_transaction_context, ctx); + struct mysql_db *db = container_of(_ctx->db, struct mysql_db, api); + int ret = 1; + + *error_r = NULL; + + if (_ctx->head != NULL) { + ret = driver_mysql_try_commit_s(ctx); + *error_r = t_strdup(ctx->error); + if (ret == 0) { + e_info(db->api.event, "Disconnected from database, " + "retrying commit"); + if (sql_connect(_ctx->db) >= 0) { + ctx->failed = FALSE; + ret = driver_mysql_try_commit_s(ctx); + } + } + } + + if (ret > 0) + ctx->committed = TRUE; + + sql_transaction_rollback(&_ctx); + return ret <= 0 ? -1 : 0; +} + +static void +driver_mysql_transaction_rollback(struct sql_transaction_context *_ctx) +{ + struct mysql_transaction_context *ctx = + container_of(_ctx, struct mysql_transaction_context, ctx); + + if (ctx->failed) { + bool rolledback = FALSE; + const char *orig_error = t_strdup(ctx->error); + if (ctx->commit_started) { + /* reset failed flag so ROLLBACK is actually sent. + otherwise, transaction_send_query() will return + without trying to send the query. */ + ctx->failed = FALSE; + if (transaction_send_query(ctx, "ROLLBACK", NULL) < 0) + e_debug(event_create_passthrough(_ctx->event)-> + add_str("error", ctx->error)->event(), + "Rollback failed: %s", ctx->error); + else + rolledback = TRUE; + } + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", orig_error)->event(), + "Transaction failed: %s%s", orig_error, + rolledback ? " - Rolled back" : ""); + } else if (ctx->committed) + e_debug(sql_transaction_finished_event(_ctx)->event(), + "Transaction committed"); + else + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", "Rolled back")->event(), + "Transaction rolled back"); + + event_unref(&ctx->ctx.event); + pool_unref(&ctx->query_pool); + i_free(ctx); +} + +static void +driver_mysql_update(struct sql_transaction_context *_ctx, const char *query, + unsigned int *affected_rows) +{ + struct mysql_transaction_context *ctx = + container_of(_ctx, struct mysql_transaction_context, ctx); + + sql_transaction_add_query(&ctx->ctx, ctx->query_pool, + query, affected_rows); +} + +static const char * +driver_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + string_t *str = t_str_new(128); + + str_append(str, "X'"); + binary_to_hex_append(str, data, size); + str_append_c(str, '\''); + return str_c(str); +} + +const struct sql_db driver_mysql_db = { + .name = "mysql", + .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_POOLED | + SQL_DB_FLAG_ON_DUPLICATE_KEY, + + .v = { + .init_full = driver_mysql_init_full_v, + .deinit = driver_mysql_deinit_v, + .connect = driver_mysql_connect, + .disconnect = driver_mysql_disconnect, + .escape_string = driver_mysql_escape_string, + .exec = driver_mysql_exec, + .query = driver_mysql_query, + .query_s = driver_mysql_query_s, + + .transaction_begin = driver_mysql_transaction_begin, + .transaction_commit = driver_mysql_transaction_commit, + .transaction_commit_s = driver_mysql_transaction_commit_s, + .transaction_rollback = driver_mysql_transaction_rollback, + + .update = driver_mysql_update, + + .escape_blob = driver_mysql_escape_blob, + } +}; + +const struct sql_result driver_mysql_result = { + .v = { + .free = driver_mysql_result_free, + .next_row = driver_mysql_result_next_row, + .get_fields_count = driver_mysql_result_get_fields_count, + .get_field_name = driver_mysql_result_get_field_name, + .find_field = driver_mysql_result_find_field, + .get_field_value = driver_mysql_result_get_field_value, + .get_field_value_binary = driver_mysql_result_get_field_value_binary, + .find_field_value = driver_mysql_result_find_field_value, + .get_values = driver_mysql_result_get_values, + .get_error = driver_mysql_result_get_error, + } +}; + +static int +driver_mysql_result_error_next_row(struct sql_result *result ATTR_UNUSED) +{ + return -1; +} + +const struct sql_result driver_mysql_error_result = { + .v = { + .free = driver_mysql_result_free, + .next_row = driver_mysql_result_error_next_row, + .get_error = driver_mysql_result_get_error, + }, + .failed_try_retry = TRUE +}; + +const char *driver_mysql_version = DOVECOT_ABI_VERSION; + +void driver_mysql_init(void); +void driver_mysql_deinit(void); + +void driver_mysql_init(void) +{ + sql_driver_register(&driver_mysql_db); +} + +void driver_mysql_deinit(void) +{ + sql_driver_unregister(&driver_mysql_db); +} + +#endif diff --git a/src/lib-sql/driver-pgsql.c b/src/lib-sql/driver-pgsql.c new file mode 100644 index 0000000..63188c0 --- /dev/null +++ b/src/lib-sql/driver-pgsql.c @@ -0,0 +1,1344 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "ioloop.h" +#include "hex-binary.h" +#include "str.h" +#include "time-util.h" +#include "sql-api-private.h" +#include "llist.h" + +#ifdef BUILD_PGSQL +#include <libpq-fe.h> + +#define PGSQL_DNS_WARN_MSECS 500 + +struct pgsql_db { + struct sql_db api; + + pool_t pool; + char *connect_string; + char *host; + PGconn *pg; + + struct io *io; + struct timeout *to_connect; + enum io_condition io_dir; + + struct pgsql_result *pending_results; + struct pgsql_result *cur_result; + struct ioloop *ioloop, *orig_ioloop; + struct sql_result *sync_result; + + bool (*next_callback)(void *); + void *next_context; + + char *error; + const char *connect_state; + + bool fatal_error:1; +}; + +struct pgsql_binary_value { + unsigned char *value; + size_t size; +}; + +struct pgsql_result { + struct sql_result api; + + struct pgsql_result *prev, *next; + + PGresult *pgres; + struct timeout *to; + + unsigned int rownum, rows; + unsigned int fields_count; + const char **fields; + const char **values; + char *query; + + ARRAY(struct pgsql_binary_value) binary_values; + + sql_query_callback_t *callback; + void *context; + + bool timeout:1; +}; + +struct pgsql_transaction_context { + struct sql_transaction_context ctx; + int refcount; + + sql_commit_callback_t *callback; + void *context; + + pool_t query_pool; + const char *error; + + bool failed:1; +}; + +extern const struct sql_db driver_pgsql_db; +extern const struct sql_result driver_pgsql_result; + +static void result_finish(struct pgsql_result *result); +static void +transaction_update_callback(struct sql_result *result, + struct sql_transaction_query *query); + +static struct event_category event_category_pgsql = { + .parent = &event_category_sql, + .name = "pgsql" +}; + +static void driver_pgsql_set_state(struct pgsql_db *db, enum sql_db_state state) +{ + i_assert(state == SQL_DB_STATE_BUSY || db->cur_result == NULL); + + /* switch back to original ioloop in case the caller wants to + add/remove timeouts */ + if (db->ioloop != NULL) + io_loop_set_current(db->orig_ioloop); + sql_db_set_state(&db->api, state); + if (db->ioloop != NULL) + io_loop_set_current(db->ioloop); +} + +static bool driver_pgsql_next_callback(struct pgsql_db *db) +{ + bool (*next_callback)(void *) = db->next_callback; + void *next_context = db->next_context; + + if (next_callback == NULL) + return FALSE; + + db->next_callback = NULL; + db->next_context = NULL; + return next_callback(next_context); +} + +static void driver_pgsql_stop_io(struct pgsql_db *db) +{ + if (db->io != NULL) { + io_remove(&db->io); + db->io_dir = 0; + } +} + +static void driver_pgsql_close(struct pgsql_db *db) +{ + db->io_dir = 0; + db->fatal_error = FALSE; + + driver_pgsql_stop_io(db); + + PQfinish(db->pg); + db->pg = NULL; + + timeout_remove(&db->to_connect); + + driver_pgsql_set_state(db, SQL_DB_STATE_DISCONNECTED); + + if (db->ioloop != NULL) { + /* running a sync query, stop it */ + io_loop_stop(db->ioloop); + } + driver_pgsql_next_callback(db); +} + +static const char *last_error(struct pgsql_db *db) +{ + const char *msg; + size_t len; + + msg = PQerrorMessage(db->pg); + if (msg == NULL) + return "(no error set)"; + + /* Error message should contain trailing \n, we don't want it */ + len = strlen(msg); + return len == 0 || msg[len-1] != '\n' ? msg : + t_strndup(msg, len-1); +} + +static void connect_callback(struct pgsql_db *db) +{ + enum io_condition io_dir = 0; + int ret; + + driver_pgsql_stop_io(db); + + while ((ret = PQconnectPoll(db->pg)) == PGRES_POLLING_ACTIVE) + ; + + switch (ret) { + case PGRES_POLLING_READING: + db->connect_state = "wait for input"; + io_dir = IO_READ; + break; + case PGRES_POLLING_WRITING: + db->connect_state = "wait for output"; + io_dir = IO_WRITE; + break; + case PGRES_POLLING_OK: + break; + case PGRES_POLLING_FAILED: + e_error(db->api.event, "Connect failed to database %s: %s (state: %s)", + PQdb(db->pg), last_error(db), db->connect_state); + driver_pgsql_close(db); + return; + } + + if (io_dir != 0) { + db->io = io_add(PQsocket(db->pg), io_dir, connect_callback, db); + db->io_dir = io_dir; + } + + if (io_dir == 0) { + db->connect_state = "connected"; + timeout_remove(&db->to_connect); + if (PQserverVersion(db->pg) >= 90500) { + /* v9.5+ */ + db->api.flags |= SQL_DB_FLAG_ON_CONFLICT_DO; + } + driver_pgsql_set_state(db, SQL_DB_STATE_IDLE); + if (db->ioloop != NULL) { + /* driver_pgsql_sync_init() waiting for connection to + finish */ + io_loop_stop(db->ioloop); + } + } +} + +static void driver_pgsql_connect_timeout(struct pgsql_db *db) +{ + unsigned int secs = ioloop_time - db->api.last_connect_try; + + e_error(db->api.event, "Connect failed: Timeout after %u seconds (state: %s)", + secs, db->connect_state); + driver_pgsql_close(db); +} + +static int driver_pgsql_connect(struct sql_db *_db) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + struct timeval tv_start; + int msecs; + + i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED); + + io_loop_time_refresh(); + tv_start = ioloop_timeval; + + db->pg = PQconnectStart(db->connect_string); + if (db->pg == NULL) { + i_fatal("pgsql: PQconnectStart() failed (out of memory)"); + } + + if (PQstatus(db->pg) == CONNECTION_BAD) { + e_error(_db->event, "Connect failed to database %s: %s", + PQdb(db->pg), last_error(db)); + driver_pgsql_close(db); + return -1; + } + /* PQconnectStart() blocks on host name resolving. Log a warning if + it takes too long. Also don't include time spent on that in the + connect timeout (by refreshing ioloop time). */ + io_loop_time_refresh(); + msecs = timeval_diff_msecs(&ioloop_timeval, &tv_start); + if (msecs > PGSQL_DNS_WARN_MSECS) { + e_warning(_db->event, "DNS lookup took %d.%03d s", + msecs/1000, msecs % 1000); + } + + /* nonblocking connecting begins. */ + if (PQsetnonblocking(db->pg, 1) < 0) + e_error(_db->event, "PQsetnonblocking() failed"); + i_assert(db->to_connect == NULL); + db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000, + driver_pgsql_connect_timeout, db); + db->connect_state = "connecting"; + db->io = io_add(PQsocket(db->pg), IO_WRITE, connect_callback, db); + db->io_dir = IO_WRITE; + driver_pgsql_set_state(db, SQL_DB_STATE_CONNECTING); + return 0; +} + +static void driver_pgsql_disconnect(struct sql_db *_db) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + + if (db->cur_result != NULL && db->cur_result->to != NULL) { + driver_pgsql_stop_io(db); + result_finish(db->cur_result); + } + + _db->no_reconnect = TRUE; + driver_pgsql_close(db); + _db->no_reconnect = FALSE; +} + +static void driver_pgsql_free(struct pgsql_db **_db) +{ + struct pgsql_db *db = *_db; + *_db = NULL; + + event_unref(&db->api.event); + i_free(db->connect_string); + i_free(db->host); + i_free(db->error); + array_free(&db->api.module_contexts); + i_free(db); +} + +static enum sql_db_flags driver_pgsql_get_flags(struct sql_db *db) +{ + switch (db->state) { + case SQL_DB_STATE_DISCONNECTED: + if (sql_connect(db) < 0) + break; + /* fall through */ + case SQL_DB_STATE_CONNECTING: + /* Wait for connection to finish, so we can get the flags + reliably. */ + sql_wait(db); + break; + case SQL_DB_STATE_IDLE: + case SQL_DB_STATE_BUSY: + break; + } + return db->flags; +} + +static int driver_pgsql_init_full_v(const struct sql_settings *set, + struct sql_db **db_r, const char **error_r ATTR_UNUSED) +{ + struct pgsql_db *db; + + db = i_new(struct pgsql_db, 1); + db->connect_string = i_strdup(set->connect_string); + db->api = driver_pgsql_db; + db->api.event = event_create(set->event_parent); + event_add_category(db->api.event, &event_category_pgsql); + + /* NOTE: Connection string will be parsed by pgsql itself + We only pick the host part here */ + T_BEGIN { + const char *const *arg = t_strsplit(db->connect_string, " "); + + for (; *arg != NULL; arg++) { + if (str_begins(*arg, "host=")) + db->host = i_strdup(*arg + 5); + + } + } T_END; + + event_set_append_log_prefix(db->api.event, t_strdup_printf("pgsql(%s): ", db->host)); + + *db_r = &db->api; + return 0; +} + +static void driver_pgsql_deinit_v(struct sql_db *_db) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + + driver_pgsql_disconnect(_db); + driver_pgsql_free(&db); +} + +static void driver_pgsql_set_idle(struct pgsql_db *db) +{ + i_assert(db->api.state == SQL_DB_STATE_BUSY); + + if (db->fatal_error) + driver_pgsql_close(db); + else if (!driver_pgsql_next_callback(db)) + driver_pgsql_set_state(db, SQL_DB_STATE_IDLE); +} + +static void consume_results(struct pgsql_db *db) +{ + PGresult *pgres; + + driver_pgsql_stop_io(db); + + while (PQconsumeInput(db->pg) != 0) { + if (PQisBusy(db->pg) != 0) { + db->io = io_add(PQsocket(db->pg), IO_READ, + consume_results, db); + db->io_dir = IO_READ; + return; + } + + pgres = PQgetResult(db->pg); + if (pgres == NULL) + break; + PQclear(pgres); + } + + if (PQstatus(db->pg) == CONNECTION_BAD) + driver_pgsql_close(db); + else + driver_pgsql_set_idle(db); +} + +static void driver_pgsql_result_free(struct sql_result *_result) +{ + struct pgsql_db *db = (struct pgsql_db *)_result->db; + struct pgsql_result *result = (struct pgsql_result *)_result; + bool success; + + i_assert(!result->api.callback); + i_assert(db->cur_result == result); + i_assert(result->callback == NULL); + + if (_result == db->sync_result) + db->sync_result = NULL; + db->cur_result = NULL; + + success = result->pgres != NULL && !db->fatal_error; + if (result->pgres != NULL) { + PQclear(result->pgres); + result->pgres = NULL; + } + + if (success) { + /* we'll have to read the rest of the results as well */ + i_assert(db->io == NULL); + consume_results(db); + } else { + driver_pgsql_set_idle(db); + } + + if (array_is_created(&result->binary_values)) { + struct pgsql_binary_value *value; + + array_foreach_modifiable(&result->binary_values, value) + PQfreemem(value->value); + array_free(&result->binary_values); + } + + event_unref(&result->api.event); + i_free(result->query); + i_free(result->fields); + i_free(result->values); + i_free(result); +} + +static void result_finish(struct pgsql_result *result) +{ + struct pgsql_db *db = (struct pgsql_db *)result->api.db; + bool free_result = TRUE; + int duration; + + i_assert(db->io == NULL); + timeout_remove(&result->to); + DLLIST_REMOVE(&db->pending_results, result); + + /* if connection to server was lost, we don't yet see that the + connection is bad. we only see the fatal error, so assume it also + means disconnection. */ + if (PQstatus(db->pg) == CONNECTION_BAD || result->pgres == NULL || + PQresultStatus(result->pgres) == PGRES_FATAL_ERROR) + db->fatal_error = TRUE; + + if (db->fatal_error) { + result->api.failed = TRUE; + result->api.failed_try_retry = TRUE; + } + + /* emit event */ + if (result->api.failed) { + const char *error = result->timeout ? "Timed out" : last_error(db); + struct event_passthrough *e = + sql_query_finished_event(&db->api, result->api.event, + result->query, TRUE, &duration); + e->add_str("error", error); + e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", result->query, + duration, error); + } else { + e_debug(sql_query_finished_event(&db->api, result->api.event, + result->query, FALSE, &duration)-> + event(), + SQL_QUERY_FINISHED_FMT, result->query, duration); + } + result->api.callback = TRUE; + T_BEGIN { + if (result->callback != NULL) + result->callback(&result->api, result->context); + } T_END; + result->api.callback = FALSE; + + free_result = db->sync_result != &result->api; + if (db->ioloop != NULL) + io_loop_stop(db->ioloop); + + i_assert(!free_result || result->api.refcount > 0); + result->callback = NULL; + if (free_result) + sql_result_unref(&result->api); +} + +static void get_result(struct pgsql_result *result) +{ + struct pgsql_db *db = (struct pgsql_db *)result->api.db; + + driver_pgsql_stop_io(db); + + if (PQconsumeInput(db->pg) == 0) { + result_finish(result); + return; + } + + if (PQisBusy(db->pg) != 0) { + db->io = io_add(PQsocket(db->pg), IO_READ, + get_result, result); + db->io_dir = IO_READ; + return; + } + + result->pgres = PQgetResult(db->pg); + result_finish(result); +} + +static void flush_callback(struct pgsql_result *result) +{ + struct pgsql_db *db = (struct pgsql_db *)result->api.db; + int ret; + + driver_pgsql_stop_io(db); + + ret = PQflush(db->pg); + if (ret > 0) { + db->io = io_add(PQsocket(db->pg), IO_WRITE, + flush_callback, result); + db->io_dir = IO_WRITE; + return; + } + + if (ret < 0) { + result_finish(result); + } else { + /* all flushed */ + get_result(result); + } +} + +static void query_timeout(struct pgsql_result *result) +{ + struct pgsql_db *db = (struct pgsql_db *)result->api.db; + + driver_pgsql_stop_io(db); + + result->timeout = TRUE; + result_finish(result); +} + +static void do_query(struct pgsql_result *result, const char *query) +{ + struct pgsql_db *db = (struct pgsql_db *)result->api.db; + int ret; + + i_assert(SQL_DB_IS_READY(&db->api)); + i_assert(db->cur_result == NULL); + i_assert(db->io == NULL); + + driver_pgsql_set_state(db, SQL_DB_STATE_BUSY); + db->cur_result = result; + DLLIST_PREPEND(&db->pending_results, result); + result->to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000, + query_timeout, result); + result->query = i_strdup(query); + + if (PQsendQuery(db->pg, query) == 0 || + (ret = PQflush(db->pg)) < 0) { + /* failed to send query */ + result_finish(result); + return; + } + + if (ret > 0) { + /* write blocks */ + db->io = io_add(PQsocket(db->pg), IO_WRITE, + flush_callback, result); + db->io_dir = IO_WRITE; + } else { + get_result(result); + } +} + +static const char * +driver_pgsql_escape_string(struct sql_db *_db, const char *string) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + size_t len = strlen(string); + char *to; + +#ifdef HAVE_PQESCAPE_STRING_CONN + if (db->api.state == SQL_DB_STATE_DISCONNECTED) { + /* try connecting again */ + (void)sql_connect(&db->api); + } + if (db->api.state != SQL_DB_STATE_DISCONNECTED) { + int error; + + to = t_buffer_get(len * 2 + 1); + len = PQescapeStringConn(db->pg, to, string, len, &error); + } else +#endif + { + to = t_buffer_get(len * 2 + 1); + len = PQescapeString(to, string, len); + } + t_buffer_alloc(len + 1); + return to; +} + +static void exec_callback(struct sql_result *_result, + void *context ATTR_UNUSED) +{ + struct pgsql_result *result = (struct pgsql_result*)_result; + result_finish(result); +} + +static void driver_pgsql_exec(struct sql_db *db, const char *query) +{ + struct pgsql_result *result; + + result = i_new(struct pgsql_result, 1); + result->api = driver_pgsql_result; + result->api.db = db; + result->api.refcount = 1; + result->api.event = event_create(db->event); + result->callback = exec_callback; + do_query(result, query); +} + +static void driver_pgsql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) +{ + struct pgsql_result *result; + + result = i_new(struct pgsql_result, 1); + result->api = driver_pgsql_result; + result->api.db = db; + result->api.refcount = 1; + result->api.event = event_create(db->event); + result->callback = callback; + result->context = context; + do_query(result, query); +} + +static void pgsql_query_s_callback(struct sql_result *result, void *context) +{ + struct pgsql_db *db = context; + + db->sync_result = result; +} + +static void driver_pgsql_sync_init(struct pgsql_db *db) +{ + bool add_to_connect; + + db->orig_ioloop = current_ioloop; + if (db->io == NULL) { + db->ioloop = io_loop_create(); + return; + } + + i_assert(db->api.state == SQL_DB_STATE_CONNECTING); + + /* have to move our existing I/O and timeout handlers to new I/O loop */ + io_remove(&db->io); + + add_to_connect = (db->to_connect != NULL); + timeout_remove(&db->to_connect); + + db->ioloop = io_loop_create(); + if (add_to_connect) { + db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000, + driver_pgsql_connect_timeout, db); + } + db->io = io_add(PQsocket(db->pg), db->io_dir, connect_callback, db); + /* wait for connecting to finish */ + io_loop_run(db->ioloop); +} + +static void driver_pgsql_sync_deinit(struct pgsql_db *db) +{ + io_loop_destroy(&db->ioloop); +} + +static struct sql_result * +driver_pgsql_sync_query(struct pgsql_db *db, const char *query) +{ + struct sql_result *result; + + i_assert(db->sync_result == NULL); + + switch (db->api.state) { + case SQL_DB_STATE_CONNECTING: + case SQL_DB_STATE_BUSY: + i_unreached(); + case SQL_DB_STATE_DISCONNECTED: + sql_not_connected_result.refcount++; + return &sql_not_connected_result; + case SQL_DB_STATE_IDLE: + break; + } + + driver_pgsql_query(&db->api, query, pgsql_query_s_callback, db); + if (db->sync_result == NULL) + io_loop_run(db->ioloop); + + i_assert(db->io == NULL); + + result = db->sync_result; + if (result == &sql_not_connected_result) { + /* we don't end up in pgsql's free function, so sync_result + won't be set to NULL if we don't do it here. */ + db->sync_result = NULL; + } else if (result == NULL) { + result = &sql_not_connected_result; + result->refcount++; + } + + i_assert(db->io == NULL); + return result; +} + +static struct sql_result * +driver_pgsql_query_s(struct sql_db *_db, const char *query) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + struct sql_result *result; + + driver_pgsql_sync_init(db); + result = driver_pgsql_sync_query(db, query); + driver_pgsql_sync_deinit(db); + return result; +} + +static int driver_pgsql_result_next_row(struct sql_result *_result) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + struct pgsql_db *db = (struct pgsql_db *)_result->db; + + if (result->rows != 0) { + /* second time we're here */ + if (++result->rownum < result->rows) + return 1; + + /* end of this packet. see if there's more. FIXME: this may + block, but the current API doesn't provide a non-blocking + way to do this.. */ + PQclear(result->pgres); + result->pgres = PQgetResult(db->pg); + if (result->pgres == NULL) + return 0; + } + + if (result->pgres == NULL) { + _result->failed = TRUE; + return -1; + } + + switch (PQresultStatus(result->pgres)) { + case PGRES_COMMAND_OK: + /* no rows returned */ + return 0; + case PGRES_TUPLES_OK: + result->rows = PQntuples(result->pgres); + return result->rows > 0 ? 1 : 0; + case PGRES_EMPTY_QUERY: + case PGRES_NONFATAL_ERROR: + /* nonfatal error */ + _result->failed = TRUE; + return -1; + default: + /* treat as fatal error */ + _result->failed = TRUE; + db->fatal_error = TRUE; + return -1; + } +} + +static void driver_pgsql_result_fetch_fields(struct pgsql_result *result) +{ + unsigned int i; + + if (result->fields != NULL) + return; + + /* @UNSAFE */ + result->fields_count = PQnfields(result->pgres); + result->fields = i_new(const char *, result->fields_count); + for (i = 0; i < result->fields_count; i++) + result->fields[i] = PQfname(result->pgres, i); +} + +static unsigned int +driver_pgsql_result_get_fields_count(struct sql_result *_result) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + + driver_pgsql_result_fetch_fields(result); + return result->fields_count; +} + +static const char * +driver_pgsql_result_get_field_name(struct sql_result *_result, unsigned int idx) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + + driver_pgsql_result_fetch_fields(result); + i_assert(idx < result->fields_count); + return result->fields[idx]; +} + +static int driver_pgsql_result_find_field(struct sql_result *_result, + const char *field_name) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + unsigned int i; + + driver_pgsql_result_fetch_fields(result); + for (i = 0; i < result->fields_count; i++) { + if (strcmp(result->fields[i], field_name) == 0) + return i; + } + return -1; +} + +static const char * +driver_pgsql_result_get_field_value(struct sql_result *_result, + unsigned int idx) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + + if (PQgetisnull(result->pgres, result->rownum, idx) != 0) + return NULL; + + return PQgetvalue(result->pgres, result->rownum, idx); +} + +static const unsigned char * +driver_pgsql_result_get_field_value_binary(struct sql_result *_result, + unsigned int idx, size_t *size_r) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + const char *value; + struct pgsql_binary_value *binary_value; + + if (PQgetisnull(result->pgres, result->rownum, idx) != 0) { + *size_r = 0; + return NULL; + } + + value = PQgetvalue(result->pgres, result->rownum, idx); + + if (!array_is_created(&result->binary_values)) + i_array_init(&result->binary_values, idx + 1); + + binary_value = array_idx_get_space(&result->binary_values, idx); + if (binary_value->value == NULL) { + binary_value->value = + PQunescapeBytea((const unsigned char *)value, + &binary_value->size); + } + + *size_r = binary_value->size; + return binary_value->value; +} + +static const char * +driver_pgsql_result_find_field_value(struct sql_result *result, + const char *field_name) +{ + int idx; + + idx = driver_pgsql_result_find_field(result, field_name); + if (idx < 0) + return NULL; + return driver_pgsql_result_get_field_value(result, idx); +} + +static const char *const * +driver_pgsql_result_get_values(struct sql_result *_result) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + unsigned int i; + + if (result->values == NULL) { + driver_pgsql_result_fetch_fields(result); + result->values = i_new(const char *, result->fields_count); + } + + /* @UNSAFE */ + for (i = 0; i < result->fields_count; i++) { + result->values[i] = + driver_pgsql_result_get_field_value(_result, i); + } + + return result->values; +} + +static const char *driver_pgsql_result_get_error(struct sql_result *_result) +{ + struct pgsql_result *result = (struct pgsql_result *)_result; + struct pgsql_db *db = (struct pgsql_db *)_result->db; + const char *msg; + size_t len; + + i_free_and_null(db->error); + + if (result->timeout) { + db->error = i_strdup("Query timed out"); + } else if (result->pgres == NULL) { + /* connection error */ + db->error = i_strdup(last_error(db)); + } else { + msg = PQresultErrorMessage(result->pgres); + if (msg == NULL) + return "(no error set)"; + + /* Error message should contain trailing \n, we don't want it */ + len = strlen(msg); + db->error = len == 0 || msg[len-1] != '\n' ? + i_strdup(msg) : i_strndup(msg, len-1); + } + return db->error; +} + +static struct sql_transaction_context * +driver_pgsql_transaction_begin(struct sql_db *db) +{ + struct pgsql_transaction_context *ctx; + + ctx = i_new(struct pgsql_transaction_context, 1); + ctx->ctx.db = db; + ctx->ctx.event = event_create(db->event); + /* we need to be able to handle multiple open transactions, so at least + for now just keep them in memory until commit time. */ + ctx->query_pool = pool_alloconly_create("pgsql transaction", 1024); + return &ctx->ctx; +} + +static void +driver_pgsql_transaction_free(struct pgsql_transaction_context *ctx) +{ + pool_unref(&ctx->query_pool); + event_unref(&ctx->ctx.event); + i_free(ctx); +} + +static void +transaction_commit_callback(struct sql_result *result, + struct pgsql_transaction_context *ctx) +{ + struct sql_commit_result commit_result; + + i_zero(&commit_result); + if (sql_result_next_row(result) < 0) { + commit_result.error = sql_result_get_error(result); + commit_result.error_type = sql_result_get_error_type(result); + } + ctx->callback(&commit_result, ctx->context); + driver_pgsql_transaction_free(ctx); +} + +static bool transaction_send_next(void *context) +{ + struct pgsql_transaction_context *ctx = context; + + i_assert(!ctx->failed); + + if (ctx->ctx.db->state == SQL_DB_STATE_BUSY) { + /* kludgy.. */ + ctx->ctx.db->state = SQL_DB_STATE_IDLE; + } else if (!SQL_DB_IS_READY(ctx->ctx.db)) { + struct sql_commit_result commit_result = { + .error = "Not connected" + }; + ctx->callback(&commit_result, ctx->context); + return FALSE; + } + + if (ctx->ctx.head != NULL) { + struct sql_transaction_query *query = ctx->ctx.head; + + ctx->ctx.head = ctx->ctx.head->next; + sql_query(ctx->ctx.db, query->query, + transaction_update_callback, query); + } else { + sql_query(ctx->ctx.db, "COMMIT", + transaction_commit_callback, ctx); + } + return TRUE; +} + +static void +transaction_commit_error_callback(struct pgsql_transaction_context *ctx, + struct sql_result *result) +{ + struct sql_commit_result commit_result; + + i_zero(&commit_result); + commit_result.error = sql_result_get_error(result); + commit_result.error_type = sql_result_get_error_type(result); + e_debug(sql_transaction_finished_event(&ctx->ctx)-> + add_str("error", commit_result.error)->event(), + "Transaction failed: %s", commit_result.error); + ctx->callback(&commit_result, ctx->context); +} + +static void +transaction_begin_callback(struct sql_result *result, + struct pgsql_transaction_context *ctx) +{ + struct pgsql_db *db = (struct pgsql_db *)result->db; + + i_assert(result->db == ctx->ctx.db); + + if (sql_result_next_row(result) < 0) { + transaction_commit_error_callback(ctx, result); + driver_pgsql_transaction_free(ctx); + return; + } + i_assert(db->next_callback == NULL); + db->next_callback = transaction_send_next; + db->next_context = ctx; +} + +static void +transaction_update_callback(struct sql_result *result, + struct sql_transaction_query *query) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)query->trans; + struct pgsql_db *db = (struct pgsql_db *)result->db; + + if (sql_result_next_row(result) < 0) { + transaction_commit_error_callback(ctx, result); + driver_pgsql_transaction_free(ctx); + return; + } + + if (query->affected_rows != NULL) { + struct pgsql_result *pg_result = (struct pgsql_result *)result; + + if (str_to_uint(PQcmdTuples(pg_result->pgres), + query->affected_rows) < 0) + i_unreached(); + } + i_assert(db->next_callback == NULL); + db->next_callback = transaction_send_next; + db->next_context = ctx; +} + +static void +transaction_trans_query_callback(struct sql_result *result, + struct sql_transaction_query *query) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)query->trans; + struct sql_commit_result commit_result; + + if (sql_result_next_row(result) < 0) { + transaction_commit_error_callback(ctx, result); + driver_pgsql_transaction_free(ctx); + return; + } + + if (query->affected_rows != NULL) { + struct pgsql_result *pg_result = (struct pgsql_result *)result; + + if (str_to_uint(PQcmdTuples(pg_result->pgres), + query->affected_rows) < 0) + i_unreached(); + } + e_debug(sql_transaction_finished_event(&ctx->ctx)->event(), + "Transaction committed"); + i_zero(&commit_result); + ctx->callback(&commit_result, ctx->context); + driver_pgsql_transaction_free(ctx); +} + +static void +driver_pgsql_transaction_commit(struct sql_transaction_context *_ctx, + sql_commit_callback_t *callback, void *context) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)_ctx; + struct sql_commit_result result; + + i_zero(&result); + ctx->callback = callback; + ctx->context = context; + + if (ctx->failed || _ctx->head == NULL) { + if (ctx->failed) { + result.error = ctx->error; + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", ctx->error)->event(), + "Transaction failed: %s", ctx->error); + } else { + e_debug(sql_transaction_finished_event(_ctx)->event(), + "Transaction committed"); + } + callback(&result, context); + driver_pgsql_transaction_free(ctx); + } else if (_ctx->head->next == NULL) { + /* just a single query, send it */ + sql_query(_ctx->db, _ctx->head->query, + transaction_trans_query_callback, _ctx->head); + } else { + /* multiple queries, use a transaction */ + i_assert(_ctx->db->v.query == driver_pgsql_query); + sql_query(_ctx->db, "BEGIN", transaction_begin_callback, ctx); + } +} + +static void +commit_multi_fail(struct pgsql_transaction_context *ctx, + struct sql_result *result, const char *query) +{ + ctx->failed = TRUE; + ctx->error = t_strdup_printf("%s (query: %s)", + sql_result_get_error(result), query); + sql_result_unref(result); +} + +static struct sql_result * +driver_pgsql_transaction_commit_multi(struct pgsql_transaction_context *ctx) +{ + struct pgsql_db *db = (struct pgsql_db *)ctx->ctx.db; + struct sql_result *result; + struct sql_transaction_query *query; + + result = driver_pgsql_sync_query(db, "BEGIN"); + if (sql_result_next_row(result) < 0) { + commit_multi_fail(ctx, result, "BEGIN"); + return NULL; + } + sql_result_unref(result); + + /* send queries */ + for (query = ctx->ctx.head; query != NULL; query = query->next) { + result = driver_pgsql_sync_query(db, query->query); + if (sql_result_next_row(result) < 0) { + commit_multi_fail(ctx, result, query->query); + break; + } + if (query->affected_rows != NULL) { + struct pgsql_result *pg_result = + (struct pgsql_result *)result; + + if (str_to_uint(PQcmdTuples(pg_result->pgres), + query->affected_rows) < 0) + i_unreached(); + } + sql_result_unref(result); + } + + return driver_pgsql_sync_query(db, ctx->failed ? + "ROLLBACK" : "COMMIT"); +} + +static void +driver_pgsql_try_commit_s(struct pgsql_transaction_context *ctx, + const char **error_r) +{ + struct sql_transaction_context *_ctx = &ctx->ctx; + struct pgsql_db *db = (struct pgsql_db *)_ctx->db; + struct sql_transaction_query *single_query = NULL; + struct sql_result *result; + + if (_ctx->head->next == NULL) { + /* just a single query, send it */ + single_query = _ctx->head; + result = sql_query_s(_ctx->db, single_query->query); + } else { + /* multiple queries, use a transaction */ + driver_pgsql_sync_init(db); + result = driver_pgsql_transaction_commit_multi(ctx); + driver_pgsql_sync_deinit(db); + } + + if (ctx->failed) { + i_assert(ctx->error != NULL); + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", ctx->error)->event(), + "Transaction failed: %s", ctx->error); + *error_r = ctx->error; + } else if (result != NULL) { + if (sql_result_next_row(result) < 0) + *error_r = sql_result_get_error(result); + else if (single_query != NULL && + single_query->affected_rows != NULL) { + struct pgsql_result *pg_result = + (struct pgsql_result *)result; + + if (str_to_uint(PQcmdTuples(pg_result->pgres), + single_query->affected_rows) < 0) + i_unreached(); + } + } + + if (!ctx->failed) { + e_debug(sql_transaction_finished_event(_ctx)->event(), + "Transaction committed"); + } + + if (result != NULL) + sql_result_unref(result); +} + +static int +driver_pgsql_transaction_commit_s(struct sql_transaction_context *_ctx, + const char **error_r) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)_ctx; + struct pgsql_db *db = (struct pgsql_db *)_ctx->db; + + *error_r = NULL; + + if (_ctx->head != NULL) { + driver_pgsql_try_commit_s(ctx, error_r); + if (_ctx->db->state == SQL_DB_STATE_DISCONNECTED) { + *error_r = t_strdup(*error_r); + e_info(db->api.event, "Disconnected from database, " + "retrying commit"); + if (sql_connect(_ctx->db) >= 0) { + ctx->failed = FALSE; + *error_r = NULL; + driver_pgsql_try_commit_s(ctx, error_r); + } + } + } + + driver_pgsql_transaction_free(ctx); + return *error_r == NULL ? 0 : -1; +} + +static void +driver_pgsql_transaction_rollback(struct sql_transaction_context *_ctx) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)_ctx; + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", "Rolled back")->event(), + "Transaction rolled back"); + + driver_pgsql_transaction_free(ctx); +} + +static void +driver_pgsql_update(struct sql_transaction_context *_ctx, const char *query, + unsigned int *affected_rows) +{ + struct pgsql_transaction_context *ctx = + (struct pgsql_transaction_context *)_ctx; + + sql_transaction_add_query(_ctx, ctx->query_pool, query, affected_rows); +} + +static const char * +driver_pgsql_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + string_t *str = t_str_new(128); + + str_append(str, "E'\\\\x"); + binary_to_hex_append(str, data, size); + str_append_c(str, '\''); + return str_c(str); +} + +static bool driver_pgsql_have_work(struct pgsql_db *db) +{ + return db->next_callback != NULL || db->pending_results != NULL || + db->api.state == SQL_DB_STATE_CONNECTING; +} + +static void driver_pgsql_wait(struct sql_db *_db) +{ + struct pgsql_db *db = (struct pgsql_db *)_db; + + if (!driver_pgsql_have_work(db)) + return; + + db->orig_ioloop = current_ioloop; + db->ioloop = io_loop_create(); + db->io = io_loop_move_io(&db->io); + while (driver_pgsql_have_work(db)) + io_loop_run(db->ioloop); + + io_loop_set_current(db->orig_ioloop); + db->io = io_loop_move_io(&db->io); + io_loop_set_current(db->ioloop); + io_loop_destroy(&db->ioloop); +} + +const struct sql_db driver_pgsql_db = { + .name = "pgsql", + .flags = SQL_DB_FLAG_POOLED, + + .v = { + .get_flags = driver_pgsql_get_flags, + .init_full = driver_pgsql_init_full_v, + .deinit = driver_pgsql_deinit_v, + .connect = driver_pgsql_connect, + .disconnect = driver_pgsql_disconnect, + .escape_string = driver_pgsql_escape_string, + .exec = driver_pgsql_exec, + .query = driver_pgsql_query, + .query_s = driver_pgsql_query_s, + .wait = driver_pgsql_wait, + + .transaction_begin = driver_pgsql_transaction_begin, + .transaction_commit = driver_pgsql_transaction_commit, + .transaction_commit_s = driver_pgsql_transaction_commit_s, + .transaction_rollback = driver_pgsql_transaction_rollback, + + .update = driver_pgsql_update, + + .escape_blob = driver_pgsql_escape_blob, + } +}; + +const struct sql_result driver_pgsql_result = { + .v = { + .free = driver_pgsql_result_free, + .next_row = driver_pgsql_result_next_row, + .get_fields_count = driver_pgsql_result_get_fields_count, + .get_field_name = driver_pgsql_result_get_field_name, + .find_field = driver_pgsql_result_find_field, + .get_field_value = driver_pgsql_result_get_field_value, + .get_field_value_binary = driver_pgsql_result_get_field_value_binary, + .find_field_value = driver_pgsql_result_find_field_value, + .get_values = driver_pgsql_result_get_values, + .get_error = driver_pgsql_result_get_error, + } +}; + +const char *driver_pgsql_version = DOVECOT_ABI_VERSION; + +void driver_pgsql_init(void); +void driver_pgsql_deinit(void); + +void driver_pgsql_init(void) +{ + sql_driver_register(&driver_pgsql_db); +} + +void driver_pgsql_deinit(void) +{ + sql_driver_unregister(&driver_pgsql_db); +} + +#endif diff --git a/src/lib-sql/driver-sqlite.c b/src/lib-sql/driver-sqlite.c new file mode 100644 index 0000000..e05ed18 --- /dev/null +++ b/src/lib-sql/driver-sqlite.c @@ -0,0 +1,555 @@ +/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "ioloop.h" +#include "str.h" +#include "hex-binary.h" +#include "sql-api-private.h" + +#ifdef BUILD_SQLITE +#include <sqlite3.h> + +/* retry time if db is busy (in ms) */ +static const int sqlite_busy_timeout = 1000; + +struct sqlite_db { + struct sql_db api; + + pool_t pool; + const char *dbfile; + sqlite3 *sqlite; + bool connected:1; + int rc; +}; + +struct sqlite_result { + struct sql_result api; + sqlite3_stmt *stmt; + unsigned int cols; + const char **row; +}; + +struct sqlite_transaction_context { + struct sql_transaction_context ctx; + bool failed:1; +}; + +extern const struct sql_db driver_sqlite_db; +extern const struct sql_result driver_sqlite_result; +extern const struct sql_result driver_sqlite_error_result; + +static struct event_category event_category_sqlite = { + .parent = &event_category_sql, + .name = "sqlite" +}; + +static int driver_sqlite_connect(struct sql_db *_db) +{ + struct sqlite_db *db = (struct sqlite_db *)_db; + + if (db->connected) + return 1; + + db->rc = sqlite3_open(db->dbfile, &db->sqlite); + + if (db->rc == SQLITE_OK) { + db->connected = TRUE; + sqlite3_busy_timeout(db->sqlite, sqlite_busy_timeout); + return 1; + } else { + e_error(_db->event, "open(%s) failed: %s", db->dbfile, + sqlite3_errmsg(db->sqlite)); + sqlite3_close(db->sqlite); + db->sqlite = NULL; + return -1; + } +} + +static void driver_sqlite_disconnect(struct sql_db *_db) +{ + struct sqlite_db *db = (struct sqlite_db *)_db; + + sqlite3_close(db->sqlite); + db->sqlite = NULL; +} + +static int driver_sqlite_init_full_v(const struct sql_settings *set, struct sql_db **db_r, + const char **error_r ATTR_UNUSED) +{ + struct sqlite_db *db; + pool_t pool; + + pool = pool_alloconly_create("sqlite driver", 512); + db = p_new(pool, struct sqlite_db, 1); + db->pool = pool; + db->api = driver_sqlite_db; + db->dbfile = p_strdup(db->pool, set->connect_string); + db->connected = FALSE; + db->api.event = event_create(set->event_parent); + event_add_category(db->api.event, &event_category_sqlite); + event_set_append_log_prefix(db->api.event, "sqlite: "); + + *db_r = &db->api; + return 0; +} + +static void driver_sqlite_deinit_v(struct sql_db *_db) +{ + struct sqlite_db *db = (struct sqlite_db *)_db; + + _db->no_reconnect = TRUE; + sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED); + + sqlite3_close(db->sqlite); + sql_connection_log_finished(_db); + event_unref(&_db->event); + array_free(&_db->module_contexts); + pool_unref(&db->pool); +} + +static const char * +driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED, + const char *string) +{ + const char *p; + char *dest, *destbegin; + + /* find the first ' */ + for (p = string; *p != '\''; p++) { + if (*p == '\0') + return t_strdup_noconst(string); + } + + /* @UNSAFE: escape ' with '' */ + dest = destbegin = t_buffer_get((p - string) + strlen(string) * 2 + 1); + + memcpy(dest, string, p - string); + dest += p - string; + + for (; *p != '\0'; p++) { + *dest++ = *p; + if (*p == '\'') + *dest++ = *p; + } + *dest++ = '\0'; + t_buffer_alloc(dest - destbegin); + + return destbegin; +} + +static void driver_sqlite_result_log(const struct sql_result *result, const char *query) +{ + struct sqlite_db *db = (struct sqlite_db *)result->db; + bool success = db->connected && db->rc == SQLITE_OK; + int duration; + const char *suffix = ""; + struct event_passthrough *e = + sql_query_finished_event(&db->api, result->event, query, success, + &duration); + io_loop_time_refresh(); + + if (!db->connected) { + suffix = ": Cannot connect to database"; + e->add_str("error", "Cannot connect to database"); + } else if (db->rc != SQLITE_OK) { + suffix = t_strdup_printf(": %s (%d)", sqlite3_errmsg(db->sqlite), + db->rc); + e->add_str("error", sqlite3_errmsg(db->sqlite)); + e->add_int("error_code", db->rc); + } + + e_debug(e->event(), SQL_QUERY_FINISHED_FMT"%s", query, duration, suffix); +} + +static void driver_sqlite_exec(struct sql_db *_db, const char *query) +{ + struct sqlite_db *db = (struct sqlite_db *)_db; + struct sql_result result; + + i_zero(&result); + result.db = _db; + result.event = event_create(_db->event); + + /* Other drivers do not include time spent connecting + but this simplifies error logging, so we include + it here. */ + if (driver_sqlite_connect(_db) < 0) { + driver_sqlite_result_log(&result, query); + } else { + db->rc = sqlite3_exec(db->sqlite, query, NULL, NULL, NULL); + driver_sqlite_result_log(&result, query); + } + + event_unref(&result.event); +} + +static void driver_sqlite_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) +{ + struct sql_result *result; + + result = sql_query_s(db, query); + result->callback = TRUE; + callback(result, context); + result->callback = FALSE; + sql_result_unref(result); +} + +static struct sql_result * +driver_sqlite_query_s(struct sql_db *_db, const char *query) +{ + struct sqlite_db *db = (struct sqlite_db *)_db; + struct sqlite_result *result; + struct event *event; + + result = i_new(struct sqlite_result, 1); + result->api.db = _db; + /* Temporarily store the event since result->api gets + * overwritten later here and we need to reset it. */ + event = event_create(_db->event); + result->api.event = event; + + if (driver_sqlite_connect(_db) < 0) { + driver_sqlite_result_log(&result->api, query); + result->api = driver_sqlite_error_result; + result->stmt = NULL; + result->cols = 0; + } else { + db->rc = sqlite3_prepare(db->sqlite, query, -1, &result->stmt, NULL); + driver_sqlite_result_log(&result->api, query); + if (db->rc == SQLITE_OK) { + result->api = driver_sqlite_result; + result->cols = sqlite3_column_count(result->stmt); + result->row = i_new(const char *, result->cols); + } else { + result->api = driver_sqlite_error_result; + result->stmt = NULL; + result->cols = 0; + } + } + + result->api.db = _db; + result->api.refcount = 1; + result->api.event = event; + return &result->api; +} + +static void driver_sqlite_result_free(struct sql_result *_result) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + struct sqlite_db *db = (struct sqlite_db *) result->api.db; + int rc; + + if (_result->callback) + return; + + if (result->stmt != NULL) { + if ((rc = sqlite3_finalize(result->stmt)) != SQLITE_OK) { + e_warning(_result->event, "finalize failed: %s (%d)", + sqlite3_errmsg(db->sqlite), rc); + } + i_free(result->row); + } + event_unref(&result->api.event); + i_free(result); +} + +static int driver_sqlite_result_next_row(struct sql_result *_result) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + + switch (sqlite3_step(result->stmt)) { + case SQLITE_ROW: + return 1; + case SQLITE_DONE: + return 0; + default: + return -1; + } +} + +static unsigned int +driver_sqlite_result_get_fields_count(struct sql_result *_result) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + + return result->cols; +} + +static const char * +driver_sqlite_result_get_field_name(struct sql_result *_result, + unsigned int idx) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + + return sqlite3_column_name(result->stmt, idx); +} + +static int driver_sqlite_result_find_field(struct sql_result *_result, + const char *field_name) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + unsigned int i; + + for (i = 0; i < result->cols; ++i) { + const char *col = sqlite3_column_name(result->stmt, i); + + if (strcmp(col, field_name) == 0) + return i; + } + + return -1; +} + +static const char * +driver_sqlite_result_get_field_value(struct sql_result *_result, + unsigned int idx) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + + return (const char*)sqlite3_column_text(result->stmt, idx); +} + +static const unsigned char * +driver_sqlite_result_get_field_value_binary(struct sql_result *_result, + unsigned int idx, size_t *size_r) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + + *size_r = sqlite3_column_bytes(result->stmt, idx); + return sqlite3_column_blob(result->stmt, idx); +} + +static const char * +driver_sqlite_result_find_field_value(struct sql_result *result, + const char *field_name) +{ + int idx; + + idx = driver_sqlite_result_find_field(result, field_name); + if (idx < 0) + return NULL; + return driver_sqlite_result_get_field_value(result, idx); +} + +static const char *const * +driver_sqlite_result_get_values(struct sql_result *_result) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + unsigned int i; + + for (i = 0; i < result->cols; ++i) { + result->row[i] = + driver_sqlite_result_get_field_value(_result, i); + } + + return (const char *const *)result->row; +} + +static const char *driver_sqlite_result_get_error(struct sql_result *_result) +{ + struct sqlite_result *result = (struct sqlite_result *)_result; + struct sqlite_db *db = (struct sqlite_db *)result->api.db; + + if (db->connected) + return sqlite3_errmsg(db->sqlite); + else + return "Cannot connect to database"; +} + +static struct sql_transaction_context * +driver_sqlite_transaction_begin(struct sql_db *_db) +{ + struct sqlite_transaction_context *ctx; + struct sqlite_db *db = (struct sqlite_db *)_db; + + ctx = i_new(struct sqlite_transaction_context, 1); + ctx->ctx.db = _db; + ctx->ctx.event = event_create(_db->event); + + sql_exec(_db, "BEGIN TRANSACTION"); + if (db->rc != SQLITE_OK) + ctx->failed = TRUE; + + return &ctx->ctx; +} + +static void +driver_sqlite_transaction_rollback(struct sql_transaction_context *_ctx) +{ + struct sqlite_transaction_context *ctx = + (struct sqlite_transaction_context *)_ctx; + + if (!ctx->failed) { + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", "Rolled back")->event(), + "Transaction rolled back"); + } + sql_exec(_ctx->db, "ROLLBACK"); + event_unref(&_ctx->event); + i_free(ctx); +} + +static void +driver_sqlite_transaction_commit(struct sql_transaction_context *_ctx, + sql_commit_callback_t *callback, void *context) +{ + struct sqlite_transaction_context *ctx = + (struct sqlite_transaction_context *)_ctx; + struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db; + struct sql_commit_result commit_result; + + if (!ctx->failed) { + sql_exec(_ctx->db, "COMMIT"); + if (db->rc != SQLITE_OK) + ctx->failed = TRUE; + } + + i_zero(&commit_result); + if (ctx->failed) { + commit_result.error = sqlite3_errmsg(db->sqlite); + callback(&commit_result, context); + e_debug(sql_transaction_finished_event(_ctx)-> + add_str("error", commit_result.error)->event(), + "Transaction failed"); + /* From SQLite manual: It is recommended that applications + respond to the errors listed above by explicitly issuing a + ROLLBACK command. If the transaction has already been rolled + back automatically by the error response, then the ROLLBACK + command will fail with an error, but no harm is caused by + this. */ + driver_sqlite_transaction_rollback(_ctx); + } else { + e_debug(sql_transaction_finished_event(_ctx)->event(), + "Transaction committed"); + callback(&commit_result, context); + event_unref(&_ctx->event); + i_free(ctx); + } +} + +static int +driver_sqlite_transaction_commit_s(struct sql_transaction_context *_ctx, + const char **error_r) +{ + struct sqlite_transaction_context *ctx = + (struct sqlite_transaction_context *)_ctx; + struct sqlite_db *db = (struct sqlite_db *) ctx->ctx.db; + + if (ctx->failed) { + /* also does i_free(ctx) */ + driver_sqlite_transaction_rollback(_ctx); + return -1; + } + + sql_exec(_ctx->db, "COMMIT"); + *error_r = sqlite3_errmsg(db->sqlite); + i_free(ctx); + return 0; +} + +static void +driver_sqlite_update(struct sql_transaction_context *_ctx, const char *query, + unsigned int *affected_rows) +{ + struct sqlite_transaction_context *ctx = + (struct sqlite_transaction_context *)_ctx; + struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db; + + if (ctx->failed) + return; + + sql_exec(_ctx->db, query); + if (db->rc != SQLITE_OK) + ctx->failed = TRUE; + else if (affected_rows != NULL) + *affected_rows = sqlite3_changes(db->sqlite); +} + +static const char * +driver_sqlite_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + string_t *str = t_str_new(128); + + str_append(str, "x'"); + binary_to_hex_append(str, data, size); + str_append_c(str, '\''); + return str_c(str); +} + +const struct sql_db driver_sqlite_db = { + .name = "sqlite", + .flags = +#if SQLITE_VERSION_NUMBER >= 3024000 + SQL_DB_FLAG_ON_CONFLICT_DO | +#endif + SQL_DB_FLAG_BLOCKING, + + .v = { + .init_full = driver_sqlite_init_full_v, + .deinit = driver_sqlite_deinit_v, + .connect = driver_sqlite_connect, + .disconnect = driver_sqlite_disconnect, + .escape_string = driver_sqlite_escape_string, + .exec = driver_sqlite_exec, + .query = driver_sqlite_query, + .query_s = driver_sqlite_query_s, + + .transaction_begin = driver_sqlite_transaction_begin, + .transaction_commit = driver_sqlite_transaction_commit, + .transaction_commit_s = driver_sqlite_transaction_commit_s, + .transaction_rollback = driver_sqlite_transaction_rollback, + + .update = driver_sqlite_update, + + .escape_blob = driver_sqlite_escape_blob, + } +}; + +const struct sql_result driver_sqlite_result = { + .v = { + .free = driver_sqlite_result_free, + .next_row = driver_sqlite_result_next_row, + .get_fields_count = driver_sqlite_result_get_fields_count, + .get_field_name = driver_sqlite_result_get_field_name, + .find_field = driver_sqlite_result_find_field, + .get_field_value = driver_sqlite_result_get_field_value, + .get_field_value_binary = driver_sqlite_result_get_field_value_binary, + .find_field_value = driver_sqlite_result_find_field_value, + .get_values = driver_sqlite_result_get_values, + .get_error = driver_sqlite_result_get_error, + } +}; + +static int +driver_sqlite_result_error_next_row(struct sql_result *result ATTR_UNUSED) +{ + return -1; +} + +const struct sql_result driver_sqlite_error_result = { + .v = { + .free = driver_sqlite_result_free, + .next_row = driver_sqlite_result_error_next_row, + .get_error = driver_sqlite_result_get_error, + } +}; + +const char *driver_sqlite_version = DOVECOT_ABI_VERSION; + +void driver_sqlite_init(void); +void driver_sqlite_deinit(void); + +void driver_sqlite_init(void) +{ + sql_driver_register(&driver_sqlite_db); +} + +void driver_sqlite_deinit(void) +{ + sql_driver_unregister(&driver_sqlite_db); +} + +#endif diff --git a/src/lib-sql/driver-sqlpool.c b/src/lib-sql/driver-sqlpool.c new file mode 100644 index 0000000..553b2a0 --- /dev/null +++ b/src/lib-sql/driver-sqlpool.c @@ -0,0 +1,934 @@ +/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "llist.h" +#include "ioloop.h" +#include "sql-api-private.h" + +#include <time.h> + +#define QUERY_TIMEOUT_SECS 6 + +/* sqlpool events are separate from category:sql, because + they are usually not very interesting, and would only + make logging too noisy. They can be enabled explicitly. +*/ +static struct event_category event_category_sqlpool = { + .name = "sqlpool", +}; + +struct sqlpool_host { + char *connect_string; + + unsigned int connection_count; +}; + +struct sqlpool_connection { + struct sql_db *db; + unsigned int host_idx; +}; + +struct sqlpool_db { + struct sql_db api; + + pool_t pool; + const struct sql_db *driver; + unsigned int connection_limit; + + ARRAY(struct sqlpool_host) hosts; + /* all connections from all hosts */ + ARRAY(struct sqlpool_connection) all_connections; + /* index of last connection in all_connections that was used to + send a query. */ + unsigned int last_query_conn_idx; + + /* queued requests */ + struct sqlpool_request *requests_head, *requests_tail; + struct timeout *request_to; +}; + +struct sqlpool_request { + struct sqlpool_request *prev, *next; + + struct sqlpool_db *db; + time_t created; + + unsigned int host_idx; + unsigned int retry_count; + + struct event *event; + + /* requests are a) queries */ + char *query; + sql_query_callback_t *callback; + void *context; + + /* b) transaction waiters */ + struct sqlpool_transaction_context *trans; +}; + +struct sqlpool_transaction_context { + struct sql_transaction_context ctx; + + sql_commit_callback_t *callback; + void *context; + + pool_t query_pool; + struct sqlpool_request *commit_request; +}; + +extern struct sql_db driver_sqlpool_db; + +static struct sqlpool_connection * +sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host, + unsigned int host_idx); +static void +driver_sqlpool_query_callback(struct sql_result *result, + struct sqlpool_request *request); +static void +driver_sqlpool_commit_callback(const struct sql_commit_result *result, + struct sqlpool_transaction_context *ctx); +static void driver_sqlpool_deinit(struct sql_db *_db); + +static struct sqlpool_request * ATTR_NULL(2) +sqlpool_request_new(struct sqlpool_db *db, const char *query) +{ + struct sqlpool_request *request; + + request = i_new(struct sqlpool_request, 1); + request->db = db; + request->created = time(NULL); + request->query = i_strdup(query); + request->event = event_create(db->api.event); + return request; +} + +static void +sqlpool_request_free(struct sqlpool_request **_request) +{ + struct sqlpool_request *request = *_request; + + *_request = NULL; + + i_assert(request->prev == NULL && request->next == NULL); + event_unref(&request->event); + i_free(request->query); + i_free(request); +} + +static void +sqlpool_request_abort(struct sqlpool_request **_request) +{ + struct sqlpool_request *request = *_request; + + *_request = NULL; + + if (request->callback != NULL) + request->callback(&sql_not_connected_result, request->context); + + i_assert(request->prev != NULL || + request->db->requests_head == request); + DLLIST2_REMOVE(&request->db->requests_head, + &request->db->requests_tail, request); + sqlpool_request_free(&request); +} + +static struct sql_transaction_context * +driver_sqlpool_new_conn_trans(struct sqlpool_transaction_context *trans, + struct sql_db *conndb) +{ + struct sql_transaction_context *conn_trans; + struct sql_transaction_query *query; + + conn_trans = sql_transaction_begin(conndb); + /* backend will use our queries list (we might still append more + queries to the list) */ + conn_trans->head = trans->ctx.head; + conn_trans->tail = trans->ctx.tail; + for (query = conn_trans->head; query != NULL; query = query->next) + query->trans = conn_trans; + return conn_trans; +} + +static void +sqlpool_request_handle_transaction(struct sql_db *conndb, + struct sqlpool_transaction_context *trans) +{ + struct sql_transaction_context *conn_trans; + + sqlpool_request_free(&trans->commit_request); + conn_trans = driver_sqlpool_new_conn_trans(trans, conndb); + sql_transaction_commit(&conn_trans, + driver_sqlpool_commit_callback, trans); +} + +static void +sqlpool_request_send_next(struct sqlpool_db *db, struct sql_db *conndb) +{ + struct sqlpool_request *request; + + if (db->requests_head == NULL || !SQL_DB_IS_READY(conndb)) + return; + + request = db->requests_head; + DLLIST2_REMOVE(&db->requests_head, &db->requests_tail, request); + timeout_reset(db->request_to); + + if (request->query != NULL) { + sql_query(conndb, request->query, + driver_sqlpool_query_callback, request); + } else if (request->trans != NULL) { + sqlpool_request_handle_transaction(conndb, request->trans); + } else { + i_unreached(); + } +} + +static void sqlpool_reconnect(struct sql_db *conndb) +{ + timeout_remove(&conndb->to_reconnect); + (void)sql_connect(conndb); +} + +static struct sqlpool_host * +sqlpool_find_host_with_least_connections(struct sqlpool_db *db, + unsigned int *host_idx_r) +{ + struct sqlpool_host *hosts, *min = NULL; + unsigned int i, count; + + hosts = array_get_modifiable(&db->hosts, &count); + i_assert(count > 0); + + min = &hosts[0]; + *host_idx_r = 0; + + for (i = 1; i < count; i++) { + if (min->connection_count > hosts[i].connection_count) { + min = &hosts[i]; + *host_idx_r = i; + } + } + return min; +} + +static bool sqlpool_have_successful_connections(struct sqlpool_db *db) +{ + const struct sqlpool_connection *conn; + + array_foreach(&db->all_connections, conn) { + if (conn->db->state >= SQL_DB_STATE_IDLE) + return TRUE; + } + return FALSE; +} + +static void +sqlpool_handle_connect_failed(struct sqlpool_db *db, struct sql_db *conndb) +{ + struct sqlpool_host *host; + unsigned int host_idx; + + if (conndb->connect_failure_count > 0) { + /* increase delay between reconnections to this + server */ + conndb->connect_delay *= 5; + if (conndb->connect_delay > SQL_CONNECT_MAX_DELAY) + conndb->connect_delay = SQL_CONNECT_MAX_DELAY; + } + conndb->connect_failure_count++; + + /* reconnect after the delay */ + timeout_remove(&conndb->to_reconnect); + conndb->to_reconnect = timeout_add(conndb->connect_delay * 1000, + sqlpool_reconnect, conndb); + + /* if we have zero successful hosts and there still are hosts + without connections, connect to one of them. */ + if (!sqlpool_have_successful_connections(db)) { + host = sqlpool_find_host_with_least_connections(db, &host_idx); + if (host->connection_count == 0) + (void)sqlpool_add_connection(db, host, host_idx); + } +} + +static void +sqlpool_state_changed(struct sql_db *conndb, enum sql_db_state prev_state, + void *context) +{ + struct sqlpool_db *db = context; + + if (conndb->state == SQL_DB_STATE_IDLE) { + conndb->connect_failure_count = 0; + conndb->connect_delay = SQL_CONNECT_MIN_DELAY; + sqlpool_request_send_next(db, conndb); + } + + if (prev_state == SQL_DB_STATE_CONNECTING && + conndb->state == SQL_DB_STATE_DISCONNECTED && + !conndb->no_reconnect) + sqlpool_handle_connect_failed(db, conndb); +} + +static struct sqlpool_connection * +sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host, + unsigned int host_idx) +{ + struct sql_db *conndb; + struct sqlpool_connection *conn; + const char *error; + int ret = 0; + + host->connection_count++; + + e_debug(db->api.event, "Creating new connection"); + + if (db->driver->v.init_full == NULL) { + conndb = db->driver->v.init(host->connect_string); + } else { + struct sql_settings set = { + .connect_string = host->connect_string, + .event_parent = event_get_parent(db->api.event), + }; + ret = db->driver->v.init_full(&set, &conndb, &error); + } + if (ret < 0) + i_fatal("sqlpool: %s", error); + + sql_init_common(conndb); + + conndb->state_change_callback = sqlpool_state_changed; + conndb->state_change_context = db; + conndb->connect_delay = SQL_CONNECT_MIN_DELAY; + + conn = array_append_space(&db->all_connections); + conn->host_idx = host_idx; + conn->db = conndb; + return conn; +} + +static struct sqlpool_connection * +sqlpool_add_new_connection(struct sqlpool_db *db) +{ + struct sqlpool_host *host; + unsigned int host_idx; + + host = sqlpool_find_host_with_least_connections(db, &host_idx); + if (host->connection_count >= db->connection_limit) + return NULL; + else + return sqlpool_add_connection(db, host, host_idx); +} + +static const struct sqlpool_connection * +sqlpool_find_available_connection(struct sqlpool_db *db, + unsigned int unwanted_host_idx, + bool *all_disconnected_r) +{ + const struct sqlpool_connection *conns; + unsigned int i, count; + + *all_disconnected_r = TRUE; + + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + unsigned int idx = (i + db->last_query_conn_idx + 1) % count; + struct sql_db *conndb = conns[idx].db; + + if (conns[idx].host_idx == unwanted_host_idx) + continue; + + if (!SQL_DB_IS_READY(conndb) && conndb->to_reconnect == NULL) { + /* see if we could reconnect to it immediately */ + (void)sql_connect(conndb); + } + if (SQL_DB_IS_READY(conndb)) { + db->last_query_conn_idx = idx; + *all_disconnected_r = FALSE; + return &conns[idx]; + } + if (conndb->state != SQL_DB_STATE_DISCONNECTED) + *all_disconnected_r = FALSE; + } + return NULL; +} + +static bool +driver_sqlpool_get_connection(struct sqlpool_db *db, + unsigned int unwanted_host_idx, + const struct sqlpool_connection **conn_r) +{ + const struct sqlpool_connection *conn, *conns; + unsigned int i, count; + bool all_disconnected; + + conn = sqlpool_find_available_connection(db, unwanted_host_idx, + &all_disconnected); + if (conn == NULL && unwanted_host_idx != UINT_MAX) { + /* maybe there are no wanted hosts. use any of them. */ + conn = sqlpool_find_available_connection(db, UINT_MAX, + &all_disconnected); + } + if (conn == NULL && all_disconnected) { + /* no connected connections. connect_delays may have gotten too + high, reset all of them to see if some are still alive. */ + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + struct sql_db *conndb = conns[i].db; + + if (conndb->connect_delay > SQL_CONNECT_RESET_DELAY) + conndb->connect_delay = SQL_CONNECT_RESET_DELAY; + } + conn = sqlpool_find_available_connection(db, UINT_MAX, + &all_disconnected); + } + if (conn == NULL) { + /* still nothing. try creating new connections */ + conn = sqlpool_add_new_connection(db); + if (conn != NULL) + (void)sql_connect(conn->db); + if (conn == NULL || !SQL_DB_IS_READY(conn->db)) + return FALSE; + } + *conn_r = conn; + return TRUE; +} + +static bool +driver_sqlpool_get_sync_connection(struct sqlpool_db *db, + const struct sqlpool_connection **conn_r) +{ + const struct sqlpool_connection *conns; + unsigned int i, count; + + if (driver_sqlpool_get_connection(db, UINT_MAX, conn_r)) + return TRUE; + + /* no idling connections, but maybe we can find one that's trying to + connect to server, and we can use it once it's finished */ + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + if (conns[i].db->state == SQL_DB_STATE_CONNECTING) { + *conn_r = &conns[i]; + return TRUE; + } + } + return FALSE; +} + +static bool +driver_sqlpool_get_connected_flags(struct sqlpool_db *db, + enum sql_db_flags *flags_r) +{ + const struct sqlpool_connection *conn; + + array_foreach(&db->all_connections, conn) { + if (conn->db->state > SQL_DB_STATE_CONNECTING) { + *flags_r = sql_get_flags(conn->db); + return TRUE; + } + } + return FALSE; +} + +static enum sql_db_flags driver_sqlpool_get_flags(struct sql_db *_db) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conn; + enum sql_db_flags flags; + + /* try to use a connected db */ + if (driver_sqlpool_get_connected_flags(db, &flags)) + return flags; + + if (!driver_sqlpool_get_sync_connection(db, &conn)) { + /* Failed to connect to database. Just use the first + connection. */ + conn = array_idx(&db->all_connections, 0); + } + return sql_get_flags(conn->db); +} + +static int +driver_sqlpool_parse_hosts(struct sqlpool_db *db, const char *connect_string, + const char **error_r) +{ + const char *const *args, *key, *value, *hostname; + struct sqlpool_host *host; + ARRAY_TYPE(const_string) hostnames, connect_args; + + t_array_init(&hostnames, 8); + t_array_init(&connect_args, 32); + + /* connect string is a space separated list. it may contain + backend-specific strings which we'll pass as-is. we'll only care + about our own settings, plus the host settings. */ + args = t_strsplit_spaces(connect_string, " "); + for (; *args != NULL; args++) { + value = strchr(*args, '='); + if (value == NULL) { + key = *args; + value = ""; + } else { + key = t_strdup_until(*args, value); + value++; + } + + if (strcmp(key, "maxconns") == 0) { + if (str_to_uint(value, &db->connection_limit) < 0) { + *error_r = t_strdup_printf("Invalid value for maxconns: %s", + value); + return -1; + } + } else if (strcmp(key, "host") == 0) { + array_push_back(&hostnames, &value); + } else { + array_push_back(&connect_args, args); + } + } + + /* build a new connect string without our settings or hosts */ + array_append_zero(&connect_args); + connect_string = t_strarray_join(array_front(&connect_args), " "); + + if (array_count(&hostnames) == 0) { + /* no hosts specified. create a default one. */ + host = array_append_space(&db->hosts); + host->connect_string = i_strdup(connect_string); + } else { + if (*connect_string == '\0') + connect_string = NULL; + + array_foreach_elem(&hostnames, hostname) { + host = array_append_space(&db->hosts); + host->connect_string = + i_strconcat("host=", hostname, " ", + connect_string, NULL); + } + } + + if (db->connection_limit == 0) + db->connection_limit = SQL_DEFAULT_CONNECTION_LIMIT; + return 0; +} + +static void sqlpool_add_all_once(struct sqlpool_db *db) +{ + struct sqlpool_host *host; + unsigned int host_idx; + + for (;;) { + host = sqlpool_find_host_with_least_connections(db, &host_idx); + if (host->connection_count > 0) + break; + (void)sqlpool_add_connection(db, host, host_idx); + } +} + +int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver, + struct sql_db **db_r, const char **error_r) +{ + struct sqlpool_db *db; + int ret; + + db = i_new(struct sqlpool_db, 1); + db->driver = driver; + db->api = driver_sqlpool_db; + db->api.flags = driver->flags; + db->api.event = event_create(set->event_parent); + event_add_category(db->api.event, &event_category_sqlpool); + event_set_append_log_prefix(db->api.event, + t_strdup_printf("sqlpool(%s): ", driver->name)); + i_array_init(&db->hosts, 8); + + T_BEGIN { + ret = driver_sqlpool_parse_hosts(db, set->connect_string, + error_r); + } T_END_PASS_STR_IF(ret < 0, error_r); + + if (ret < 0) { + driver_sqlpool_deinit(&db->api); + return ret; + } + i_array_init(&db->all_connections, 16); + /* connect to all databases so we can do load balancing immediately */ + sqlpool_add_all_once(db); + + *db_r = &db->api; + return 0; +} + +static void driver_sqlpool_abort_requests(struct sqlpool_db *db) +{ + while (db->requests_head != NULL) { + struct sqlpool_request *request = db->requests_head; + + sqlpool_request_abort(&request); + } + timeout_remove(&db->request_to); +} + +static void driver_sqlpool_deinit(struct sql_db *_db) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + struct sqlpool_host *host; + struct sqlpool_connection *conn; + + array_foreach_modifiable(&db->all_connections, conn) + sql_unref(&conn->db); + array_clear(&db->all_connections); + + driver_sqlpool_abort_requests(db); + + array_foreach_modifiable(&db->hosts, host) + i_free(host->connect_string); + + i_assert(array_count(&db->all_connections) == 0); + array_free(&db->hosts); + array_free(&db->all_connections); + array_free(&_db->module_contexts); + event_unref(&_db->event); + i_free(db); +} + +static int driver_sqlpool_connect(struct sql_db *_db) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conn; + int ret = -1, ret2; + + array_foreach(&db->all_connections, conn) { + ret2 = conn->db->to_reconnect != NULL ? -1 : + sql_connect(conn->db); + if (ret2 > 0) + ret = 1; + else if (ret2 == 0 && ret < 0) + ret = 0; + } + return ret; +} + +static void driver_sqlpool_disconnect(struct sql_db *_db) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conn; + + array_foreach(&db->all_connections, conn) + sql_disconnect(conn->db); + driver_sqlpool_abort_requests(db); +} + +static const char * +driver_sqlpool_escape_string(struct sql_db *_db, const char *string) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conns; + unsigned int i, count; + + /* use the first ready connection */ + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + if (SQL_DB_IS_READY(conns[i].db)) + return sql_escape_string(conns[i].db, string); + } + /* no ready connections. just use the first one (we're guaranteed + to always have one) */ + return sql_escape_string(conns[0].db, string); +} + +static void driver_sqlpool_timeout(struct sqlpool_db *db) +{ + int duration; + + while (db->requests_head != NULL) { + struct sqlpool_request *request = db->requests_head; + + if (request->created + SQL_QUERY_TIMEOUT_SECS > ioloop_time) + break; + + + if (request->query != NULL) { + e_error(sql_query_finished_event(&db->api, request->event, + request->query, FALSE, + &duration)-> + add_str("error", "Query timed out")-> + event(), + SQL_QUERY_FINISHED_FMT": Query timed out " + "(no free connections for %u secs)", + request->query, duration, + (unsigned int)(ioloop_time - request->created)); + } else { + e_error(event_create_passthrough(request->event)-> + add_str("error", "Timed out")-> + set_name(SQL_TRANSACTION_FINISHED)->event(), + "Transaction timed out " + "(no free connections for %u secs)", + (unsigned int)(ioloop_time - request->created)); + } + sqlpool_request_abort(&request); + } + + if (db->requests_head == NULL) + timeout_remove(&db->request_to); +} + +static void +driver_sqlpool_prepend_request(struct sqlpool_db *db, + struct sqlpool_request *request) +{ + DLLIST2_PREPEND(&db->requests_head, &db->requests_tail, request); + if (db->request_to == NULL) { + db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000, + driver_sqlpool_timeout, db); + } +} + +static void +driver_sqlpool_append_request(struct sqlpool_db *db, + struct sqlpool_request *request) +{ + DLLIST2_APPEND(&db->requests_head, &db->requests_tail, request); + if (db->request_to == NULL) { + db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000, + driver_sqlpool_timeout, db); + } +} + +static void +driver_sqlpool_query_callback(struct sql_result *result, + struct sqlpool_request *request) +{ + struct sqlpool_db *db = request->db; + const struct sqlpool_connection *conn = NULL; + struct sql_db *conndb; + + if (result->failed_try_retry && + request->retry_count < array_count(&db->hosts)) { + e_warning(db->api.event, "Query failed, retrying: %s", + sql_result_get_error(result)); + request->retry_count++; + driver_sqlpool_prepend_request(db, request); + + if (driver_sqlpool_get_connection(request->db, + request->host_idx, &conn)) { + request->host_idx = conn->host_idx; + sqlpool_request_send_next(db, conn->db); + } + } else { + if (result->failed) { + e_error(db->api.event, "Query failed, aborting: %s", + request->query); + } + conndb = result->db; + + if (request->callback != NULL) + request->callback(result, request->context); + sqlpool_request_free(&request); + + sqlpool_request_send_next(db, conndb); + } +} + +static void ATTR_NULL(3, 4) +driver_sqlpool_query(struct sql_db *_db, const char *query, + sql_query_callback_t *callback, void *context) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + struct sqlpool_request *request; + const struct sqlpool_connection *conn; + + request = sqlpool_request_new(db, query); + request->callback = callback; + request->context = context; + + if (!driver_sqlpool_get_connection(db, UINT_MAX, &conn)) + driver_sqlpool_append_request(db, request); + else { + request->host_idx = conn->host_idx; + sql_query(conn->db, query, driver_sqlpool_query_callback, + request); + } +} + +static void driver_sqlpool_exec(struct sql_db *_db, const char *query) +{ + driver_sqlpool_query(_db, query, NULL, NULL); +} + +static struct sql_result * +driver_sqlpool_query_s(struct sql_db *_db, const char *query) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conn; + struct sql_result *result; + + if (!driver_sqlpool_get_sync_connection(db, &conn)) { + sql_not_connected_result.refcount++; + return &sql_not_connected_result; + } + + result = sql_query_s(conn->db, query); + if (result->failed_try_retry) { + if (!driver_sqlpool_get_sync_connection(db, &conn)) + return result; + + sql_result_unref(result); + result = sql_query_s(conn->db, query); + } + return result; +} + +static struct sql_transaction_context * +driver_sqlpool_transaction_begin(struct sql_db *_db) +{ + struct sqlpool_transaction_context *ctx; + + ctx = i_new(struct sqlpool_transaction_context, 1); + ctx->ctx.db = _db; + + /* queue changes until commit. even if we did have a free connection + now, don't use it or multiple open transactions could tie up all + connections. */ + ctx->query_pool = pool_alloconly_create("sqlpool transaction", 1024); + return &ctx->ctx; +} + +static void +driver_sqlpool_transaction_free(struct sqlpool_transaction_context *ctx) +{ + if (ctx->commit_request != NULL) + sqlpool_request_abort(&ctx->commit_request); + pool_unref(&ctx->query_pool); + i_free(ctx); +} + +static void +driver_sqlpool_commit_callback(const struct sql_commit_result *result, + struct sqlpool_transaction_context *ctx) +{ + ctx->callback(result, ctx->context); + driver_sqlpool_transaction_free(ctx); +} + +static void +driver_sqlpool_transaction_commit(struct sql_transaction_context *_ctx, + sql_commit_callback_t *callback, + void *context) +{ + struct sqlpool_transaction_context *ctx = + (struct sqlpool_transaction_context *)_ctx; + struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db; + const struct sqlpool_connection *conn; + + ctx->callback = callback; + ctx->context = context; + + ctx->commit_request = sqlpool_request_new(db, NULL); + ctx->commit_request->trans = ctx; + + if (driver_sqlpool_get_connection(db, UINT_MAX, &conn)) + sqlpool_request_handle_transaction(conn->db, ctx); + else + driver_sqlpool_append_request(db, ctx->commit_request); +} + +static int +driver_sqlpool_transaction_commit_s(struct sql_transaction_context *_ctx, + const char **error_r) +{ + struct sqlpool_transaction_context *ctx = + (struct sqlpool_transaction_context *)_ctx; + struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db; + const struct sqlpool_connection *conn; + struct sql_transaction_context *conn_trans; + int ret; + + *error_r = NULL; + + if (!driver_sqlpool_get_sync_connection(db, &conn)) { + *error_r = SQL_ERRSTR_NOT_CONNECTED; + driver_sqlpool_transaction_free(ctx); + return -1; + } + + conn_trans = driver_sqlpool_new_conn_trans(ctx, conn->db); + ret = sql_transaction_commit_s(&conn_trans, error_r); + driver_sqlpool_transaction_free(ctx); + return ret; +} + +static void +driver_sqlpool_transaction_rollback(struct sql_transaction_context *_ctx) +{ + struct sqlpool_transaction_context *ctx = + (struct sqlpool_transaction_context *)_ctx; + + driver_sqlpool_transaction_free(ctx); +} + +static void +driver_sqlpool_update(struct sql_transaction_context *_ctx, const char *query, + unsigned int *affected_rows) +{ + struct sqlpool_transaction_context *ctx = + (struct sqlpool_transaction_context *)_ctx; + + /* we didn't get a connection for transaction immediately. + queue updates until commit transfers all of these */ + sql_transaction_add_query(&ctx->ctx, ctx->query_pool, + query, affected_rows); +} + +static const char * +driver_sqlpool_escape_blob(struct sql_db *_db, + const unsigned char *data, size_t size) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conns; + unsigned int i, count; + + /* use the first ready connection */ + conns = array_get(&db->all_connections, &count); + for (i = 0; i < count; i++) { + if (SQL_DB_IS_READY(conns[i].db)) + return sql_escape_blob(conns[i].db, data, size); + } + /* no ready connections. just use the first one (we're guaranteed + to always have one) */ + return sql_escape_blob(conns[0].db, data, size); +} + +static void driver_sqlpool_wait(struct sql_db *_db) +{ + struct sqlpool_db *db = (struct sqlpool_db *)_db; + const struct sqlpool_connection *conn; + + array_foreach(&db->all_connections, conn) + sql_wait(conn->db); +} + +struct sql_db driver_sqlpool_db = { + "", + + .v = { + .get_flags = driver_sqlpool_get_flags, + .deinit = driver_sqlpool_deinit, + .connect = driver_sqlpool_connect, + .disconnect = driver_sqlpool_disconnect, + .escape_string = driver_sqlpool_escape_string, + .exec = driver_sqlpool_exec, + .query = driver_sqlpool_query, + .query_s = driver_sqlpool_query_s, + .wait = driver_sqlpool_wait, + + .transaction_begin = driver_sqlpool_transaction_begin, + .transaction_commit = driver_sqlpool_transaction_commit, + .transaction_commit_s = driver_sqlpool_transaction_commit_s, + .transaction_rollback = driver_sqlpool_transaction_rollback, + + .update = driver_sqlpool_update, + + .escape_blob = driver_sqlpool_escape_blob, + } +}; diff --git a/src/lib-sql/driver-test.c b/src/lib-sql/driver-test.c new file mode 100644 index 0000000..9d4db76 --- /dev/null +++ b/src/lib-sql/driver-test.c @@ -0,0 +1,514 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "test-lib.h" +#include "str.h" +#include "buffer.h" +#include "sql-api-private.h" +#include "driver-test.h" +#include "array.h" +#include "hex-binary.h" + +struct test_sql_db { + struct sql_db api; + + pool_t pool; + ARRAY(struct test_driver_result) expected; + const char *error; + bool failed:1; +}; + +struct test_sql_result { + struct sql_result api; + struct test_driver_result *result; + const char *error; +}; + +static struct sql_db *driver_test_mysql_init(const char *connect_string); +static struct sql_db *driver_test_cassandra_init(const char *connect_string); +static struct sql_db *driver_test_sqlite_init(const char *connect_string); +static void driver_test_deinit(struct sql_db *_db); +static int driver_test_connect(struct sql_db *_db); +static void driver_test_disconnect(struct sql_db *_db); +static const char * +driver_test_mysql_escape_string(struct sql_db *_db, const char *string); +static const char * +driver_test_escape_string(struct sql_db *_db, const char *string); +static void driver_test_exec(struct sql_db *_db, const char *query); +static void driver_test_query(struct sql_db *_db, const char *query, + sql_query_callback_t *callback, void *context); +static struct sql_result * +driver_test_query_s(struct sql_db *_db, const char *query); +static struct sql_transaction_context * +driver_test_transaction_begin(struct sql_db *_db); +static void driver_test_transaction_commit(struct sql_transaction_context *ctx, + sql_commit_callback_t *callback, + void *context); +static int +driver_test_transaction_commit_s(struct sql_transaction_context *ctx, + const char **error_r); +static void +driver_test_transaction_rollback(struct sql_transaction_context *ctx); +static void +driver_test_update(struct sql_transaction_context *ctx, const char *query, + unsigned int *affected_rows); +static const char * +driver_test_mysql_escape_blob(struct sql_db *_db, const unsigned char *data, + size_t size); +static const char * +driver_test_escape_blob(struct sql_db *_db, const unsigned char *data, + size_t size); + +static void driver_test_result_free(struct sql_result *result); +static int driver_test_result_next_row(struct sql_result *result); + +static unsigned int +driver_test_result_get_fields_count(struct sql_result *result); +static const char * +driver_test_result_get_field_name(struct sql_result *result, unsigned int idx); +static int +driver_test_result_find_field(struct sql_result *result, const char *field_name); + +static const char * +driver_test_result_get_field_value(struct sql_result *result, unsigned int idx); +static const unsigned char * +driver_test_result_get_field_value_binary(struct sql_result *result, + unsigned int idx, size_t *size_r); +static const char * +driver_test_result_find_field_value(struct sql_result *result, + const char *field_name); +static const char *const * +driver_test_result_get_values(struct sql_result *result); + +const char *driver_test_result_get_error(struct sql_result *result); + + +const struct sql_db driver_test_mysql_db = { + .name = "mysql", + .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_ON_DUPLICATE_KEY, + + .v = { + .init = driver_test_mysql_init, + .deinit = driver_test_deinit, + .connect = driver_test_connect, + .disconnect = driver_test_disconnect, + .escape_string = driver_test_mysql_escape_string, + .exec = driver_test_exec, + .query = driver_test_query, + .query_s = driver_test_query_s, + + .transaction_begin = driver_test_transaction_begin, + .transaction_commit = driver_test_transaction_commit, + .transaction_commit_s = driver_test_transaction_commit_s, + .transaction_rollback = driver_test_transaction_rollback, + .update = driver_test_update, + + .escape_blob = driver_test_mysql_escape_blob, + } +}; + +const struct sql_db driver_test_cassandra_db = { + .name = "cassandra", + + .v = { + .init = driver_test_cassandra_init, + .deinit = driver_test_deinit, + .connect = driver_test_connect, + .disconnect = driver_test_disconnect, + .escape_string = driver_test_escape_string, + .exec = driver_test_exec, + .query = driver_test_query, + .query_s = driver_test_query_s, + + .transaction_begin = driver_test_transaction_begin, + .transaction_commit = driver_test_transaction_commit, + .transaction_commit_s = driver_test_transaction_commit_s, + .transaction_rollback = driver_test_transaction_rollback, + .update = driver_test_update, + + .escape_blob = driver_test_escape_blob, + } +}; + +const struct sql_db driver_test_sqlite_db = { + .name = "sqlite", + .flags = SQL_DB_FLAG_ON_CONFLICT_DO | SQL_DB_FLAG_BLOCKING, + + .v = { + .init = driver_test_sqlite_init, + .deinit = driver_test_deinit, + .connect = driver_test_connect, + .disconnect = driver_test_disconnect, + .escape_string = driver_test_escape_string, + .exec = driver_test_exec, + .query = driver_test_query, + .query_s = driver_test_query_s, + + .transaction_begin = driver_test_transaction_begin, + .transaction_commit = driver_test_transaction_commit, + .transaction_commit_s = driver_test_transaction_commit_s, + .transaction_rollback = driver_test_transaction_rollback, + .update = driver_test_update, + + .escape_blob = driver_test_escape_blob, + } +}; + + +const struct sql_result driver_test_result = { + .v = { + .free = driver_test_result_free, + .next_row = driver_test_result_next_row, + .get_fields_count = driver_test_result_get_fields_count, + .get_field_name = driver_test_result_get_field_name, + .find_field = driver_test_result_find_field, + .get_field_value = driver_test_result_get_field_value, + .get_field_value_binary = driver_test_result_get_field_value_binary, + .find_field_value = driver_test_result_find_field_value, + .get_values = driver_test_result_get_values, + .get_error = driver_test_result_get_error, + } +}; + +void sql_driver_test_register(void) +{ + sql_driver_register(&driver_test_mysql_db); + sql_driver_register(&driver_test_cassandra_db); + sql_driver_register(&driver_test_sqlite_db); +} + +void sql_driver_test_unregister(void) +{ + sql_driver_unregister(&driver_test_mysql_db); + sql_driver_unregister(&driver_test_cassandra_db); + sql_driver_unregister(&driver_test_sqlite_db); +} + +static struct sql_db *driver_test_init(const struct sql_db *driver, + const char *connect_string ATTR_UNUSED) +{ + pool_t pool = pool_alloconly_create(MEMPOOL_GROWING" test sql driver", 2048); + struct test_sql_db *ret = p_new(pool, struct test_sql_db, 1); + ret->pool = pool; + ret->api = *driver; + p_array_init(&ret->expected, pool, 8); + return &ret->api; +} + +static struct sql_db *driver_test_mysql_init(const char *connect_string) +{ + return driver_test_init(&driver_test_mysql_db, connect_string); +} + +static struct sql_db *driver_test_cassandra_init(const char *connect_string) +{ + return driver_test_init(&driver_test_cassandra_db, connect_string); +} + +static struct sql_db *driver_test_sqlite_init(const char *connect_string) +{ + return driver_test_init(&driver_test_sqlite_db, connect_string); +} + +static void driver_test_deinit(struct sql_db *_db ATTR_UNUSED) +{ + struct test_sql_db *db = (struct test_sql_db*)_db; + array_free(&_db->module_contexts); + pool_unref(&db->pool); +} + +static int driver_test_connect(struct sql_db *_db ATTR_UNUSED) +{ + /* nix */ + return 0; +} + +static void driver_test_disconnect(struct sql_db *_db ATTR_UNUSED) +{ } + +static const char * +driver_test_mysql_escape_string(struct sql_db *_db ATTR_UNUSED, + const char *string) +{ + string_t *esc = t_str_new(strlen(string)); + for(const char *ptr = string; *ptr != '\0'; ptr++) { + if (*ptr == '\n' || *ptr == '\r' || *ptr == '\\' || + *ptr == '\'' || *ptr == '\"' || *ptr == '\x1a') + str_append_c(esc, '\\'); + str_append_c(esc, *ptr); + } + return str_c(esc); +} + +static const char * +driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string) +{ + return string; +} + +static void driver_test_exec(struct sql_db *_db, const char *query) +{ + struct test_sql_db *db = (struct test_sql_db*)_db; + struct test_driver_result *result = + array_front_modifiable(&db->expected); + i_assert(result->cur < result->nqueries); + +/* i_debug("DUMMY EXECUTE: %s", query); + i_debug("DUMMY EXPECT : %s", result->queries[result->cur]); */ + + test_assert_strcmp(result->queries[result->cur], query); + + if (strcmp(result->queries[result->cur], query) != 0) { + db->error = "Invalid query"; + db->failed = TRUE; + } + + result->cur++; +} + +static void +driver_test_query(struct sql_db *_db, const char *query, + sql_query_callback_t *callback, void *context) +{ + struct sql_result *result = driver_test_query_s(_db, query); + if (callback != NULL) + callback(result, context); +} + +static struct sql_result * +driver_test_query_s(struct sql_db *_db, const char *query) +{ + struct test_sql_db *db = (struct test_sql_db*)_db; + struct test_driver_result *result = + array_front_modifiable(&db->expected); + struct test_sql_result *res = i_new(struct test_sql_result, 1); + + driver_test_exec(_db, query); + + if (db->failed) { + res->api.failed = TRUE; + } + + res->api.v = driver_test_result.v; + res->api.db = _db; + if (result->result != NULL) { + res->result = i_new(struct test_driver_result, 1); + memcpy(res->result, result, sizeof(*result)); + } + res->api.refcount = 1; + + /* drop it from array if it's used up */ + if (result->cur == result->nqueries) + array_pop_front(&db->expected); + + return &res->api; +} + +static struct sql_transaction_context * +driver_test_transaction_begin(struct sql_db *_db) +{ + struct sql_transaction_context *ctx = + i_new(struct sql_transaction_context, 1); + ctx->db = _db; + return ctx; +} + +static void +driver_test_transaction_commit(struct sql_transaction_context *ctx, + sql_commit_callback_t *callback, void *context) +{ + struct sql_commit_result res; + res.error_type = driver_test_transaction_commit_s(ctx, &res.error); + callback(&res, context); +} + +static int +driver_test_transaction_commit_s(struct sql_transaction_context *ctx, + const char **error_r) +{ + struct test_sql_db *db = (struct test_sql_db*)ctx->db; + int ret = 0; + + if (db->error != NULL) { + *error_r = db->error; + ret = -1; + } + i_free(ctx); + db->error = NULL; + db->failed = FALSE; + + return ret; +} + +static void +driver_test_transaction_rollback(struct sql_transaction_context *ctx) +{ + struct test_sql_db *db = (struct test_sql_db*)ctx->db; + i_free(ctx); + db->error = NULL; + db->failed = FALSE; +} + +static void +driver_test_update(struct sql_transaction_context *ctx, const char *query, + unsigned int *affected_rows) +{ + struct test_sql_db *db= (struct test_sql_db*)ctx->db; + struct test_driver_result *result = + array_front_modifiable(&db->expected); + driver_test_exec(ctx->db, query); + + if (affected_rows != NULL) + *affected_rows = result->affected_rows; + + /* drop it from array if it's used up */ + if (result->cur == result->nqueries) + array_pop_front(&db->expected); +} + +static const char * +driver_test_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + return t_strdup_printf("X'%s'", binary_to_hex(data,size)); +} + +static const char * +driver_test_escape_blob(struct sql_db *_db ATTR_UNUSED, + const unsigned char *data, size_t size) +{ + return t_strdup_printf("X'%s'", binary_to_hex(data,size)); +} + +static void driver_test_result_free(struct sql_result *result) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + if (tsr->result != NULL) + i_free(tsr->result); + i_free(result); +} + +static int driver_test_result_next_row(struct sql_result *result) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + + if (r == NULL) return 0; + + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + if (rs->cur <= rs->rows) { + rs->cur++; + } + + return rs->cur <= rs->rows ? 1 : 0; +} + +static unsigned int +driver_test_result_get_fields_count(struct sql_result *result) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + return rs->cols; +} + +static const char * +driver_test_result_get_field_name(struct sql_result *result, unsigned int idx) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + i_assert(idx < rs->cols); + return rs->col_names[idx]; +} + +static int +driver_test_result_find_field(struct sql_result *result, const char *field_name) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + for(size_t i = 0; i < rs->cols; i++) { + if (strcmp(field_name, rs->col_names[i])==0) + return i; + } + return -1; +} + +static const char * +driver_test_result_get_field_value(struct sql_result *result, unsigned int idx) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + + i_assert(idx < rs->cols); + i_assert(rs->cur <= rs->rows); + + return rs->row_data[rs->cur-1][idx]; +} +static const unsigned char * +driver_test_result_get_field_value_binary(struct sql_result *result, + unsigned int idx, size_t *size_r) +{ + buffer_t *buf = t_buffer_create(64); + const char *value = driver_test_result_get_field_value(result, idx); + /* expect it hex encoded */ + if (hex_to_binary(value, buf) < 0) { + *size_r = 0; + return NULL; + } + *size_r = buf->used; + return buf->data; +} +static const char * +driver_test_result_find_field_value(struct sql_result *result, + const char *field_name) +{ + int idx = driver_test_result_find_field(result, field_name); + if (idx < 0) return NULL; + return driver_test_result_get_field_value(result, idx); +} +static const char *const * +driver_test_result_get_values(struct sql_result *result) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + struct test_driver_result *r = tsr->result; + struct test_driver_result_set *rs = + &(r->result[r->cur-1]); + i_assert(rs->cur <= rs->rows); + return rs->row_data[rs->cur-1]; +} + +const char *driver_test_result_get_error(struct sql_result *result) +{ + struct test_sql_result *tsr = + (struct test_sql_result *)result; + return tsr->error; +} + + +void sql_driver_test_add_expected_result(struct sql_db *_db, + const struct test_driver_result *result) +{ + struct test_sql_db *db = (struct test_sql_db*)_db; + array_push_back(&db->expected, result); +} + +void sql_driver_test_clear_expected_results(struct sql_db *_db) +{ + struct test_sql_db *db = (struct test_sql_db*)_db; + array_clear(&db->expected); +} diff --git a/src/lib-sql/driver-test.h b/src/lib-sql/driver-test.h new file mode 100644 index 0000000..49915ad --- /dev/null +++ b/src/lib-sql/driver-test.h @@ -0,0 +1,28 @@ +#ifndef DRIVER_TEST_H +#define DRIVER_TEST_H 1 + +struct test_driver_result_set { + size_t rows, cols, cur; + const char *const *col_names; + const char ***row_data; +}; + +struct test_driver_result { + /* expected queries */ + size_t nqueries; + size_t cur; + unsigned int affected_rows; + const char *const *queries; + + /* test result, rows and columns */ + struct test_driver_result_set *result; +}; + +void sql_driver_test_register(void); +void sql_driver_test_unregister(void); + +void sql_driver_test_add_expected_result(struct sql_db *_db, + const struct test_driver_result *result); +void sql_driver_test_clear_expected_results(struct sql_db *_db); + +#endif diff --git a/src/lib-sql/sql-api-private.h b/src/lib-sql/sql-api-private.h new file mode 100644 index 0000000..3026512 --- /dev/null +++ b/src/lib-sql/sql-api-private.h @@ -0,0 +1,255 @@ +#ifndef SQL_API_PRIVATE_H +#define SQL_API_PRIVATE_H + +#include "sql-api.h" +#include "module-context.h" + +enum sql_db_state { + /* not connected to database */ + SQL_DB_STATE_DISCONNECTED, + /* waiting for connection attempt to succeed or fail */ + SQL_DB_STATE_CONNECTING, + /* connected, allowing more queries */ + SQL_DB_STATE_IDLE, + /* connected, no more queries allowed */ + SQL_DB_STATE_BUSY +}; + +/* Minimum delay between reconnecting to same server */ +#define SQL_CONNECT_MIN_DELAY 1 +/* Maximum time to avoiding reconnecting to same server */ +#define SQL_CONNECT_MAX_DELAY (60*30) +/* If no servers are connected but a query is requested, try reconnecting to + next server which has been disconnected longer than this (with a single + server setup this is really the "max delay" and the SQL_CONNECT_MAX_DELAY + is never used). */ +#define SQL_CONNECT_RESET_DELAY 15 +/* Abort connect() if it can't connect within this time. */ +#define SQL_CONNECT_TIMEOUT_SECS 5 +/* Abort queries after this many seconds */ +#define SQL_QUERY_TIMEOUT_SECS 60 +/* Default max. number of connections to create per host */ +#define SQL_DEFAULT_CONNECTION_LIMIT 5 + +#define SQL_DB_IS_READY(db) \ + ((db)->state == SQL_DB_STATE_IDLE) +#define SQL_ERRSTR_NOT_CONNECTED "Not connected to database" + +/* What is considered slow query */ +#define SQL_SLOW_QUERY_MSEC 1000 + +#define SQL_QUERY_FINISHED "sql_query_finished" +#define SQL_CONNECTION_FINISHED "sql_connection_finished" +#define SQL_TRANSACTION_FINISHED "sql_transaction_finished" + +#define SQL_QUERY_FINISHED_FMT "Finished query '%s' in %u msecs" + +struct sql_db_module_register { + unsigned int id; +}; + +union sql_db_module_context { + struct sql_db_module_register *reg; +}; + +extern struct sql_db_module_register sql_db_module_register; + +extern struct event_category event_category_sql; + +struct sql_transaction_query { + struct sql_transaction_query *next; + struct sql_transaction_context *trans; + + const char *query; + unsigned int *affected_rows; +}; + +struct sql_db_vfuncs { + struct sql_db *(*init)(const char *connect_string); + int (*init_full)(const struct sql_settings *set, struct sql_db **db_r, + const char **error); + void (*deinit)(struct sql_db *db); + void (*unref)(struct sql_db *db); + void (*wait) (struct sql_db *db); + + enum sql_db_flags (*get_flags)(struct sql_db *db); + + int (*connect)(struct sql_db *db); + void (*disconnect)(struct sql_db *db); + const char *(*escape_string)(struct sql_db *db, const char *string); + + void (*exec)(struct sql_db *db, const char *query); + void (*query)(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context); + struct sql_result *(*query_s)(struct sql_db *db, const char *query); + + struct sql_transaction_context *(*transaction_begin)(struct sql_db *db); + void (*transaction_commit)(struct sql_transaction_context *ctx, + sql_commit_callback_t *callback, + void *context); + int (*transaction_commit_s)(struct sql_transaction_context *ctx, + const char **error_r); + void (*transaction_rollback)(struct sql_transaction_context *ctx); + + void (*update)(struct sql_transaction_context *ctx, const char *query, + unsigned int *affected_rows); + const char *(*escape_blob)(struct sql_db *db, + const unsigned char *data, size_t size); + + struct sql_prepared_statement * + (*prepared_statement_init)(struct sql_db *db, + const char *query_template); + void (*prepared_statement_deinit)(struct sql_prepared_statement *prep_stmt); + + + struct sql_statement * + (*statement_init)(struct sql_db *db, const char *query_template); + struct sql_statement * + (*statement_init_prepared)(struct sql_prepared_statement *prep_stmt); + void (*statement_abort)(struct sql_statement *stmt); + void (*statement_set_timestamp)(struct sql_statement *stmt, + const struct timespec *ts); + void (*statement_bind_str)(struct sql_statement *stmt, + unsigned int column_idx, const char *value); + void (*statement_bind_binary)(struct sql_statement *stmt, + unsigned int column_idx, const void *value, + size_t value_size); + void (*statement_bind_int64)(struct sql_statement *stmt, + unsigned int column_idx, int64_t value); + void (*statement_query)(struct sql_statement *stmt, + sql_query_callback_t *callback, void *context); + struct sql_result *(*statement_query_s)(struct sql_statement *stmt); + void (*update_stmt)(struct sql_transaction_context *ctx, + struct sql_statement *stmt, + unsigned int *affected_rows); +}; + +struct sql_db { + const char *name; + enum sql_db_flags flags; + int refcount; + + struct sql_db_vfuncs v; + ARRAY(union sql_db_module_context *) module_contexts; + + void (*state_change_callback)(struct sql_db *db, + enum sql_db_state prev_state, + void *context); + void *state_change_context; + + struct event *event; + HASH_TABLE(char *, struct sql_prepared_statement *) prepared_stmt_hash; + + enum sql_db_state state; + /* last time we started connecting to this server + (which may or may not have succeeded) */ + time_t last_connect_try; + unsigned int connect_delay; + unsigned int connect_failure_count; + struct timeout *to_reconnect; + + uint64_t succeeded_queries; + uint64_t failed_queries; + /* includes both succeeded and failed */ + uint64_t slow_queries; + + bool no_reconnect:1; +}; + +struct sql_result_vfuncs { + void (*free)(struct sql_result *result); + int (*next_row)(struct sql_result *result); + + unsigned int (*get_fields_count)(struct sql_result *result); + const char *(*get_field_name)(struct sql_result *result, + unsigned int idx); + int (*find_field)(struct sql_result *result, const char *field_name); + + const char *(*get_field_value)(struct sql_result *result, + unsigned int idx); + const unsigned char * + (*get_field_value_binary)(struct sql_result *result, + unsigned int idx, + size_t *size_r); + const char *(*find_field_value)(struct sql_result *result, + const char *field_name); + const char *const *(*get_values)(struct sql_result *result); + + const char *(*get_error)(struct sql_result *result); + void (*more)(struct sql_result **result, bool async, + sql_query_callback_t *callback, void *context); +}; + +struct sql_prepared_statement { + struct sql_db *db; + int refcount; + char *query_template; +}; + +struct sql_statement { + struct sql_db *db; + + pool_t pool; + const char *query_template; + ARRAY_TYPE(const_string) args; + + /* Tell the driver to not log this query with expanded values. */ + bool no_log_expanded_values; +}; + +struct sql_field_map { + enum sql_field_type type; + size_t offset; +}; + +struct sql_result { + struct sql_result_vfuncs v; + int refcount; + + struct sql_db *db; + const struct sql_field_def *fields; + + unsigned int map_size; + struct sql_field_map *map; + void *fetch_dest; + struct event *event; + size_t fetch_dest_size; + enum sql_result_error_type error_type; + + bool failed:1; + bool failed_try_retry:1; + bool callback:1; +}; + +struct sql_transaction_context { + struct sql_db *db; + struct event *event; + + /* commit() must use this query list if head is non-NULL. */ + struct sql_transaction_query *head, *tail; +}; + +ARRAY_DEFINE_TYPE(sql_drivers, const struct sql_db *); + +extern ARRAY_TYPE(sql_drivers) sql_drivers; +extern struct sql_result sql_not_connected_result; + +void sql_init_common(struct sql_db *db); +struct sql_db * +driver_sqlpool_init(const char *connect_string, const struct sql_db *driver); +int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver, + struct sql_db **db_r, const char **error_r); + +void sql_db_set_state(struct sql_db *db, enum sql_db_state state); + +void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool, + const char *query, unsigned int *affected_rows); +const char *sql_statement_get_log_query(struct sql_statement *stmt); +const char *sql_statement_get_query(struct sql_statement *stmt); + +void sql_connection_log_finished(struct sql_db *db); +struct event_passthrough * +sql_query_finished_event(struct sql_db *db, struct event *event, const char *query, + bool success, int *duration_r); +struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx); +#endif diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c new file mode 100644 index 0000000..cd16a5f --- /dev/null +++ b/src/lib-sql/sql-api.c @@ -0,0 +1,846 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "ioloop.h" +#include "hash.h" +#include "str.h" +#include "time-util.h" +#include "sql-api-private.h" + +#include <time.h> + +struct event_category event_category_sql = { + .name = "sql", +}; + +struct sql_db_module_register sql_db_module_register = { 0 }; +ARRAY_TYPE(sql_drivers) sql_drivers; + +void sql_drivers_init(void) +{ + i_array_init(&sql_drivers, 8); +} + +void sql_drivers_deinit(void) +{ + array_free(&sql_drivers); +} + +static const struct sql_db *sql_driver_lookup(const char *name) +{ + const struct sql_db *const *drivers; + unsigned int i, count; + + drivers = array_get(&sql_drivers, &count); + for (i = 0; i < count; i++) { + if (strcmp(drivers[i]->name, name) == 0) + return drivers[i]; + } + return NULL; +} + +void sql_driver_register(const struct sql_db *driver) +{ + if (sql_driver_lookup(driver->name) != NULL) { + i_fatal("sql_driver_register(%s): Already registered", + driver->name); + } + array_push_back(&sql_drivers, &driver); +} + +void sql_driver_unregister(const struct sql_db *driver) +{ + const struct sql_db *const *drivers; + unsigned int i, count; + + drivers = array_get(&sql_drivers, &count); + for (i = 0; i < count; i++) { + if (drivers[i] == driver) { + array_delete(&sql_drivers, i, 1); + break; + } + } +} + +struct sql_db *sql_init(const char *db_driver, const char *connect_string) +{ + const char *error; + struct sql_db *db; + struct sql_settings set = { + .driver = db_driver, + .connect_string = connect_string, + }; + + if (sql_init_full(&set, &db, &error) < 0) + i_fatal("%s", error); + return db; +} + +int sql_init_full(const struct sql_settings *set, struct sql_db **db_r, + const char **error_r) +{ + const struct sql_db *driver; + struct sql_db *db; + int ret = 0; + + i_assert(set->connect_string != NULL); + + driver = sql_driver_lookup(set->driver); + if (driver == NULL) { + *error_r = t_strdup_printf("Unknown database driver '%s'", set->driver); + return -1; + } + + if ((driver->flags & SQL_DB_FLAG_POOLED) == 0) { + if (driver->v.init_full == NULL) { + db = driver->v.init(set->connect_string); + } else + ret = driver->v.init_full(set, &db, error_r); + } else + ret = driver_sqlpool_init_full(set, driver, &db, error_r); + + if (ret < 0) + return -1; + + sql_init_common(db); + *db_r = db; + return 0; +} + +void sql_init_common(struct sql_db *db) +{ + db->refcount = 1; + i_array_init(&db->module_contexts, 5); + hash_table_create(&db->prepared_stmt_hash, default_pool, 0, + str_hash, strcmp); +} + +void sql_ref(struct sql_db *db) +{ + i_assert(db->refcount > 0); + db->refcount++; +} + +static void +default_sql_prepared_statement_deinit(struct sql_prepared_statement *prep_stmt) +{ + i_free(prep_stmt->query_template); + i_free(prep_stmt); +} + +static void sql_prepared_statements_free(struct sql_db *db) +{ + struct hash_iterate_context *iter; + struct sql_prepared_statement *prep_stmt; + char *query; + + iter = hash_table_iterate_init(db->prepared_stmt_hash); + while (hash_table_iterate(iter, db->prepared_stmt_hash, &query, &prep_stmt)) { + i_assert(prep_stmt->refcount == 0); + if (prep_stmt->db->v.prepared_statement_deinit != NULL) + prep_stmt->db->v.prepared_statement_deinit(prep_stmt); + else + default_sql_prepared_statement_deinit(prep_stmt); + } + hash_table_iterate_deinit(&iter); + hash_table_clear(db->prepared_stmt_hash, TRUE); +} + +void sql_unref(struct sql_db **_db) +{ + struct sql_db *db = *_db; + + *_db = NULL; + + i_assert(db->refcount > 0); + if (db->v.unref != NULL) + db->v.unref(db); + if (--db->refcount > 0) + return; + + timeout_remove(&db->to_reconnect); + sql_prepared_statements_free(db); + hash_table_destroy(&db->prepared_stmt_hash); + db->v.deinit(db); +} + +enum sql_db_flags sql_get_flags(struct sql_db *db) +{ + if (db->v.get_flags != NULL) + return db->v.get_flags(db); + else + return db->flags; +} + +int sql_connect(struct sql_db *db) +{ + time_t now; + + switch (db->state) { + case SQL_DB_STATE_DISCONNECTED: + break; + case SQL_DB_STATE_CONNECTING: + return 0; + default: + return 1; + } + + /* don't try reconnecting more than once a second */ + now = time(NULL); + if (db->last_connect_try + (time_t)db->connect_delay > now) + return -1; + db->last_connect_try = now; + + return db->v.connect(db); +} + +void sql_disconnect(struct sql_db *db) +{ + timeout_remove(&db->to_reconnect); + db->v.disconnect(db); +} + +const char *sql_escape_string(struct sql_db *db, const char *string) +{ + return db->v.escape_string(db, string); +} + +const char *sql_escape_blob(struct sql_db *db, + const unsigned char *data, size_t size) +{ + return db->v.escape_blob(db, data, size); +} + +void sql_exec(struct sql_db *db, const char *query) +{ + db->v.exec(db, query); +} + +#undef sql_query +void sql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context) +{ + db->v.query(db, query, callback, context); +} + +struct sql_result *sql_query_s(struct sql_db *db, const char *query) +{ + return db->v.query_s(db, query); +} + +static struct sql_prepared_statement * +default_sql_prepared_statement_init(struct sql_db *db, + const char *query_template) +{ + struct sql_prepared_statement *prep_stmt; + + prep_stmt = i_new(struct sql_prepared_statement, 1); + prep_stmt->db = db; + prep_stmt->refcount = 1; + prep_stmt->query_template = i_strdup(query_template); + return prep_stmt; +} + +static struct sql_statement * +default_sql_statement_init_prepared(struct sql_prepared_statement *stmt) +{ + return sql_statement_init(stmt->db, stmt->query_template); +} + +const char *sql_statement_get_log_query(struct sql_statement *stmt) +{ + if (stmt->no_log_expanded_values) + return stmt->query_template; + return sql_statement_get_query(stmt); +} + +const char *sql_statement_get_query(struct sql_statement *stmt) +{ + string_t *query = t_str_new(128); + const char *const *args; + unsigned int i, args_count, arg_pos = 0; + + args = array_get(&stmt->args, &args_count); + + for (i = 0; stmt->query_template[i] != '\0'; i++) { + if (stmt->query_template[i] == '?') { + if (arg_pos >= args_count || + args[arg_pos] == NULL) { + i_panic("lib-sql: Missing bind for arg #%u in statement: %s", + arg_pos, stmt->query_template); + } + str_append(query, args[arg_pos++]); + } else { + str_append_c(query, stmt->query_template[i]); + } + } + if (arg_pos != args_count) { + i_panic("lib-sql: Too many bind args (%u) for statement: %s", + args_count, stmt->query_template); + } + return str_c(query); +} + +static void +default_sql_statement_query(struct sql_statement *stmt, + sql_query_callback_t *callback, void *context) +{ + sql_query(stmt->db, sql_statement_get_query(stmt), + callback, context); + pool_unref(&stmt->pool); +} + +static struct sql_result * +default_sql_statement_query_s(struct sql_statement *stmt) +{ + struct sql_result *result = + sql_query_s(stmt->db, sql_statement_get_query(stmt)); + pool_unref(&stmt->pool); + return result; +} + +static void default_sql_update_stmt(struct sql_transaction_context *ctx, + struct sql_statement *stmt, + unsigned int *affected_rows) +{ + ctx->db->v.update(ctx, sql_statement_get_query(stmt), + affected_rows); + pool_unref(&stmt->pool); +} + +struct sql_prepared_statement * +sql_prepared_statement_init(struct sql_db *db, const char *query_template) +{ + struct sql_prepared_statement *stmt; + + stmt = hash_table_lookup(db->prepared_stmt_hash, query_template); + if (stmt != NULL) { + stmt->refcount++; + return stmt; + } + + if (db->v.prepared_statement_init != NULL) + stmt = db->v.prepared_statement_init(db, query_template); + else + stmt = default_sql_prepared_statement_init(db, query_template); + + hash_table_insert(db->prepared_stmt_hash, stmt->query_template, stmt); + return stmt; +} + +void sql_prepared_statement_unref(struct sql_prepared_statement **_prep_stmt) +{ + struct sql_prepared_statement *prep_stmt = *_prep_stmt; + + *_prep_stmt = NULL; + + i_assert(prep_stmt->refcount > 0); + prep_stmt->refcount--; +} + +static void +sql_statement_init_fields(struct sql_statement *stmt, struct sql_db *db) +{ + stmt->db = db; + p_array_init(&stmt->args, stmt->pool, 8); +} + +struct sql_statement * +sql_statement_init(struct sql_db *db, const char *query_template) +{ + struct sql_statement *stmt; + + if (db->v.statement_init != NULL) + stmt = db->v.statement_init(db, query_template); + else { + pool_t pool = pool_alloconly_create("sql statement", 1024); + stmt = p_new(pool, struct sql_statement, 1); + stmt->pool = pool; + } + stmt->query_template = p_strdup(stmt->pool, query_template); + sql_statement_init_fields(stmt, db); + return stmt; +} + +struct sql_statement * +sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt) +{ + struct sql_statement *stmt; + + if (prep_stmt->db->v.statement_init_prepared == NULL) + return default_sql_statement_init_prepared(prep_stmt); + + stmt = prep_stmt->db->v.statement_init_prepared(prep_stmt); + sql_statement_init_fields(stmt, prep_stmt->db); + return stmt; +} + +void sql_statement_abort(struct sql_statement **_stmt) +{ + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; + if (stmt->db->v.statement_abort != NULL) + stmt->db->v.statement_abort(stmt); + pool_unref(&stmt->pool); +} + +void sql_statement_set_timestamp(struct sql_statement *stmt, + const struct timespec *ts) +{ + if (stmt->db->v.statement_set_timestamp != NULL) + stmt->db->v.statement_set_timestamp(stmt, ts); +} + +void sql_statement_set_no_log_expanded_values(struct sql_statement *stmt, + bool no_expand) +{ + stmt->no_log_expanded_values = no_expand; +} + +void sql_statement_bind_str(struct sql_statement *stmt, + unsigned int column_idx, const char *value) +{ + const char *escaped_value = + p_strdup_printf(stmt->pool, "'%s'", + sql_escape_string(stmt->db, value)); + array_idx_set(&stmt->args, column_idx, &escaped_value); + + if (stmt->db->v.statement_bind_str != NULL) + stmt->db->v.statement_bind_str(stmt, column_idx, value); +} + +void sql_statement_bind_binary(struct sql_statement *stmt, + unsigned int column_idx, const void *value, + size_t value_size) +{ + const char *value_str = + p_strdup_printf(stmt->pool, "%s", + sql_escape_blob(stmt->db, value, value_size)); + array_idx_set(&stmt->args, column_idx, &value_str); + + if (stmt->db->v.statement_bind_binary != NULL) { + stmt->db->v.statement_bind_binary(stmt, column_idx, + value, value_size); + } +} + +void sql_statement_bind_int64(struct sql_statement *stmt, + unsigned int column_idx, int64_t value) +{ + const char *value_str = p_strdup_printf(stmt->pool, "%"PRId64, value); + array_idx_set(&stmt->args, column_idx, &value_str); + + if (stmt->db->v.statement_bind_int64 != NULL) + stmt->db->v.statement_bind_int64(stmt, column_idx, value); +} + +#undef sql_statement_query +void sql_statement_query(struct sql_statement **_stmt, + sql_query_callback_t *callback, void *context) +{ + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; + if (stmt->db->v.statement_query != NULL) + stmt->db->v.statement_query(stmt, callback, context); + else + default_sql_statement_query(stmt, callback, context); +} + +struct sql_result *sql_statement_query_s(struct sql_statement **_stmt) +{ + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; + if (stmt->db->v.statement_query_s != NULL) + return stmt->db->v.statement_query_s(stmt); + else + return default_sql_statement_query_s(stmt); +} + +void sql_result_ref(struct sql_result *result) +{ + result->refcount++; +} + +void sql_result_unref(struct sql_result *result) +{ + i_assert(result->refcount > 0); + if (--result->refcount > 0) + return; + + i_free(result->map); + result->v.free(result); +} + +static const struct sql_field_def * +sql_field_def_find(const struct sql_field_def *fields, const char *name) +{ + unsigned int i; + + for (i = 0; fields[i].name != NULL; i++) { + if (strcasecmp(fields[i].name, name) == 0) + return &fields[i]; + } + return NULL; +} + +static void +sql_result_build_map(struct sql_result *result, + const struct sql_field_def *fields, size_t dest_size) +{ + const struct sql_field_def *def; + const char *name; + unsigned int i, count, field_size = 0; + + count = sql_result_get_fields_count(result); + + result->map_size = count; + result->map = i_new(struct sql_field_map, result->map_size); + for (i = 0; i < count; i++) { + name = sql_result_get_field_name(result, i); + def = sql_field_def_find(fields, name); + if (def != NULL) { + result->map[i].type = def->type; + result->map[i].offset = def->offset; + switch (def->type) { + case SQL_TYPE_STR: + field_size = sizeof(const char *); + break; + case SQL_TYPE_UINT: + field_size = sizeof(unsigned int); + break; + case SQL_TYPE_ULLONG: + field_size = sizeof(unsigned long long); + break; + case SQL_TYPE_BOOL: + field_size = sizeof(bool); + break; + } + i_assert(def->offset + field_size <= dest_size); + } else { + result->map[i].offset = SIZE_MAX; + } + } +} + +void sql_result_setup_fetch(struct sql_result *result, + const struct sql_field_def *fields, + void *dest, size_t dest_size) +{ + if (result->map == NULL) + sql_result_build_map(result, fields, dest_size); + result->fetch_dest = dest; + result->fetch_dest_size = dest_size; +} + +static void sql_result_fetch(struct sql_result *result) +{ + unsigned int i, count; + const char *value; + void *ptr; + + memset(result->fetch_dest, 0, result->fetch_dest_size); + count = result->map_size; + for (i = 0; i < count; i++) { + if (result->map[i].offset == SIZE_MAX) + continue; + + value = sql_result_get_field_value(result, i); + ptr = STRUCT_MEMBER_P(result->fetch_dest, + result->map[i].offset); + + switch (result->map[i].type) { + case SQL_TYPE_STR: { + *((const char **)ptr) = value; + break; + } + case SQL_TYPE_UINT: { + if (value != NULL && + str_to_uint(value, (unsigned int *)ptr) < 0) + i_error("sql: Value not uint: %s", value); + break; + } + case SQL_TYPE_ULLONG: { + if (value != NULL && + str_to_ullong(value, (unsigned long long *)ptr) < 0) + i_error("sql: Value not ullong: %s", value); + break; + } + case SQL_TYPE_BOOL: { + if (value != NULL && (*value == 't' || *value == '1')) + *((bool *)ptr) = TRUE; + break; + } + } + } +} + +int sql_result_next_row(struct sql_result *result) +{ + int ret; + + if ((ret = result->v.next_row(result)) <= 0) + return ret; + + if (result->fetch_dest != NULL) + sql_result_fetch(result); + return 1; +} + +#undef sql_result_more +void sql_result_more(struct sql_result **result, + sql_query_callback_t *callback, void *context) +{ + i_assert((*result)->v.more != NULL); + + (*result)->v.more(result, TRUE, callback, context); +} + +static void +sql_result_more_sync_callback(struct sql_result *result, void *context) +{ + struct sql_result **dest_result = context; + + *dest_result = result; +} + +void sql_result_more_s(struct sql_result **result) +{ + i_assert((*result)->v.more != NULL); + + (*result)->v.more(result, FALSE, sql_result_more_sync_callback, result); + /* the callback must have been called */ + i_assert(*result != NULL); +} + +unsigned int sql_result_get_fields_count(struct sql_result *result) +{ + return result->v.get_fields_count(result); +} + +const char *sql_result_get_field_name(struct sql_result *result, + unsigned int idx) +{ + return result->v.get_field_name(result, idx); +} + +int sql_result_find_field(struct sql_result *result, const char *field_name) +{ + return result->v.find_field(result, field_name); +} + +const char *sql_result_get_field_value(struct sql_result *result, + unsigned int idx) +{ + return result->v.get_field_value(result, idx); +} + +const unsigned char * +sql_result_get_field_value_binary(struct sql_result *result, + unsigned int idx, size_t *size_r) +{ + return result->v.get_field_value_binary(result, idx, size_r); +} + +const char *sql_result_find_field_value(struct sql_result *result, + const char *field_name) +{ + return result->v.find_field_value(result, field_name); +} + +const char *const *sql_result_get_values(struct sql_result *result) +{ + return result->v.get_values(result); +} + +const char *sql_result_get_error(struct sql_result *result) +{ + return result->v.get_error(result); +} + +enum sql_result_error_type sql_result_get_error_type(struct sql_result *result) +{ + return result->error_type; +} + +static void +sql_result_not_connected_free(struct sql_result *result ATTR_UNUSED) +{ +} + +static int +sql_result_not_connected_next_row(struct sql_result *result ATTR_UNUSED) +{ + return -1; +} + +static const char * +sql_result_not_connected_get_error(struct sql_result *result ATTR_UNUSED) +{ + return SQL_ERRSTR_NOT_CONNECTED; +} + +struct sql_transaction_context *sql_transaction_begin(struct sql_db *db) +{ + return db->v.transaction_begin(db); +} + +#undef sql_transaction_commit +void sql_transaction_commit(struct sql_transaction_context **_ctx, + sql_commit_callback_t *callback, void *context) +{ + struct sql_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + ctx->db->v.transaction_commit(ctx, callback, context); +} + +int sql_transaction_commit_s(struct sql_transaction_context **_ctx, + const char **error_r) +{ + struct sql_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + return ctx->db->v.transaction_commit_s(ctx, error_r); +} + +void sql_transaction_rollback(struct sql_transaction_context **_ctx) +{ + struct sql_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + ctx->db->v.transaction_rollback(ctx); +} + +void sql_update(struct sql_transaction_context *ctx, const char *query) +{ + ctx->db->v.update(ctx, query, NULL); +} + +void sql_update_stmt(struct sql_transaction_context *ctx, + struct sql_statement **_stmt) +{ + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; + if (ctx->db->v.update_stmt != NULL) + ctx->db->v.update_stmt(ctx, stmt, NULL); + else + default_sql_update_stmt(ctx, stmt, NULL); +} + +void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query, + unsigned int *affected_rows) +{ + ctx->db->v.update(ctx, query, affected_rows); +} + +void sql_update_stmt_get_rows(struct sql_transaction_context *ctx, + struct sql_statement **_stmt, + unsigned int *affected_rows) +{ + struct sql_statement *stmt = *_stmt; + + *_stmt = NULL; + if (ctx->db->v.update_stmt != NULL) + ctx->db->v.update_stmt(ctx, stmt, affected_rows); + else + default_sql_update_stmt(ctx, stmt, affected_rows); +} + +void sql_db_set_state(struct sql_db *db, enum sql_db_state state) +{ + enum sql_db_state old_state = db->state; + + if (db->state == state) + return; + + db->state = state; + if (db->state_change_callback != NULL) { + db->state_change_callback(db, old_state, + db->state_change_context); + } +} + +void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool, + const char *query, unsigned int *affected_rows) +{ + struct sql_transaction_query *tquery; + + tquery = p_new(pool, struct sql_transaction_query, 1); + tquery->trans = ctx; + tquery->query = p_strdup(pool, query); + tquery->affected_rows = affected_rows; + + if (ctx->head == NULL) + ctx->head = tquery; + else + ctx->tail->next = tquery; + ctx->tail = tquery; +} + +void sql_connection_log_finished(struct sql_db *db) +{ + struct event_passthrough *e = event_create_passthrough(db->event)-> + set_name(SQL_CONNECTION_FINISHED); + e_debug(e->event(), + "Connection finished (queries=%"PRIu64", slow queries=%"PRIu64")", + db->succeeded_queries + db->failed_queries, + db->slow_queries); +} + +struct event_passthrough * +sql_query_finished_event(struct sql_db *db, struct event *event, const char *query, + bool success, int *duration_r) +{ + int diff; + struct timeval tv; + event_get_create_time(event, &tv); + struct event_passthrough *e = event_create_passthrough(event)-> + set_name(SQL_QUERY_FINISHED)-> + add_str("query_first_word", t_strcut(query, ' ')); + diff = timeval_diff_msecs(&ioloop_timeval, &tv); + + if (!success) { + db->failed_queries++; + } else { + db->succeeded_queries++; + } + + if (diff >= SQL_SLOW_QUERY_MSEC) { + e->add_str("slow_query", "y"); + db->slow_queries++; + } + + if (duration_r != NULL) + *duration_r = diff; + + return e; +} + +struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx) +{ + return event_create_passthrough(ctx->event)-> + set_name(SQL_TRANSACTION_FINISHED); +} + +void sql_wait(struct sql_db *db) +{ + if (db->v.wait != NULL) + db->v.wait(db); +} + + +struct sql_result sql_not_connected_result = { + .v = { + sql_result_not_connected_free, + sql_result_not_connected_next_row, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, + sql_result_not_connected_get_error, + NULL, + }, + .failed_try_retry = TRUE +}; diff --git a/src/lib-sql/sql-api.h b/src/lib-sql/sql-api.h new file mode 100644 index 0000000..669a851 --- /dev/null +++ b/src/lib-sql/sql-api.h @@ -0,0 +1,251 @@ +#ifndef SQL_API_H +#define SQL_API_H + +struct timespec; + +/* This SQL API is designed to work asynchronously. The underlying drivers + however may not. */ + +enum sql_db_flags { + /* Set if queries are not executed asynchronously */ + SQL_DB_FLAG_BLOCKING = 0x01, + /* Set if database wants to use connection pooling */ + SQL_DB_FLAG_POOLED = 0x02, + /* Prepared statements are supported by the database. If they aren't, + the functions can still be used, but they're just internally + convered into regular statements. */ + SQL_DB_FLAG_PREP_STATEMENTS = 0x04, + /* Database supports INSERT .. ON DUPLICATE KEY syntax. */ + SQL_DB_FLAG_ON_DUPLICATE_KEY = 0x08, + /* Database supports INSERT .. ON CONFLICT DO UPDATE syntax. */ + SQL_DB_FLAG_ON_CONFLICT_DO = 0x10, +}; + +enum sql_field_type { + SQL_TYPE_STR, + SQL_TYPE_UINT, + SQL_TYPE_ULLONG, + SQL_TYPE_BOOL +}; + +struct sql_field_def { + enum sql_field_type type; + const char *name; + size_t offset; +}; + +enum sql_result_error_type { + SQL_RESULT_ERROR_TYPE_UNKNOWN = 0, + /* It's unknown whether write succeeded or not. This could be due to + a timeout or a disconnection from server. */ + SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN +}; + +enum sql_result_next { + /* Row was returned */ + SQL_RESULT_NEXT_OK = 1, + /* There are no more rows */ + SQL_RESULT_NEXT_LAST = 0, + /* Error occurred - see sql_result_get_error*() */ + SQL_RESULT_NEXT_ERROR = -1, + /* There are more results - call sql_result_more() */ + SQL_RESULT_NEXT_MORE = -99 +}; + +#define SQL_DEF_STRUCT(name, struct_name, type, c_type) \ + { (type) + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \ + ((struct struct_name *)0)->name, c_type), \ + #name, offsetof(struct struct_name, name) } + +#define SQL_DEF_STRUCT_STR(name, struct_name) \ + SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_STR, const char *) +#define SQL_DEF_STRUCT_UINT(name, struct_name) \ + SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_UINT, unsigned int) +#define SQL_DEF_STRUCT_ULLONG(name, struct_name) \ + SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_ULLONG, unsigned long long) +#define SQL_DEF_STRUCT_BOOL(name, struct_name) \ + SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_BOOL, bool) + +struct sql_db; +struct sql_result; + +struct sql_commit_result { + const char *error; + enum sql_result_error_type error_type; +}; + +struct sql_settings { + const char *driver; + const char *connect_string; + struct event *event_parent; +}; + +typedef void sql_query_callback_t(struct sql_result *result, void *context); +typedef void sql_commit_callback_t(const struct sql_commit_result *result, void *context); + +void sql_drivers_init(void); +void sql_drivers_deinit(void); + +/* register all built-in SQL drivers */ +void sql_drivers_register_all(void); + +void sql_driver_register(const struct sql_db *driver); +void sql_driver_unregister(const struct sql_db *driver); + +/* Initialize database connections. db_driver is the database driver name, + eg. "mysql" or "pgsql". connect_string is driver-specific. */ +struct sql_db *sql_init(const char *db_driver, const char *connect_string); +int sql_init_full(const struct sql_settings *set, struct sql_db **db_r, + const char **error_r); + +void sql_ref(struct sql_db *db); +void sql_unref(struct sql_db **db); + +/* Returns SQL database state flags. */ +enum sql_db_flags sql_get_flags(struct sql_db *db); + +/* Explicitly connect to the database. It's not required to call this function + though. Returns -1 if we're not connected, 0 if we started connecting or + 1 if we are fully connected now. */ +int sql_connect(struct sql_db *db); +/* Explicitly disconnect from database and abort pending auth requests. */ +void sql_disconnect(struct sql_db *db); + +/* Escape the given string if needed and return it. */ +const char *sql_escape_string(struct sql_db *db, const char *string); +/* Escape the given data as a string. */ +const char *sql_escape_blob(struct sql_db *db, + const unsigned char *data, size_t size); + +/* Execute SQL query without waiting for results. */ +void sql_exec(struct sql_db *db, const char *query); +/* Execute SQL query and return result in callback. If fields list is given, + the returned fields are validated to be of correct type, and you can use + sql_result_next_row_get() */ +void sql_query(struct sql_db *db, const char *query, + sql_query_callback_t *callback, void *context); +#define sql_query(db, query, callback, context) \ + sql_query(db, query - \ + CALLBACK_TYPECHECK(callback, void (*)( \ + struct sql_result *, typeof(context))), \ + (sql_query_callback_t *)callback, context) +/* Execute blocking SQL query and return result. */ +struct sql_result *sql_query_s(struct sql_db *db, const char *query); + +struct sql_prepared_statement * +sql_prepared_statement_init(struct sql_db *db, const char *query_template); +void sql_prepared_statement_unref(struct sql_prepared_statement **prep_stmt); + +struct sql_statement * +sql_statement_init(struct sql_db *db, const char *query_template); +struct sql_statement * +sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt); +void sql_statement_abort(struct sql_statement **stmt); +void sql_statement_set_timestamp(struct sql_statement *stmt, + const struct timespec *ts); +void sql_statement_set_no_log_expanded_values(struct sql_statement *stmt, + bool no_expand); +void sql_statement_bind_str(struct sql_statement *stmt, + unsigned int column_idx, const char *value); +void sql_statement_bind_binary(struct sql_statement *stmt, + unsigned int column_idx, const void *value, + size_t value_size); +void sql_statement_bind_int64(struct sql_statement *stmt, + unsigned int column_idx, int64_t value); +void sql_statement_query(struct sql_statement **stmt, + sql_query_callback_t *callback, void *context); +#define sql_statement_query(stmt, callback, context) \ + sql_statement_query(stmt, \ + (sql_query_callback_t *)callback, TRUE ? context : \ + CALLBACK_TYPECHECK(callback, void (*)( \ + struct sql_result *, typeof(context)))) +struct sql_result *sql_statement_query_s(struct sql_statement **stmt); + +void sql_result_setup_fetch(struct sql_result *result, + const struct sql_field_def *fields, + void *dest, size_t dest_size); + +/* Go to next row. See enum sql_result_next. */ +int sql_result_next_row(struct sql_result *result); + +/* If sql_result_next_row() returned SQL_RESULT_NEXT_MORE, this can be called + to continue returning more results. The result is freed with this call, so + it must not be accesed anymore until the callback is finished. */ +void sql_result_more(struct sql_result **result, + sql_query_callback_t *callback, void *context); +#define sql_result_more(result, callback, context) \ + sql_result_more(result - \ + CALLBACK_TYPECHECK(callback, void (*)( \ + struct sql_result *, typeof(context))), \ + (sql_query_callback_t *)callback, context) +/* Synchronous version of sql_result_more(). The result will be replaced with + the new result. */ +void sql_result_more_s(struct sql_result **result); + +void sql_result_ref(struct sql_result *result); +/* Needs to be called only with sql_query_s() or when result has been + explicitly referenced. */ +void sql_result_unref(struct sql_result *result); + +/* Return number of fields in result. */ +unsigned int sql_result_get_fields_count(struct sql_result *result); +/* Return name of the given field index. */ +const char *sql_result_get_field_name(struct sql_result *result, + unsigned int idx); +/* Return field index for given name, or -1 if not found. */ +int sql_result_find_field(struct sql_result *result, const char *field_name); + +/* Returns value of given field as string. Note that it can be NULL. */ +const char *sql_result_get_field_value(struct sql_result *result, + unsigned int idx); +/* Returns a binary value. Note that a NULL is returned as NULL with size=0, + while empty string returns non-NULL with size=0. */ +const unsigned char * +sql_result_get_field_value_binary(struct sql_result *result, + unsigned int idx, size_t *size_r); +/* Find the field and return its value. NULL return value can mean that either + the field didn't exist or that its value is NULL. */ +const char *sql_result_find_field_value(struct sql_result *result, + const char *field_name); +/* Return all values of current row. Note that this array is not + NULL-terminated - you must use sql_result_get_fields_count() to find out + the array's length. It's also possible that some of the values inside the + array are NULL. */ +const char *const *sql_result_get_values(struct sql_result *result); + +/* Return last error message in result. */ +const char *sql_result_get_error(struct sql_result *result); +enum sql_result_error_type sql_result_get_error_type(struct sql_result *result); + +/* Begin a new transaction. Currently you're limited to only one open + transaction at a time. */ +struct sql_transaction_context *sql_transaction_begin(struct sql_db *db); +/* Commit transaction. */ +void sql_transaction_commit(struct sql_transaction_context **ctx, + sql_commit_callback_t *callback, void *context); +#define sql_transaction_commit(ctx, callback, context) \ + sql_transaction_commit(ctx - \ + CALLBACK_TYPECHECK(callback, void (*)( \ + const struct sql_commit_result *, typeof(context))), \ + (sql_commit_callback_t *)callback, context) +/* Synchronous commit. Returns 0 if ok, -1 if error. */ +int sql_transaction_commit_s(struct sql_transaction_context **ctx, + const char **error_r); +void sql_transaction_rollback(struct sql_transaction_context **ctx); + +/* Execute query in given transaction. */ +void sql_update(struct sql_transaction_context *ctx, const char *query); +void sql_update_stmt(struct sql_transaction_context *ctx, + struct sql_statement **stmt); +/* Save the number of rows updated by this query. The value is set before + commit callback is called. */ +void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query, + unsigned int *affected_rows); +void sql_update_stmt_get_rows(struct sql_transaction_context *ctx, + struct sql_statement **stmt, + unsigned int *affected_rows); + +/* Wait for SQL query results. */ +void sql_wait(struct sql_db *db); + +#endif diff --git a/src/lib-sql/sql-db-cache.c b/src/lib-sql/sql-db-cache.c new file mode 100644 index 0000000..b2fb9fb --- /dev/null +++ b/src/lib-sql/sql-db-cache.c @@ -0,0 +1,156 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "sql-api-private.h" +#include "sql-db-cache.h" + +#define SQL_DB_CACHE_CONTEXT(obj) \ + MODULE_CONTEXT_REQUIRE(obj, sql_db_cache_module) + +struct sql_db_cache_context { + union sql_db_module_context module_ctx; + struct sql_db *prev, *next; /* These are set while refcount=0 */ + + struct sql_db_cache *cache; + int refcount; + char *key; + void (*orig_deinit)(struct sql_db *db); +}; + +struct sql_db_cache { + HASH_TABLE(char *, struct sql_db *) dbs; + unsigned int unused_count, max_unused_connections; + struct sql_db *unused_tail, *unused_head; +}; + +static MODULE_CONTEXT_DEFINE_INIT(sql_db_cache_module, &sql_db_module_register); + +static void sql_db_cache_db_unref(struct sql_db *db) +{ + struct sql_db_cache_context *ctx = SQL_DB_CACHE_CONTEXT(db); + struct sql_db_cache_context *head_ctx; + + if (--ctx->refcount > 0) + return; + + i_assert(db->refcount == 2); + + ctx->cache->unused_count++; + if (ctx->cache->unused_tail == NULL) + ctx->cache->unused_tail = db; + else { + head_ctx = SQL_DB_CACHE_CONTEXT(ctx->cache->unused_head); + head_ctx->next = db; + } + ctx->prev = ctx->cache->unused_head; + ctx->cache->unused_head = db; +} + +static void sql_db_cache_unlink(struct sql_db_cache_context *ctx) +{ + struct sql_db_cache_context *prev_ctx, *next_ctx; + + i_assert(ctx->refcount == 0); + + if (ctx->prev == NULL) + ctx->cache->unused_tail = ctx->next; + else { + prev_ctx = SQL_DB_CACHE_CONTEXT(ctx->prev); + prev_ctx->next = ctx->next; + } + if (ctx->next == NULL) + ctx->cache->unused_head = ctx->prev; + else { + next_ctx = SQL_DB_CACHE_CONTEXT(ctx->next); + next_ctx->prev = ctx->prev; + } + ctx->cache->unused_count--; +} + +static void sql_db_cache_free_tail(struct sql_db_cache *cache) +{ + struct sql_db *db; + struct sql_db_cache_context *ctx; + + db = cache->unused_tail; + i_assert(db->refcount == 1); + + ctx = SQL_DB_CACHE_CONTEXT(db); + sql_db_cache_unlink(ctx); + hash_table_remove(cache->dbs, ctx->key); + + i_free(ctx->key); + i_free(ctx); + + db->v.unref = NULL; + sql_unref(&db); +} + +static void sql_db_cache_drop_oldest(struct sql_db_cache *cache) +{ + while (cache->unused_count >= cache->max_unused_connections) + sql_db_cache_free_tail(cache); +} + +int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set, + struct sql_db **db_r, const char **error_r) +{ + struct sql_db_cache_context *ctx; + struct sql_db *db; + char *key; + + key = i_strdup_printf("%s\t%s", set->driver, set->connect_string); + db = hash_table_lookup(cache->dbs, key); + if (db != NULL) { + ctx = SQL_DB_CACHE_CONTEXT(db); + if (ctx->refcount == 0) { + sql_db_cache_unlink(ctx); + ctx->prev = ctx->next = NULL; + } + i_free(key); + } else { + sql_db_cache_drop_oldest(cache); + + if (sql_init_full(set, &db, error_r) < 0) { + i_free(key); + return -1; + } + + ctx = i_new(struct sql_db_cache_context, 1); + ctx->cache = cache; + ctx->key = key; + ctx->orig_deinit = db->v.deinit; + db->v.unref = sql_db_cache_db_unref; + + MODULE_CONTEXT_SET(db, sql_db_cache_module, ctx); + hash_table_insert(cache->dbs, ctx->key, db); + } + + ctx->refcount++; + sql_ref(db); + *db_r = db; + return 0; +} + +struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections) +{ + struct sql_db_cache *cache; + + cache = i_new(struct sql_db_cache, 1); + hash_table_create(&cache->dbs, default_pool, 0, str_hash, strcmp); + cache->max_unused_connections = max_unused_connections; + return cache; +} + +void sql_db_cache_deinit(struct sql_db_cache **_cache) +{ + struct sql_db_cache *cache = *_cache; + + *_cache = NULL; + while (cache->unused_tail != NULL) + sql_db_cache_free_tail(cache); + hash_table_destroy(&cache->dbs); + i_free(cache); +} diff --git a/src/lib-sql/sql-db-cache.h b/src/lib-sql/sql-db-cache.h new file mode 100644 index 0000000..1517f5f --- /dev/null +++ b/src/lib-sql/sql-db-cache.h @@ -0,0 +1,13 @@ +#ifndef SQL_DB_CACHE_H +#define SQL_DB_CACHE_H + +struct sql_db_cache; + +/* Like sql_init(), but use a connection pool. */ +int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set, + struct sql_db **db_r, const char **error_r); + +struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections); +void sql_db_cache_deinit(struct sql_db_cache **cache); + +#endif |