2
0
Эх сурвалжийг харах

Merge pull request #1432 from agranig/agranig/db_redis

db_redis: Implement db_redis generic db driver
Andreas Granig 7 жил өмнө
parent
commit
94e2fe9ac3
86 өөрчлөгдсөн 4395 нэмэгдсэн , 3 устгасан
  1. 126 0
      doc/stylesheets/dbschema_k/xsl/db_redis.xsl
  2. 1 1
      src/Makefile.groups
  3. 17 2
      src/lib/srdb1/schema/Makefile
  4. 31 0
      src/modules/db_redis/Makefile
  5. 189 0
      src/modules/db_redis/README
  6. 115 0
      src/modules/db_redis/db_redis_mod.c
  7. 53 0
      src/modules/db_redis/db_redis_mod.h
  8. 4 0
      src/modules/db_redis/doc/Makefile
  9. 33 0
      src/modules/db_redis/doc/db_redis.xml
  10. 192 0
      src/modules/db_redis/doc/db_redis_admin.xml
  11. 301 0
      src/modules/db_redis/redis_connection.c
  12. 73 0
      src/modules/db_redis/redis_connection.h
  13. 2128 0
      src/modules/db_redis/redis_dbase.c
  14. 88 0
      src/modules/db_redis/redis_dbase.h
  15. 838 0
      src/modules/db_redis/redis_table.c
  16. 66 0
      src/modules/db_redis/redis_table.h
  17. 2 0
      utils/kamctl/db_redis/kamailio/acc
  18. 2 0
      utils/kamctl/db_redis/kamailio/acc_cdrs
  19. 2 0
      utils/kamctl/db_redis/kamailio/active_watchers
  20. 2 0
      utils/kamctl/db_redis/kamailio/address
  21. 2 0
      utils/kamctl/db_redis/kamailio/aliases
  22. 2 0
      utils/kamctl/db_redis/kamailio/carrier_name
  23. 2 0
      utils/kamctl/db_redis/kamailio/carrierfailureroute
  24. 2 0
      utils/kamctl/db_redis/kamailio/carrierroute
  25. 2 0
      utils/kamctl/db_redis/kamailio/cpl
  26. 2 0
      utils/kamctl/db_redis/kamailio/dbaliases
  27. 2 0
      utils/kamctl/db_redis/kamailio/dialog
  28. 2 0
      utils/kamctl/db_redis/kamailio/dialog_vars
  29. 2 0
      utils/kamctl/db_redis/kamailio/dialplan
  30. 2 0
      utils/kamctl/db_redis/kamailio/dispatcher
  31. 2 0
      utils/kamctl/db_redis/kamailio/domain
  32. 2 0
      utils/kamctl/db_redis/kamailio/domain_attrs
  33. 2 0
      utils/kamctl/db_redis/kamailio/domain_name
  34. 2 0
      utils/kamctl/db_redis/kamailio/domainpolicy
  35. 2 0
      utils/kamctl/db_redis/kamailio/dr_gateways
  36. 2 0
      utils/kamctl/db_redis/kamailio/dr_groups
  37. 2 0
      utils/kamctl/db_redis/kamailio/dr_gw_lists
  38. 2 0
      utils/kamctl/db_redis/kamailio/dr_rules
  39. 2 0
      utils/kamctl/db_redis/kamailio/globalblacklist
  40. 2 0
      utils/kamctl/db_redis/kamailio/grp
  41. 2 0
      utils/kamctl/db_redis/kamailio/htable
  42. 2 0
      utils/kamctl/db_redis/kamailio/imc_members
  43. 2 0
      utils/kamctl/db_redis/kamailio/imc_rooms
  44. 2 0
      utils/kamctl/db_redis/kamailio/lcr_gw
  45. 2 0
      utils/kamctl/db_redis/kamailio/lcr_rule
  46. 2 0
      utils/kamctl/db_redis/kamailio/lcr_rule_target
  47. 2 0
      utils/kamctl/db_redis/kamailio/location
  48. 2 0
      utils/kamctl/db_redis/kamailio/location_attrs
  49. 2 0
      utils/kamctl/db_redis/kamailio/matrix
  50. 2 0
      utils/kamctl/db_redis/kamailio/missed_calls
  51. 2 0
      utils/kamctl/db_redis/kamailio/mohqcalls
  52. 2 0
      utils/kamctl/db_redis/kamailio/mohqueues
  53. 2 0
      utils/kamctl/db_redis/kamailio/mtree
  54. 2 0
      utils/kamctl/db_redis/kamailio/mtrees
  55. 2 0
      utils/kamctl/db_redis/kamailio/pdt
  56. 2 0
      utils/kamctl/db_redis/kamailio/pl_pipes
  57. 2 0
      utils/kamctl/db_redis/kamailio/presentity
  58. 2 0
      utils/kamctl/db_redis/kamailio/pua
  59. 2 0
      utils/kamctl/db_redis/kamailio/purplemap
  60. 2 0
      utils/kamctl/db_redis/kamailio/re_grp
  61. 2 0
      utils/kamctl/db_redis/kamailio/rls_presentity
  62. 2 0
      utils/kamctl/db_redis/kamailio/rls_watchers
  63. 2 0
      utils/kamctl/db_redis/kamailio/rtpengine
  64. 2 0
      utils/kamctl/db_redis/kamailio/rtpproxy
  65. 2 0
      utils/kamctl/db_redis/kamailio/sca_subscriptions
  66. 2 0
      utils/kamctl/db_redis/kamailio/silo
  67. 2 0
      utils/kamctl/db_redis/kamailio/sip_trace
  68. 2 0
      utils/kamctl/db_redis/kamailio/speed_dial
  69. 2 0
      utils/kamctl/db_redis/kamailio/subscriber
  70. 2 0
      utils/kamctl/db_redis/kamailio/topos_d
  71. 2 0
      utils/kamctl/db_redis/kamailio/topos_t
  72. 2 0
      utils/kamctl/db_redis/kamailio/trusted
  73. 2 0
      utils/kamctl/db_redis/kamailio/uacreg
  74. 2 0
      utils/kamctl/db_redis/kamailio/uid_credentials
  75. 2 0
      utils/kamctl/db_redis/kamailio/uid_domain
  76. 2 0
      utils/kamctl/db_redis/kamailio/uid_domain_attrs
  77. 2 0
      utils/kamctl/db_redis/kamailio/uid_global_attrs
  78. 2 0
      utils/kamctl/db_redis/kamailio/uid_uri
  79. 2 0
      utils/kamctl/db_redis/kamailio/uid_uri_attrs
  80. 2 0
      utils/kamctl/db_redis/kamailio/uid_user_attrs
  81. 2 0
      utils/kamctl/db_redis/kamailio/uri
  82. 2 0
      utils/kamctl/db_redis/kamailio/userblacklist
  83. 2 0
      utils/kamctl/db_redis/kamailio/usr_preferences
  84. 2 0
      utils/kamctl/db_redis/kamailio/version
  85. 2 0
      utils/kamctl/db_redis/kamailio/watchers
  86. 2 0
      utils/kamctl/db_redis/kamailio/xcap

+ 126 - 0
doc/stylesheets/dbschema_k/xsl/db_redis.xsl

@@ -0,0 +1,126 @@
+<?xml version='1.0'?>
+<!--
+ * $Id: $
+ *
+ * XSL converter script for db_redis
+ *
+ * Copyright (C) 2001-2007 FhG Fokus
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+-->
+
+
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                version='1.0'
+                xmlns:xi="http://www.w3.org/2001/XInclude">
+
+    <xsl:import href="common.xsl"/>
+    <xsl:output method="text" indent="no" omit-xml-declaration="yes"/>
+
+    <!-- Create the file for the table in dbtext subdirectory -->
+    <xsl:template match="table">
+	<xsl:variable name="name">
+	    <xsl:call-template name="get-name"/>
+	</xsl:variable>
+
+	<xsl:variable name="path" select="concat($dir, concat('/', concat($prefix, $name)))"/>
+	<xsl:document href="{$path}" method="text" indent="no" omit-xml-declaration="yes">
+		<xsl:apply-imports/>
+		<!-- Insert version data -->
+		 <xsl:apply-templates select="version"/>
+		<!-- this is not exactly what we want for dbtext, as the version data gets
+		     appended to the actual table file, and no to the 'version' table.
+		     But its not possible (at least with XSL 1.0, AFAIK) to append data to a
+		     file. So it's much more easier to do this in the Makefile -->
+	</xsl:document>
+    </xsl:template>
+
+    <!-- version data template -->
+    <xsl:template match="version">
+	<xsl:value-of select="text()"/>
+	<xsl:text>&#x0A;</xsl:text>
+    </xsl:template>
+
+    <!-- Create column definitions -->
+    <xsl:template match="column">
+	<xsl:variable name="type">
+	    <xsl:call-template name="get-type"/>
+	</xsl:variable>
+
+	<xsl:variable name="null">
+	    <xsl:call-template name="get-null"/>
+	</xsl:variable>
+
+	<xsl:call-template name="get-name"/>
+	<xsl:text>/</xsl:text>
+	<xsl:choose>
+	    <xsl:when test="type[@db=$db]">
+		<xsl:value-of select="normalize-space(type[@db=$db])"/>
+	    </xsl:when>
+	    <xsl:when test="$type='char' or
+						$type='short' or
+						$type='int' or
+						$type='long' or
+						$type='datetime'">
+		<xsl:text>int</xsl:text>
+	    </xsl:when>
+	    <xsl:when test="$type='float' or
+						$type='double'">
+		<xsl:text>double</xsl:text>
+	    </xsl:when>
+	    <xsl:when test="$type='string' or
+						$type='text' or
+						$type='binary' or
+                        $type='largetext' or
+						$type='largebinary'">
+		<xsl:text>string</xsl:text>
+	    </xsl:when>
+	    <xsl:otherwise>
+		<xsl:call-template name="type-error"/>
+	    </xsl:otherwise>
+	</xsl:choose>
+
+    <!--
+	<xsl:if test="$null=1">
+	    <xsl:text>&amp;null</xsl:text>
+    </xsl:if>
+    -->
+	<xsl:text>,</xsl:text>
+	<xsl:if test="position()=last()">
+	    <xsl:text>&#x0A;</xsl:text>
+	</xsl:if>
+    </xsl:template>
+
+    <!-- Escape all : occurrences -->
+    <xsl:template name="escape">
+	<xsl:param name="value"/>
+	<xsl:choose>
+	    <xsl:when test="contains($value, ':')">
+		<xsl:value-of select="concat(substring-before($value, ':'), '\:')"/>
+		<xsl:call-template name="escape">
+		    <xsl:with-param name="value" select="substring-after($value, ':')"/>
+		</xsl:call-template>
+	    </xsl:when>
+	    <xsl:otherwise>
+		<xsl:value-of select="$value"/>
+	    </xsl:otherwise>
+	</xsl:choose>
+    </xsl:template>
+
+</xsl:stylesheet>

+ 1 - 1
src/Makefile.groups

@@ -153,7 +153,7 @@ mod_list_jansson=jansson
 mod_list_jansson_event=janssonrpcc
 
 # - modules depending on redis library
-mod_list_redis=ndb_redis topos_redis
+mod_list_redis=db_redis ndb_redis topos_redis
 
 # - modules depending on mono library
 mod_list_mono=app_mono

+ 17 - 2
src/lib/srdb1/schema/Makefile

@@ -37,6 +37,9 @@ ORACLE_XSL = $(STYLESHEETS)/oracle.xsl
 # Stylesheet used to generate mongodb database schema
 MONGODB_XSL = $(STYLESHEETS)/mongodb.xsl
 
+# Stylesheet used to generate Redis database schema
+DB_REDIS_XSL = $(STYLESHEETS)/db_redis.xsl
+
 # Stylesheet used to generate docbook documentation
 DOCBOOK_XSL = $(STYLESHEETS)/docbook.xsl
 
@@ -69,7 +72,7 @@ ifeq ($(VERBOSE), 1)
 	override XSLTPROC := $(XSLTPROC) --verbose
 endif
 
-all: mysql postgres dbtext db_berkeley db_sqlite oracle mongodb pi_framework
+all: mysql postgres dbtext db_berkeley db_sqlite oracle mongodb db_redis pi_framework
 
 .PHONY: pi_framework pi_framework_clean
 pi_framework:
@@ -227,6 +230,18 @@ mongodb:
 mongodb_clean:
 	-@rm -f $(SCHEME)/mongodb/kamailio/*
 
+.PHONY: db_redis db_redis_clean
+db_redis:
+	for FILE in $(TABLES); do \
+		XML_CATALOG_FILES=$(CATALOG) $(XSLTPROC) $(XSLTPROC_FLAGS) \
+		--stringparam dir "$(SCHEME)/db_redis/kamailio" \
+		--stringparam prefix "" \
+		--stringparam db "db_redis" \
+		$(DB_REDIS_XSL) kamailio-"$$FILE".xml ; \
+	done
+
+db_redis_clean:
+	-@rm -f $(SCHEME)/db_redis/*
 
 .PHONY: docbook-xml
 docbook-xml:
@@ -350,4 +365,4 @@ dbdoc_clean:
 	done
 
 .PHONY: clean
-clean: mysql_clean postgres_clean oracle_clean dbtext_clean db_berkeley_clean db_sqlite_clean pi_framework_clean docbook_clean # modules_clean dbdoc_clean
+clean: mysql_clean postgres_clean oracle_clean dbtext_clean db_berkeley_clean db_sqlite_clean mongodb_clean db_redis_clean pi_framework_clean docbook_clean # modules_clean dbdoc_clean

+ 31 - 0
src/modules/db_redis/Makefile

@@ -0,0 +1,31 @@
+#
+# WARNING: do not run this directly, it should be run by the master Makefile
+
+include ../../Makefile.defs
+auto_gen=
+NAME=db_redis.so
+
+ifeq ($(CROSS_COMPILE),)
+HIREDIS_BUILDER = $(shell \
+	if pkg-config --exists hiredis; then \
+		echo 'pkg-config hiredis'; \
+	fi)
+endif
+
+ifeq ($(HIREDIS_BUILDER),)
+	HIREDISDEFS=-I$(LOCALBASE)/include
+	HIREDISLIBS=-L$(LOCALBASE)/lib -lhiredis
+else
+	HIREDISDEFS = $(shell $(HIREDIS_BUILDER) --cflags)
+	HIREDISLIBS = $(shell $(HIREDIS_BUILDER) --libs)
+endif
+
+DEFS+=$(HIREDISDEFS)
+LIBS=$(HIREDISLIBS)
+
+DEFS+=-DKAMAILIO_MOD_INTERFACE
+
+SERLIBPATH=../../lib
+SER_LIBS=$(SERLIBPATH)/srdb2/srdb2 $(SERLIBPATH)/srdb1/srdb1
+
+include ../../Makefile.modules

+ 189 - 0
src/modules/db_redis/README

@@ -0,0 +1,189 @@
+DB_REDIS Module
+
+Andreas Granig
+
+   <[email protected]>
+
+Edited by
+
+Andreas Granig
+
+   <[email protected]>
+
+   Copyright © 2018 sipwise.com
+     __________________________________________________________________
+
+   Table of Contents
+
+   1. Admin Guide
+
+        1. Overview
+        2. Limitations
+        3. Dependencies
+
+              3.1. Kamailio Modules
+
+        4. Parameters
+
+              4.1. schema_path (string)
+              4.2. keys (string)
+
+        5. External Libraries or Applications
+        6. Usage
+
+   List of Examples
+
+   1.1. Setting schema_path module parameter
+   1.2. Setting keys module parameter
+   1.3. Usage
+
+Chapter 1. Admin Guide
+
+   Table of Contents
+
+   1. Overview
+   2. Limitations
+   3. Dependencies
+
+        3.1. Kamailio Modules
+
+   4. Parameters
+
+        4.1. schema_path (string)
+        4.2. keys (string)
+
+   5. External Libraries or Applications
+   6. Usage
+
+1. Overview
+
+   This module provides a DB APIv1 connector for Redis server.
+
+   It can be used as a replacement for other database modules such as
+   db_mysql and db_postgres. Not all the specs of DB APIv1 are
+   implemented, thus the usage of this module might be restricted to
+   specific modules. Also, for proper performance, the module needs
+   particular configuration tailored to the using modules.
+
+   Since Redis does not provide a schema by itself, db_redis ships with
+   schema files, which path has to be defined as module parameter
+   "schema_path". The schema definition is defined in one file per table
+   with the file name being the table name, and each file is composed of a
+   comma-separated list of column definitions in format
+   <column-name>/<type>[,<column-name>/<type> ...] in one line, followed
+   by a line holding the table version.
+
+   Example for location definition:
+username/string,domain/string,contact/string,received/string,path/string,expires
+/timestamp,q/double,callid/string,cseq/int,last_modified/timestamp,flags/int,cfl
+ags/int,user_agent/string,socket/string,methods/int,ruid/string,reg_id/int,insta
+nce/string,server_id/int,connection_id/int,keepalive/int,partition/int
+8
+
+   Also since Redis is a key-value store with keys having to be unique,
+   tables and rows e.g. from MySQL can not be ported 1:1 to Redis. For
+   instance, usrloc relies on a key "username@domain", but it must not be
+   unique for being able to store multiple contacts per AoR. Thus,
+   db_redis supports mapping sets in a way for example for usrloc to have
+   a set with a key "username@domain", with its entries being unique keys
+   per contact being the ruid of a contact. Thus, one contact in usrloc
+   consists of a unique key "location:entry::example-ruid-1" being a hash
+   with the columns like username, domain, contact, path etc. In addition,
+   this unique key is stored in a set
+   "location:usrdom::exampleuser:exampledomain.org". When usrloc does a
+   lookup based on "username@domain", db_redis figures out via the
+   keys/values the query is constructed by usrloc to look for the final
+   entry key in the mapping set first, then querying the actual entries
+   from there, avoiding full table scans. For usrloc, the same holds true
+   for expired contacts, requiring a different kind of mapping. There is a
+   certain balance of read performance vs. write performance to consider,
+   because inserts and deletes also have to maintain the mappings, in
+   favor of much faster selects. The mappings can be freely defined, so
+   even though other kamailio modules don't require a specific mapping to
+   be in place for proper performance, mappings could be defined for
+   external applications to read faster (for instance letting the acc
+   module also write mappings besides the actual records for billing
+   systems to correlate start and stop records faster).
+
+   The mappings can be freely defined in the "keys" module parameter. It
+   is composed of a semi-colon separated list of definitions in format
+   <table-name>=<entry>:<column-name>[&<map-name>:<column-name>,<column-na
+   me>...]. Each table must at least have an "entry" key for db_redis to
+   be able to store data.
+
+   Example:
+location=entry:ruid&usrdom:username,domain&timer:partition,keepalive;acc=entry:c
+allid,time_hires&cid:callid
+
+   For readability purposes, keys per table can be defined line by line by
+   providing multiple "keys" mod-params.
+
+   You can read more about Redis at: https://www.redis.io.
+
+2. Limitations
+
+     * This module has implemented the equivalent operations for INSERT,
+       UPDATE, DELETE and SELECT. The ORDER BY for SELECT is not
+       implemented. Raw query is not implemented inside this module, use
+       ndb_redis for sending any kind of command to a Redis server.
+
+3. Dependencies
+
+   3.1. Kamailio Modules
+
+3.1. Kamailio Modules
+
+   The following modules must be loaded before this module:
+     * none.
+
+4. Parameters
+
+   4.1. schema_path (string)
+   4.2. keys (string)
+
+4.1. schema_path (string)
+
+   The path to the schema of your tables (default
+   /usr/share/kamailio/db_redis).
+
+   Example 1.1. Setting schema_path module parameter
+modparam("db_redis", "schema_path", "/usr/local/share/kamailio/db_redis/kamailio
+")
+
+4.2. keys (string)
+
+   The entry and mapping keys of your tables.
+
+   Example 1.2. Setting keys module parameter
+modparam("db_redis", "keys", "version=entry:table_name;location=entry:ruid&usrdo
+m:username,domain&timer:partition,keepalive")
+
+5. External Libraries or Applications
+
+   The following libraries or applications must be installed before
+   running Kamailio with this module loaded:
+     * hiredis - available at https://github.com/redis/hiredis
+
+6. Usage
+
+   Load the module and set the the DB URL for specific modules to:
+   redis://[username]@host:port/database. Username is optional. Database
+   must be a valid redis database number.
+
+   Example 1.3. Usage
+...
+loadmodule "db_redis.so"
+...
+#!define DBURL_USRLOC "redis://127.0.0.1:6379/5"
+#!define DBURL_ACC    "redis://127.0.0.1:6379/6"
+#!define DBURL_AUTH   "redis://127.0.0.1:6379/7"
+...
+modparam("db_redis", "schema_path", "/usr/share/kamailio/db_redis/kamailio")
+modparam("db_redis", "keys", "location=entry:ruid&usrdom:username,domain&timer:p
+artition,keepalive")
+modparam("db_redis", "keys", "acc=entry:callid,time_hires&cid:callid")
+modparam("db_redis", "keys", "subscriber=entry:username,domain")
+modparam("usrloc", "db_url", DBURL_USRLOC)
+modparam("acc_db", "db_url", DBURL_ACC)
+modparam("auth_db", "db_url", DBURL_AUTH)
+...

+ 115 - 0
src/modules/db_redis/db_redis_mod.c

@@ -0,0 +1,115 @@
+/**
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#define DB_REDIS_DEBUG
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "db_redis_mod.h"
+#include "redis_dbase.h"
+#include "redis_table.h"
+
+MODULE_VERSION
+
+str redis_keys = str_init("");
+str redis_schema_path = str_init("/usr/share/kamailio/db_redis/kamailio");
+
+static int db_redis_bind_api(db_func_t *dbb);
+static int mod_init(void);
+static void mod_destroy(void);
+int keys_param(modparam_t type, void* val);
+
+static cmd_export_t cmds[] = {
+    {"db_bind_api",    (cmd_function)db_redis_bind_api,    0, 0, 0, 0},
+    {0, 0, 0, 0, 0, 0}
+};
+
+
+/*
+ * Exported parameters
+ */
+static param_export_t params[] = {
+    {"keys",        PARAM_STRING|USE_FUNC_PARAM, (void*)keys_param},
+    {"schema_path", PARAM_STR, &redis_schema_path },
+    {0, 0, 0}
+};
+
+
+struct module_exports exports = {
+    "db_redis",
+    DEFAULT_DLFLAGS, /* dlopen flags */
+    cmds,
+    params,             /*  module parameters */
+    0,                  /* exported statistics */
+    0,                  /* exported MI functions */
+    0,                  /* exported pseudo-variables */
+    0,                  /* extra processes */
+    mod_init,           /* module initialization function */
+    0,                  /* response function*/
+    mod_destroy,        /* destroy function */
+    0                   /* per-child init function */
+};
+
+static int db_redis_bind_api(db_func_t *dbb) {
+    if(dbb==NULL)
+        return -1;
+
+    memset(dbb, 0, sizeof(db_func_t));
+
+    dbb->use_table        = db_redis_use_table;
+    dbb->init             = db_redis_init;
+    dbb->close            = db_redis_close;
+    dbb->query            = db_redis_query;
+    dbb->fetch_result     = 0; //db_redis_fetch_result;
+    dbb->raw_query        = 0; //db_redis_raw_query;
+    dbb->free_result      = db_redis_free_result;
+    dbb->insert           = db_redis_insert;
+    dbb->delete           = db_redis_delete;
+    dbb->update           = db_redis_update;
+    dbb->replace          = 0; //db_redis_replace;
+
+    return 0;
+}
+
+int keys_param(modparam_t type, void *val)
+{
+	if (val == NULL)
+		return -1;
+	else
+		return db_redis_keys_spec((char*)val);
+}
+
+int mod_register(char *path, int *dlflags, void *p1, void *p2) {
+    if(db_api_init()<0)
+        return -1;
+    return 0;
+}
+
+static int mod_init(void) {
+    LM_DBG("module initializing\n");
+    return 0;
+}
+
+static void mod_destroy(void) {
+    LM_DBG("module destroying\n");
+}

+ 53 - 0
src/modules/db_redis/db_redis_mod.h

@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+
+#ifndef _DB_REDIS_MOD_H
+#define _DB_REDIS_MOD_H
+
+#include "../../lib/srdb1/db.h"
+#include "../../lib/srdb1/db_ut.h"
+#include "../../lib/srdb1/db_query.h"
+#include "../../lib/srdb1/db_pool.h"
+#include "../../lib/srdb1/db_id.h"
+#include "../../lib/srdb1/db_con.h"
+#include "../../lib/srdb1/db_res.h"
+#include "../../lib/srdb1/db_key.h"
+#include "../../lib/srdb1/db_op.h"
+#include "../../lib/srdb1/db_val.h"
+
+#include "../../core/mem/mem.h"
+
+#include "../../core/dprint.h"
+#include "../../core/sr_module.h"
+#include "../../core/str.h"
+#include "../../core/str_hash.h"
+#include "../../core/ut.h"
+
+#define REDIS_DIRECT_PREFIX "entry"
+#define REDIS_DIRECT_PREFIX_LEN 5
+
+#define REDIS_HT_SIZE 8
+
+extern str redis_keys;
+extern str redis_schema_path;
+
+#endif /* _DB_REDIS_MOD_H */

+ 4 - 0
src/modules/db_redis/doc/Makefile

@@ -0,0 +1,4 @@
+docs = db_redis.xml
+
+docbook_dir = ../../../docbook
+include $(docbook_dir)/Makefile.module

+ 33 - 0
src/modules/db_redis/doc/db_redis.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding='ISO-8859-1'?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
+"http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd" [
+
+<!-- Include general documentation entities -->
+<!ENTITY % docentities SYSTEM "../../../docbook/entities.xml">
+%docentities;
+
+]>
+<book xmlns:xi="http://www.w3.org/2001/XInclude">
+    <bookinfo>
+	<title>DB_REDIS Module</title>
+	<productname class="trade">kamailio.org</productname>
+	<authorgroup>
+	    <author>
+		<firstname>Andreas</firstname>
+		<surname>Granig</surname>
+		<email>[email protected]</email>
+	    </author>
+	    <editor>
+		<firstname>Andreas</firstname>
+		<surname>Granig</surname>
+		<email>[email protected]</email>
+	    </editor>
+	</authorgroup>
+	<copyright>
+	    <year>2018</year>
+	    <holder>sipwise.com</holder>
+	</copyright>
+    </bookinfo>
+    <toc></toc>
+    <xi:include href="db_redis_admin.xml"/>
+</book>

+ 192 - 0
src/modules/db_redis/doc/db_redis_admin.xml

@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding='ISO-8859-1'?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
+"http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd" [
+
+<!-- Include general documentation entities -->
+<!ENTITY % docentities SYSTEM "../../../docbook/entities.xml">
+%docentities;
+
+]>
+<!-- Module User's Guide -->
+
+<chapter>
+	<title>&adminguide;</title>
+	<section>
+		<title>Overview</title>
+		<para>
+			This module provides a DB APIv1 connector for Redis server.
+		</para>
+		<para>
+			It can be used as a replacement for other database modules such as
+			db_mysql and db_postgres. Not all the specs of DB APIv1 are
+			implemented, thus the usage of this module might be restricted to
+			specific modules. Also, for proper performance, the module needs
+			particular configuration tailored to the using modules.
+		</para>
+		<para>
+			Since Redis does not provide a schema by itself, db_redis ships with
+			schema files, which path has to be defined as module parameter "schema_path".
+			The schema definition is defined in one file per table with the file name
+			being the table name, and each file is composed of a
+			comma-separated list of column definitions in format
+			&lt;column-name&gt;/&lt;type&gt;[,&lt;column-name&gt;/&lt;type&gt; ...]
+			in one line, followed by a line holding the table version.
+		</para>
+		<para>
+			Example for location definition:
+			<programlisting format="linespecific">
+username/string,domain/string,contact/string,received/string,path/string,expires/timestamp,q/double,callid/string,cseq/int,last_modified/timestamp,flags/int,cflags/int,user_agent/string,socket/string,methods/int,ruid/string,reg_id/int,instance/string,server_id/int,connection_id/int,keepalive/int,partition/int
+8
+			</programlisting>
+		</para>
+		<para>
+			Also since Redis is a key-value store with keys having to be unique,
+			tables and rows e.g. from MySQL can not be ported 1:1 to Redis. For
+			instance, usrloc relies on a key "username@domain", but it must not be
+			unique for being able to store multiple contacts per AoR. Thus, db_redis
+			supports mapping sets in a way for example for usrloc to have a set with
+			a key "username@domain", with its entries being unique keys per contact being
+			the ruid of a contact. Thus, one contact in usrloc consists of a unique
+			key "location:entry::example-ruid-1" being a hash with the columns like
+			username, domain, contact, path etc. In addition, this unique key is stored
+			in a set "location:usrdom::exampleuser:exampledomain.org". When usrloc does
+			a lookup based on "username@domain", db_redis figures out via the keys/values
+			the query is constructed by usrloc to look for the final entry key in the
+			mapping set first, then querying the actual entries from there, avoiding full
+			table scans. For usrloc, the same holds true for expired contacts, requiring
+			a different kind of mapping. There is a certain balance of read performance
+			vs. write performance to consider, because inserts and deletes also have to
+			maintain the mappings, in favor of much faster selects. The mappings can be
+			freely defined, so even though other kamailio modules don't require a specific
+			mapping to be in place for proper performance, mappings could be defined
+			for external applications to read faster (for instance letting the acc module
+			also write mappings besides the actual records for billing systems to
+			correlate start and stop records faster).
+		</para>
+		<para>
+			The mappings can be freely defined in the "keys" module parameter. It is
+			composed of a semi-colon separated list of definitions in format
+			&lt;table-name&gt;=&lt;entry&gt;:&lt;column-name&gt;[&amp;&lt;map-name&gt;:&lt;column-name&gt;,&lt;column-name&gt;...].
+			Each table must at least have an "entry" key for db_redis to be able to store data.
+		</para>
+		<para>
+			Example:
+			<programlisting format="linespecific">
+location=entry:ruid&amp;usrdom:username,domain&amp;timer:partition,keepalive;acc=entry:callid,time_hires&amp;cid:callid
+			</programlisting>
+		</para>
+		<para>
+			For readability purposes, keys per table can be defined line by line by
+			providing multiple "keys" mod-params.
+		</para>
+		<para>
+			You can read more about Redis at:
+			<ulink url="https://www.redis.io">https://www.redis.io</ulink>.
+		</para>
+	</section>
+
+	<section>
+		<title>Limitations</title>
+		<itemizedlist>
+			<listitem>
+				<para>
+					This module has implemented the equivalent operations for INSERT,
+					UPDATE, DELETE and SELECT. The ORDER BY for SELECT is not implemented.
+					Raw query is not implemented inside this module, use ndb_redis for sending any
+					kind of command to a Redis server.
+				</para>
+			</listitem>
+		</itemizedlist>
+	</section>
+
+	<section>
+		<title>Dependencies</title>
+		<section>
+			<title>&kamailio; Modules</title>
+			<para>
+				The following modules must be loaded before this module:
+				<itemizedlist>
+					<listitem>
+						<para>
+							<emphasis>none</emphasis>.
+						</para>
+					</listitem>
+				</itemizedlist>
+			</para>
+		</section>
+	</section>
+
+	<section>
+		<title>Parameters</title>
+		<section>
+			<title><varname>schema_path</varname> (string)</title>
+			<para>
+				The path to the schema of your tables (default /usr/share/kamailio/db_redis).
+			</para>
+			<example>
+				<title>Setting schema_path module parameter</title>
+				<programlisting format="linespecific">
+modparam("db_redis", "schema_path", "/usr/local/share/kamailio/db_redis/kamailio")
+				</programlisting>
+			</example>
+		</section>
+
+		<section>
+			<title><varname>keys</varname> (string)</title>
+			<para>
+				The entry and mapping keys of your tables.
+			</para>
+			<example>
+				<title>Setting keys module parameter</title>
+				<programlisting format="linespecific">
+modparam("db_redis", "keys", "version=entry:table_name;location=entry:ruid&amp;usrdom:username,domain&amp;timer:partition,keepalive")
+				</programlisting>
+			</example>
+		</section>
+	</section>
+
+	<section>
+		<title>External Libraries or Applications</title>
+		<para>
+			The following libraries or applications must be installed before running
+			&kamailio; with this module loaded:
+			<itemizedlist>
+				<listitem>
+					<para>
+						<emphasis>hiredis</emphasis> - available at
+						<ulink url="https://github.com/redis/hiredis">https://github.com/redis/hiredis</ulink>
+					</para>
+				</listitem>
+			</itemizedlist>
+		</para>
+	</section>
+
+	<section>
+		<title>Usage</title>
+		<para>
+			Load the module and set the the DB URL for specific modules to:
+			redis://[username]@host:port/database. Username is optional.
+			Database must be a valid redis database number.
+		</para>
+		<example>
+			<title>Usage</title>
+			<programlisting format="linespecific">
+...
+loadmodule "db_redis.so"
+...
+#!define DBURL_USRLOC "redis://127.0.0.1:6379/5"
+#!define DBURL_ACC    "redis://127.0.0.1:6379/6"
+#!define DBURL_AUTH   "redis://127.0.0.1:6379/7"
+...
+modparam("db_redis", "schema_path", "/usr/share/kamailio/db_redis/kamailio")
+modparam("db_redis", "keys", "location=entry:ruid&amp;usrdom:username,domain&amp;timer:partition,keepalive")
+modparam("db_redis", "keys", "acc=entry:callid,time_hires&amp;cid:callid")
+modparam("db_redis", "keys", "subscriber=entry:username,domain")
+modparam("usrloc", "db_url", DBURL_USRLOC)
+modparam("acc_db", "db_url", DBURL_ACC)
+modparam("auth_db", "db_url", DBURL_AUTH)
+...
+			</programlisting>
+		</example>
+	</section>
+</chapter>

+ 301 - 0
src/modules/db_redis/redis_connection.c

@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <stdlib.h>
+
+#include "db_redis_mod.h"
+#include "redis_connection.h"
+#include "redis_table.h"
+
+int db_redis_connect(km_redis_con_t *con) {
+    struct timeval tv;
+    redisReply *reply;
+    int db;
+
+    tv.tv_sec = 1;
+    tv.tv_usec = 0;
+
+    db = atoi(con->id->database);
+    reply = NULL;
+
+    // TODO: introduce require_master mod-param and check if we're indeed master
+    // TODO: on carrier, if we have db fail-over, the currently connected
+    // redis server will become slave without dropping connections?
+
+    LM_DBG("connecting to redis at %s:%d\n", con->id->host, con->id->port);
+    con->con = redisConnectWithTimeout(con->id->host, con->id->port, tv);
+
+    if (!con->con) {
+        LM_ERR("cannot open connection: %.*s\n", con->id->url.len, con->id->url.s);
+        goto err;
+    }
+    if (con->con->err) {
+        LM_ERR("cannot open connection to %.*s: %s\n", con->id->url.len, con->id->url.s,
+            con->con->errstr);
+        goto err;
+    }
+
+    if (con->id->password) {
+        reply = redisCommand(con->con, "AUTH %s", con->id->password);
+        if (!reply) {
+            LM_ERR("cannot authenticate connection %.*s: %s\n",
+                    con->id->url.len, con->id->url.s, con->con->errstr);
+            goto err;
+        }
+        if (reply->type == REDIS_REPLY_ERROR) {
+            LM_ERR("cannot authenticate connection %.*s: %s\n",
+                    con->id->url.len, con->id->url.s, reply->str);
+            goto err;
+        }
+        freeReplyObject(reply); reply = NULL;
+    }
+
+    reply = redisCommand(con->con, "PING");
+    if (!reply) {
+        LM_ERR("cannot ping server on connection %.*s: %s\n",
+                con->id->url.len, con->id->url.s, con->con->errstr);
+        goto err;
+    }
+    if (reply->type == REDIS_REPLY_ERROR) {
+        LM_ERR("cannot ping server on connection %.*s: %s\n",
+                con->id->url.len, con->id->url.s, reply->str);
+        goto err;
+    }
+    freeReplyObject(reply); reply = NULL;
+
+    reply = redisCommand(con->con, "SELECT %i", db);
+    if (!reply) {
+        LM_ERR("cannot select db on connection %.*s: %s\n",
+                con->id->url.len, con->id->url.s, con->con->errstr);
+        goto err;
+    }
+    if (reply->type == REDIS_REPLY_ERROR) {
+        LM_ERR("cannot select db on connection %.*s: %s\n",
+                con->id->url.len, con->id->url.s, reply->str);
+        goto err;
+    }
+    freeReplyObject(reply); reply = NULL;
+    LM_DBG("connection opened to %.*s\n", con->id->url.len, con->id->url.s);
+
+    return 0;
+
+err:
+    if (reply)
+        freeReplyObject(reply);
+    if (con->con) {
+        redisFree(con->con);
+        con->con = NULL;
+    }
+    return -1;
+}
+
+/*! \brief
+ * Create a new connection structure,
+ * open the redis connection and set reference count to 1
+ */
+km_redis_con_t* db_redis_new_connection(const struct db_id* id) {
+    km_redis_con_t *ptr = NULL;
+
+    if (!id) {
+        LM_ERR("invalid id parameter value\n");
+        return 0;
+    }
+
+    ptr = (km_redis_con_t*)pkg_malloc(sizeof(km_redis_con_t));
+    if (!ptr) {
+        LM_ERR("no private memory left\n");
+        return 0;
+    }
+    memset(ptr, 0, sizeof(km_redis_con_t));
+    ptr->id = (struct db_id*)id;
+
+    /*
+    LM_DBG("trying to initialize connection to '%.*s' with schema path '%.*s' and keys '%.*s'\n",
+            id->url.len, id->url.s,
+            redis_schema_path.len, redis_schema_path.s,
+            redis_keys.len, redis_keys.s);
+    */
+    LM_DBG("trying to initialize connection to '%.*s'\n",
+            id->url.len, id->url.s);
+    if (db_redis_parse_schema(ptr) != 0) {
+        LM_ERR("failed to parse 'schema' module parameter\n");
+        goto err;
+    }
+    if (db_redis_parse_keys(ptr) != 0) {
+        LM_ERR("failed to parse 'keys' module parameter\n");
+        goto err;
+    }
+
+    db_redis_print_all_tables(ptr);
+
+    ptr->ref = 1;
+    ptr->append_counter = 0;
+
+    if (db_redis_connect(ptr) != 0) {
+        LM_ERR("Failed to connect to redis db\n");
+        goto err;
+    }
+
+    LM_DBG("connection opened to %.*s\n", id->url.len, id->url.s);
+
+    return ptr;
+
+ err:
+    if (ptr) {
+        if (ptr->con) {
+            redisFree(ptr->con);
+        }
+        pkg_free(ptr);
+    }
+    return 0;
+}
+
+
+/*! \brief
+ * Close the connection and release memory
+ */
+void db_redis_free_connection(struct pool_con* con) {
+    km_redis_con_t * _c;
+
+    LM_DBG("freeing db_redis connection\n");
+
+    if (!con) return;
+
+    _c = (km_redis_con_t*) con;
+
+    if (_c->id) free_db_id(_c->id);
+    if (_c->con) {
+        redisFree(_c->con);
+    }
+
+    db_redis_free_tables(_c);
+    pkg_free(_c);
+}
+
+
+static void print_query(redis_key_t *query) {
+    LM_DBG("Query dump:\n");
+    for (redis_key_t *k = query; k; k = k->next) {
+        LM_DBG("  %s\n", k->key.s);
+    }
+}
+
+void *db_redis_command_argv(km_redis_con_t *con, redis_key_t *query) {
+    char **argv = NULL;
+    int argc;
+
+    print_query(query);
+
+    argc = db_redis_key_list2arr(query, &argv);
+    if (argc < 0) {
+        LM_ERR("Failed to allocate memory for query array\n");
+        return NULL;
+    }
+    LM_DBG("query has %d args\n", argc);
+
+    redisReply *reply = redisCommandArgv(con->con, argc, (const char**)argv, NULL);
+    if (con->con->err == REDIS_ERR_EOF) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to redis db\n");
+            pkg_free(argv);
+            if (con->con) {
+                redisFree(con->con);
+                con->con = NULL;
+            }
+            return NULL;
+        }
+        reply = redisCommandArgv(con->con, argc, (const char**)argv, NULL);
+    }
+    pkg_free(argv);
+    return reply;
+}
+
+int db_redis_append_command_argv(km_redis_con_t *con, redis_key_t *query) {
+    char **argv = NULL;
+    int ret, argc;
+
+    print_query(query);
+
+    argc = db_redis_key_list2arr(query, &argv);
+    if (argc < 0) {
+        LM_ERR("Failed to allocate memory for query array\n");
+        return -1;
+    }
+    LM_DBG("query has %d args\n", argc);
+
+    ret = redisAppendCommandArgv(con->con, argc, (const char**)argv, NULL);
+    if (con->con->err == REDIS_ERR_EOF) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to redis db\n");
+            pkg_free(argv);
+            if (con->con) {
+                redisFree(con->con);
+                con->con = NULL;
+            }
+            return ret;
+        }
+        ret = redisAppendCommandArgv(con->con, argc, (const char**)argv, NULL);
+    }
+    pkg_free(argv);
+    if (!con->con->err) {
+        con->append_counter++;
+    }
+    return ret;
+}
+
+int db_redis_get_reply(km_redis_con_t *con, void **reply) {
+    int ret;
+
+    *reply = NULL;
+    ret = redisGetReply(con->con, reply);
+    if (con->con->err == REDIS_ERR_EOF) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to redis db\n");
+            if (con->con) {
+                redisFree(con->con);
+                con->con = NULL;
+            }
+            return ret;
+        }
+        ret = redisGetReply(con->con, reply);
+    }
+    if (!con->con->err)
+        con->append_counter--;
+    return ret;
+}
+
+void db_redis_free_reply(redisReply **reply) {
+    if (reply && *reply) {
+        freeReplyObject(*reply);
+        *reply = NULL;
+    }
+}
+
+void db_redis_consume_replies(km_redis_con_t *con) {
+    redisReply *reply = NULL;
+    while (con->append_counter > 0 && !con->con->err) {
+        LM_DBG("consuming outstanding reply %u", con->append_counter);
+        db_redis_get_reply(con, (void**)&reply);
+        if (reply) {
+            freeReplyObject(reply);
+            reply = NULL;
+        }
+    }
+}

+ 73 - 0
src/modules/db_redis/redis_connection.h

@@ -0,0 +1,73 @@
+/**
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+
+#ifndef _REDIS_CONNECTION_H_
+#define _REDIS_CONNECTION_H_
+
+#include <hiredis.h>
+
+#include "db_redis_mod.h"
+
+#define db_redis_check_reply(con, reply, err) do { \
+    if (!(reply) && !(con)->con) { \
+        LM_ERR("Failed to fetch type entry: no connection to server\n"); \
+        goto err; \
+    } \
+    if (!(reply)) { \
+        LM_ERR("Failed to fetch type entry: %s\n", \
+                (con)->con->errstr); \
+        goto err; \
+    } \
+    if ((reply)->type == REDIS_REPLY_ERROR) { \
+        LM_ERR("Failed to fetch type entry: %s\n", \
+                (reply)->str); \
+        goto err; \
+    } \
+} while(0);
+
+typedef struct km_redis_con {
+    struct db_id* id;
+    unsigned int ref;
+    struct pool_con* next;
+
+    redisContext *con;
+    unsigned int append_counter;
+    struct str_hash_table tables;
+} km_redis_con_t;
+
+
+struct redis_key;
+typedef struct redis_key redis_key_t;
+
+#define REDIS_CON(db_con)  ((km_redis_con_t*)((db_con)->tail))
+
+km_redis_con_t* db_redis_new_connection(const struct db_id* id);
+void db_redis_free_connection(struct pool_con* con);
+
+int db_redis_connect(km_redis_con_t *con);
+void *db_redis_command_argv(km_redis_con_t *con, redis_key_t *query);
+int db_redis_append_command_argv(km_redis_con_t *con, redis_key_t *query);
+int db_redis_get_reply(km_redis_con_t *con, void **reply);
+void db_redis_consume_replies(km_redis_con_t *con);
+void db_redis_free_reply(redisReply **reply);
+
+#endif /* _REDIS_CONNECTION_H_ */

+ 2128 - 0
src/modules/db_redis/redis_dbase.c

@@ -0,0 +1,2128 @@
+/*
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include <stdlib.h>
+#include <time.h>
+
+#include "db_redis_mod.h"
+#include "redis_connection.h"
+#include "redis_dbase.h"
+#include "redis_table.h"
+
+static void db_redis_dump_reply(redisReply *reply) {
+    if (reply->type == REDIS_REPLY_STRING) {
+        LM_DBG("%s\n", reply->str);
+    } else if (reply->type == REDIS_REPLY_INTEGER) {
+        LM_DBG("%lld\n", reply->integer);
+    } else if (reply->type == REDIS_REPLY_NIL) {
+        LM_DBG("<null>\n");
+    } else if (reply->type == REDIS_REPLY_ARRAY) {
+        LM_DBG("printing %lu elements in array reply\n", reply->elements);
+        for(int i = 0; i < reply->elements; ++i) {
+            db_redis_dump_reply(reply->element[i]);
+        }
+    } else {
+        LM_DBG("not printing invalid reply type\n");
+    }
+}
+
+// TODO: utilize auto-expiry? on insert/update, also update expire value
+// of mappings
+
+/*
+ * Initialize database module
+ * No function should be called before this
+ */
+db1_con_t* db_redis_init(const str* _url) {
+    return db_do_init(_url, (void *)db_redis_new_connection);
+}
+
+/*
+ * Shut down database module
+ * No function should be called after this
+ */
+void db_redis_close(db1_con_t* _h) {
+    LM_DBG("closing redis db connection\n");
+    db_do_close(_h, db_redis_free_connection);
+}
+
+
+static db1_res_t* db_redis_new_result(void) {
+    db1_res_t* obj;
+
+    obj = db_new_result();
+    if (!obj)
+        return NULL;
+    return obj;
+}
+
+static int db_redis_val2str(const db_val_t *v, str *_str) {
+    const char *s;
+    const str *tmpstr;
+    int vtype = VAL_TYPE(v);
+    _str->s = NULL;
+    _str->len = 32; // default for numbers
+
+    if (VAL_NULL(v)) {
+        LM_DBG("converting <null> value to str\n");
+        _str->len = 0;
+        return 0;
+    }
+
+    switch (vtype) {
+        case DB1_INT:
+            LM_DBG("converting int value %d to str\n", VAL_INT(v));
+            _str->s = (char*)pkg_malloc(_str->len);
+            if (!_str->s) goto memerr;
+            snprintf(_str->s, _str->len, "%d", VAL_INT(v));
+            _str->len = strlen(_str->s);
+            break;
+        case DB1_BIGINT:
+            LM_DBG("converting bigint value %lld to str\n", VAL_BIGINT(v));
+            _str->s = (char*)pkg_malloc(_str->len);
+            if (!_str->s) goto memerr;
+            snprintf(_str->s, _str->len, "%lld", VAL_BIGINT(v));
+            _str->len = strlen(_str->s);
+            break;
+        case DB1_STRING:
+            s = VAL_STRING(v);
+            _str->len = strlen(s);
+            LM_DBG("converting string value '%s' with len %d to str\n", s, _str->len);
+            _str->s = (char*)pkg_malloc(_str->len + 1);
+            if (!_str->s) goto memerr;
+            //memcpy(_str->s, s, _str->len);
+            //_str->s[_str->len] = '\0';
+            memset(_str->s, 0, _str->len + 1);
+            strncpy(_str->s, s, _str->len);
+            break;
+        case DB1_STR:
+            tmpstr = &(VAL_STR(v));
+            LM_DBG("converting str value '%.*s' with len %d to str\n", tmpstr->len, tmpstr->s, tmpstr->len);
+            // copy manually to add 0 termination
+            _str->s = (char*)pkg_malloc(tmpstr->len + 1);
+            if (!_str->s) goto memerr;
+            _str->len = tmpstr->len;
+            memcpy(_str->s, tmpstr->s, _str->len);
+            _str->s[_str->len] = '\0';
+            break;
+        case DB1_DATETIME:
+            LM_DBG("converting datetime value %ld to str\n", VAL_TIME(v));
+            _str->s = (char*)pkg_malloc(_str->len);
+            if (!_str->s) goto memerr;
+            strftime(_str->s, _str->len, "%Y-%m-%d %H:%M:%S", localtime(&(VAL_TIME(v))));
+            _str->len = strlen(_str->s);
+            break;
+        case DB1_DOUBLE:
+            LM_DBG("converting double value %f to str\n", VAL_DOUBLE(v));
+            _str->s = (char*)pkg_malloc(_str->len);
+            if (!_str->s) goto memerr;
+            snprintf(_str->s, _str->len, "%.6f", VAL_DOUBLE(v));
+            _str->len = strlen(_str->s);
+            break;
+        case DB1_BITMAP:
+            LM_DBG("converting bitmap value %u to str\n", VAL_BITMAP(v));
+            _str->s = (char*)pkg_malloc(_str->len);
+            if (!_str->s) goto memerr;
+            snprintf(_str->s, _str->len, "%u", VAL_BITMAP(v));
+            _str->len = strlen(_str->s);
+            break;
+        case DB1_BLOB:
+        default:
+            LM_ERR("Unsupported val type %d\n", vtype);
+            goto err;
+    }
+
+    return 0;
+
+memerr:
+    LM_ERR("Failed to allocate memory to convert value to string\n");
+err:
+    return -1;
+}
+
+static int db_redis_return_version(const db1_con_t* _h, km_redis_con_t *con, const str *table_name,
+    db1_res_t **_r) {
+
+    struct str_hash_entry *table_e;
+    redis_table_t *table;
+    db_val_t* dval;
+    db_row_t* drow;
+
+    LM_DBG("get table version\n");
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("query to undefined table '%.*s', define it in schema file!\n",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+
+    *_r = db_redis_new_result();
+    if (!*_r) {
+        LM_ERR("Failed to allocate memory for result");
+        return -1;
+    }
+    RES_NUM_ROWS(*_r) = 1;
+    RES_COL_N(*_r) = 1;
+    RES_ROW_N(*_r) = 1;
+    if (db_allocate_rows(*_r) != 0) {
+        LM_ERR("Failed to allocate memory for rows\n");
+        goto err;
+    }
+    if (db_allocate_columns(*_r, 1) != 0) {
+        LM_ERR("Failed to allocate memory for result columns");
+        goto err;
+    }
+
+    drow = &(RES_ROWS(*_r)[0]);
+
+    if (db_allocate_row(*_r, drow) != 0) {
+        LM_ERR("Failed to allocate row %d\n", RES_NUM_ROWS(*_r));
+        goto err;
+    }
+
+    dval = &(ROW_VALUES(drow)[0]);
+
+    VAL_TYPE(dval) = DB1_INT;
+    VAL_NULL(dval) = 0;
+    VAL_INT(dval) = table->version;
+
+    LM_DBG("returning short-cut table version %d for table '%.*s'",
+        table->version, table_name->len, table_name->s);
+
+    return 0;
+
+err:
+    if (*_r) db_redis_free_result((db1_con_t*)_h, *_r);
+    return -1;
+}
+
+static int db_redis_build_entry_manual_keys(redis_table_t *table, const db_key_t *_k, const db_val_t *_v, const int _n, int **manual_keys, int *manual_key_count) {
+
+    // TODO: we also put keys here which are already part of type mapping!
+    // there must be removed for performance reasons
+
+    redis_key_t *key = NULL;
+
+    *manual_keys = (int*)pkg_malloc(_n * sizeof(int));
+    if (! *manual_keys) {
+        LM_ERR("Failed to allocate memory for manual key indices\n");
+        goto err;
+    }
+    memset(*manual_keys, 0, _n * sizeof(int));
+    *manual_key_count = 0;
+
+    for (key = table->entry_keys; key; key = key->next) {
+        int subkey_found = 0;
+        LM_DBG("checking for existence of entry key '%.*s' in query to get manual key\n",
+                key->key.len, key->key.s);
+        for (int i = 0; i < _n; ++i) {
+            const db_key_t k = _k[i];
+            if (!str_strcmp(&key->key, (str*)k)) {
+                LM_DBG("found key in entry key\n");
+                subkey_found = 1;
+                break;
+            } else {
+                (*manual_keys)[*manual_key_count] = i;
+                (*manual_key_count)++;
+            }
+        }
+        if (!subkey_found) {
+            break;
+        }
+    }
+    return 0;
+
+err:
+    if (*manual_keys) {
+        pkg_free(*manual_keys);
+        *manual_keys = NULL;
+    }
+    return -1;
+}
+
+static int db_redis_find_query_key(redis_key_t *key, const str *table_name, str *type_name, const db_key_t *_k, const db_val_t *_v, const int _n, str *key_name, int *key_found) {
+
+    unsigned int len;
+    str val = {NULL, 0};
+
+    *key_found = 1;
+    key_name->len = 0;
+    key_name->s = NULL;
+
+    for (; key; key = key->next) {
+        int subkey_found = 0;
+        LM_DBG("checking for existence of entry key '%.*s' in query\n",
+                key->key.len, key->key.s);
+        for (int i = 0; i < _n; ++i) {
+            const db_key_t k = _k[i];
+            const db_val_t v = _v[i];
+
+            if (VAL_NULL(&v)) {
+                LM_DBG("Skipping null value for given key '%.*s'\n",
+                        k->len, k->s);
+                break;
+            } else if (!str_strcmp(&key->key, (str*)k)) {
+                LM_DBG("found key in entry key\n");
+                if (db_redis_val2str(&v, &val) != 0) goto err;
+                if (val.s == NULL) {
+                    LM_DBG("key value in entry key is null, skip key\n");
+                    subkey_found = 0;
+                    break;
+                }
+                if (!key_name->len) {
+                    // <table_name>:<type>::<val>
+                    len = table_name->len + 1 + type_name->len + 2 + val.len + 1; //snprintf writes term 0 char
+                    key_name->s = (char*)pkg_malloc(len);
+                    if (!key_name->s) {
+                        LM_ERR("Failed to allocate key memory\n");
+                        goto err;
+                    }
+                    snprintf(key_name->s, len, "%.*s:%.*s::%.*s",
+                            table_name->len, table_name->s,
+                            type_name->len, type_name->s,
+                            val.len, val.s);
+                    key_name->len = len-1; // subtract the term 0 char
+
+                } else {
+                    // :<val>
+                    key_name->s = (char*)pkg_realloc(key_name->s, key_name->len + val.len + 2);
+                    if (!key_name->s) {
+                        LM_ERR("Failed to allocate key memory\n");
+                        goto err;
+                    }
+                    snprintf(key_name->s + key_name->len, 1 + val.len + 1, ":%.*s",
+                            val.len, val.s);
+                    key_name->len += (1 + val.len);
+                }
+                LM_DBG("entry key so far is '%.*s'\n", key_name->len, key_name->s);
+                subkey_found = 1;
+                pkg_free(val.s);
+                val.s = NULL;
+                break;
+            }
+        }
+        if (!subkey_found) {
+            LM_DBG("key '%.*s' for type '%.*s' not found, unable to use this type\n",
+                    key->key.len, key->key.s, type_name->len, type_name->s);
+            if (key_name->s) {
+                pkg_free(key_name->s);
+                key_name->s = NULL;
+                key_name->len = 0;
+            }
+            *key_found = 0;
+            break;
+        }
+    }
+
+    return 0;
+
+err:
+    if (val.s)
+        pkg_free(val.s);
+    if(key_name->s) {
+        pkg_free(key_name->s);
+        key_name->s = NULL;
+        key_name->len = 0;
+    }
+    return -1;
+}
+
+static int db_redis_build_entry_keys(km_redis_con_t *con, const str *table_name,
+        const db_key_t *_k, const db_val_t *_v, const int _n,
+        redis_key_t **keys, int *keys_count) {
+
+    struct str_hash_entry *table_e;
+    redis_table_t *table;
+    redis_key_t *key;
+    int key_found;
+    str type_name = str_init("entry");
+    str keyname = {NULL, 0};
+
+    LM_DBG("build entry keys\n");
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("query to undefined table '%.*s', define in db_redis keys parameter!",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+    key = table->entry_keys;
+    if (db_redis_find_query_key(key, table_name, &type_name, _k, _v, _n, &keyname, &key_found) != 0) {
+        goto err;
+    }
+    if (key_found) {
+        db_redis_key_add_str(keys, &keyname);
+
+        if (db_redis_key_add_str(keys, &keyname) != 0) {
+            LM_ERR("Failed to add key string\n");
+            goto err;
+        }
+        LM_DBG("found suitable entry key '%.*s' for query\n",
+                (*keys)->key.len, (*keys)->key.s);
+        *keys_count = 1;
+        pkg_free(keyname.s);
+    } else {
+        LM_ERR("Failed to create direct entry key, no matching key definition\n");
+        goto err;
+    }
+
+    return 0;
+
+err:
+    db_redis_key_free(keys);
+    if (keyname.s)
+        pkg_free(keyname.s);
+    return -1;
+}
+
+static int db_redis_get_keys_for_all_types(km_redis_con_t *con, const str *table_name,
+        redis_key_t **keys, int *keys_count) {
+
+    struct str_hash_entry *table_e;
+    redis_table_t *table;
+    redis_type_t *type;
+    redis_key_t *key;
+
+    *keys = NULL;
+    *keys_count = 0;
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("query to undefined table '%.*s', define in db_redis keys parameter!",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+
+    for (type = table->types; type; type = type->next) {
+        for (key = type->keys; key; key = key->next) {
+            if (db_redis_key_add_str(keys, &key->key) != 0) {
+                LM_ERR("Failed to add key string\n");
+                goto err;
+            }
+            (*keys_count)++;
+        }
+    }
+
+    return 0;
+
+err:
+    db_redis_key_free(keys);
+    return -1;
+}
+
+static int db_redis_build_type_keys(km_redis_con_t *con, const str *table_name,
+        const db_key_t *_k, const db_val_t *_v, const int _n,
+        redis_key_t **keys, int *keys_count) {
+
+    struct str_hash_entry *table_e;
+    redis_table_t *table;
+    redis_type_t *type;
+    redis_key_t *key;
+
+    *keys = NULL;
+    *keys_count = 0;
+
+    LM_DBG("build type keys\n");
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("query to undefined table '%.*s', define in db_redis keys parameter!",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+
+    for (type = table->types; type; type = type->next) {
+        str *type_name = &(type->type);
+        int key_found = 0;
+        str keyname = {NULL, 0};
+        key = type->keys;
+
+        if (db_redis_find_query_key(key, table_name, &type->type, _k, _v, _n, &keyname, &key_found) != 0) {
+            goto err;
+        }
+        if (key_found) {
+            db_redis_key_add_str(keys, &keyname);
+            (*keys_count)++;
+            LM_DBG("found key '%.*s' for type '%.*s'\n",
+                    keyname.len, keyname.s,
+                    type_name->len, type_name->s);
+            pkg_free(keyname.s);
+        }
+    }
+
+    return 0;
+
+err:
+    LM_ERR("Failed to get type key\n");
+    db_redis_key_free(keys);
+    return -1;
+}
+
+static int db_redis_build_query_keys(km_redis_con_t *con, const str *table_name,
+        const db_key_t *_k, const db_val_t *_v, const db_op_t *_op, const int _n,
+        redis_key_t **query_keys, int *query_keys_count, int **manual_keys, int *manual_keys_count,
+        int *do_table_scan) {
+
+    struct str_hash_entry *table_e;
+    redis_table_t *table;
+    redis_type_t *type;
+    redis_key_t *key;
+    str keyname;
+    int key_found;
+    redisReply *reply = NULL;
+    str typename = str_init(REDIS_DIRECT_PREFIX);
+
+    *query_keys = NULL;
+    *query_keys_count = 0;
+    *do_table_scan = 1;
+
+    LM_DBG("build query keys\n");
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("query to undefined table '%.*s', define in db_redis keys parameter!",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+
+    // check if given keys directly match entry key
+    keyname.s = NULL;
+    keyname.len = 0;
+    key = table->entry_keys;
+
+    if (db_redis_find_query_key(key, table_name, &typename, _k, _v, _n, &keyname, &key_found) != 0) {
+        goto err;
+    }
+    if (key_found) {
+        LM_DBG("found suitable entry key '%.*s' for query\n",
+                keyname.len, keyname.s);
+        db_redis_key_add_str(query_keys, &keyname);
+        *query_keys_count = 1;
+        pkg_free(keyname.s);
+        keyname.s = NULL;
+    } else {
+        LM_DBG("no direct entry key found, checking type keys\n");
+        for (type = table->types; type; type = type->next) {
+            key = type->keys;
+            LM_DBG("checking type '%.*s'\n", type->type.len, type->type.s);
+            if (db_redis_find_query_key(key, table_name, &type->type, _k, _v, _n, &keyname, &key_found) != 0) {
+                goto err;
+            }
+            if (key_found) {
+                redis_key_t *query_v = NULL;
+                char *prefix = "SMEMBERS";
+
+                if (db_redis_key_add_string(&query_v, prefix, strlen(prefix)) != 0) {
+                    LM_ERR("Failed to add smembers command to query\n");
+                    goto err;
+                }
+                if (db_redis_key_add_str(&query_v, &keyname) != 0) {
+                    LM_ERR("Failed to add key name to smembers query\n");
+                    goto err;
+                }
+
+                reply = db_redis_command_argv(con, query_v);
+                pkg_free(keyname.s);
+                keyname.s = NULL;
+                db_redis_key_free(&query_v);
+                db_redis_check_reply(con, reply, err);
+                if (reply->type == REDIS_REPLY_ARRAY) {
+                    if (reply->elements == 0) {
+                        LM_DBG("type query returned empty list\n");
+                        *query_keys_count = 0;
+                        *do_table_scan = 0;
+                        db_redis_free_reply(&reply);
+                        break;
+                    } else {
+                        LM_DBG("populating query keys list with result of type query\n");
+                        *query_keys_count = reply->elements;
+                        for (int i = 0; i < reply->elements; ++i) {
+                            redisReply *subreply = reply->element[i];
+                            if (subreply->type == REDIS_REPLY_STRING) {
+                                LM_DBG("adding resulting entry key '%s' from type query\n", subreply->str);
+                                if (db_redis_key_add_string(query_keys, subreply->str, strlen(subreply->str)) != 0) {
+                                    LM_ERR("Failed to add query key\n");
+                                    goto err;
+                                }
+                            } else {
+                                LM_ERR("Unexpected entry key type in type query, expecting a string\n");
+                                goto err;
+                            }
+                        }
+                    }
+                } else {
+                    LM_ERR("Unexpected reply for type query, expecting an array\n");
+                    goto err;
+                }
+
+                db_redis_free_reply(&reply);
+                break;
+            }
+        }
+    }
+
+    if (*query_keys_count > 0) {
+        LM_DBG("building manual keys\n");
+        if (db_redis_build_entry_manual_keys(table, _k, _v, _n, manual_keys, manual_keys_count) != 0) {
+            LM_ERR("Failed to build manual entry key list\n");
+            goto err;
+
+        }
+    }
+
+    return 0;
+err:
+    if (keyname.s) {
+        pkg_free(keyname.s);
+        keyname.s = NULL;
+    }
+    if (reply) {
+        db_redis_free_reply(&reply);
+    }
+    db_redis_key_free(query_keys);
+    if (*manual_keys) {
+        pkg_free(*manual_keys);
+        *manual_keys = NULL;
+    }
+    return -1;
+}
+
+static int db_redis_scan_query_keys(km_redis_con_t *con, const str *table_name,
+        const db_key_t *_k, const int _n,
+        redis_key_t **query_keys, int *query_keys_count,
+        int **manual_keys, int *manual_keys_count) {
+
+    size_t i = 0;
+    redis_key_t *query_v = NULL;
+    char cursor_str[32] = "";
+    redisReply *reply = NULL;
+    unsigned long cursor = 0;
+    char *match = NULL;
+
+    str match_pattern = {":entry::*", strlen(":entry::*")};
+
+    *query_keys = NULL;
+    *query_keys_count = 0;
+    *manual_keys = NULL;
+    *manual_keys_count = 0;
+
+    do {
+        snprintf(cursor_str, sizeof(cursor_str), "%lu", cursor);
+        match = (char*)pkg_malloc(table_name->len + match_pattern.len + 1);
+        if (!match) {
+            LM_ERR("Failed to allocate memory for match pattern\n");
+            goto err;
+        }
+        snprintf(match, table_name->len + match_pattern.len + 1, "%s%s\n",
+                table_name->s, match_pattern.s);
+
+        if (db_redis_key_add_string(&query_v, "SCAN", 4) != 0) {
+            LM_ERR("Failed to add scan command to scan query\n");
+            goto err;
+        }
+        if (db_redis_key_add_string(&query_v, cursor_str, strlen(cursor_str)) != 0) {
+            LM_ERR("Failed to add cursor to scan query\n");
+            goto err;
+        }
+        if (db_redis_key_add_string(&query_v, "MATCH", 5) != 0) {
+            LM_ERR("Failed to add match command to scan query\n");
+            goto err;
+        }
+        if (db_redis_key_add_string(&query_v, match, strlen(match)) != 0) {
+            LM_ERR("Failed to add match pattern to scan query\n");
+            goto err;
+        }
+        pkg_free(match); match = NULL;
+
+        reply = db_redis_command_argv(con, query_v);
+        db_redis_key_free(&query_v);
+        db_redis_check_reply(con, reply, err);
+        if (reply->type != REDIS_REPLY_ARRAY) {
+            LM_ERR("Invalid reply type for scan on table '%.*s', expected array\n",
+                    table_name->len, table_name->s);
+            goto err;
+        }
+        if (reply->elements != 2) {
+            LM_ERR("Invalid number of reply elements for scan on table '%.*s', expected 2, got %lu\n",
+                    table_name->len, table_name->s, reply->elements);
+            goto err;
+        }
+
+        if (reply->element[0]->type == REDIS_REPLY_STRING) {
+            cursor = atol(reply->element[0]->str);
+        } else if (reply->element[0]->type == REDIS_REPLY_INTEGER) {
+            // should not happen, but play it safe
+            cursor = reply->element[0]->integer;
+        } else {
+            LM_ERR("Invalid cursor type for scan on table '%.*s', expected string or integer\n",
+                    table_name->len, table_name->s);
+            goto err;
+        }
+
+        if (reply->element[1]->type != REDIS_REPLY_ARRAY) {
+            LM_ERR("Invalid content type for scan on table '%.*s', expected array\n",
+                    table_name->len, table_name->s);
+            goto err;
+        }
+        if (reply->element[1]->elements == 0) {
+            LM_DBG("no matching entries found for scan on table '%.*s'\n",
+                    table_name->len, table_name->s);
+            return 0;
+        }
+
+        *query_keys_count += reply->element[1]->elements;
+
+        for (size_t j = 0; j < reply->element[1]->elements; ++i, ++j) {
+            redisReply *key = reply->element[1]->element[j];
+            if (!key) {
+                LM_ERR("Invalid null key at cursor result index %lu while scanning table '%.*s'\n",
+                        j, table_name->len, table_name->s);
+                goto err;
+            }
+            if (key->type != REDIS_REPLY_STRING) {
+                LM_ERR("Invalid key type at cursor result index %lu while scanning table '%.*s', expected string\n",
+                        j, table_name->len, table_name->s);
+                goto err;
+            }
+            if (db_redis_key_add_string(query_keys, key->str, strlen(key->str)) != 0) {
+                LM_ERR("Failed to add redis key\n");
+                goto err;
+            }
+        }
+        db_redis_free_reply(&reply);
+    } while (cursor > 0);
+
+    // for full table scans, we have to manually match all given keys
+    *manual_keys_count = _n;
+    *manual_keys = (int*)pkg_malloc(*manual_keys_count * sizeof(int));
+    if (! *manual_keys) {
+        LM_ERR("Failed to allocate memory for manual keys\n");
+        goto err;
+    }
+    memset(*manual_keys, 0, *manual_keys_count * sizeof(int));
+    for (int i = 0; i < _n; ++i) {
+        (*manual_keys)[i] = i;
+    }
+
+    if (reply) {
+        db_redis_free_reply(&reply);
+    }
+    return 0;
+
+err:
+    if (match)
+        pkg_free(match);
+    if (reply)
+        db_redis_free_reply(&reply);
+    db_redis_key_free(&query_v);
+    db_redis_key_free(query_keys);
+    *query_keys_count = 0;
+    if (*manual_keys) {
+        pkg_free(*manual_keys);
+        *manual_keys = NULL;
+    }
+    return -1;
+}
+
+static int db_redis_compare_column(db_key_t k, db_val_t *v, db_op_t op, redisReply *reply) {
+    int i_value;
+    long long ll_value;
+    double d_value;
+    str *tmpstr;
+    char tmp[32] = "";
+
+    int vtype = VAL_TYPE(v);
+
+    if (VAL_NULL(v) && reply->type == REDIS_REPLY_NIL) {
+        LM_DBG("comparing matching NULL values\n");
+        return 0;
+    } else if (VAL_NULL(v) || reply->type == REDIS_REPLY_NIL) {
+        LM_DBG("comparing non-matching NULL values\n");
+        return -1;
+    }
+
+    switch (vtype) {
+        case DB1_INT:
+            i_value = atoi(reply->str);
+            LM_DBG("comparing INT %d %s %d\n", i_value, op, VAL_INT(v));
+            if (!strcmp(op, OP_EQ)) {
+                if (i_value == VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LT)) {
+                if (i_value < VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GT)) {
+                if (i_value > VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LEQ)) {
+                if (i_value <= VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GEQ)) {
+                if (i_value >= VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_NEQ)) {
+                if (i_value != VAL_INT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_BITWISE_AND)) {
+                if (i_value & VAL_INT(v))
+                    return 0;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_BIGINT:
+            ll_value = atoll(reply->str);
+            LM_DBG("comparing BIGINT %lld %s %lld\n", ll_value, op, VAL_BIGINT(v));
+            if (!strcmp(op, OP_EQ)) {
+                if (ll_value == VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LT)) {
+                if (ll_value < VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GT)) {
+                if (ll_value > VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LEQ)) {
+                if (ll_value <= VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GEQ)) {
+                if (ll_value >= VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_NEQ)) {
+                if (ll_value != VAL_BIGINT(v))
+                    return 0;
+            } else if (!strcmp(op, OP_BITWISE_AND)) {
+                if (ll_value & VAL_BIGINT(v))
+                    return 0;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_STRING:
+            LM_DBG("comparing STRING %s %s %s\n", reply->str, op, VAL_STRING(v));
+            if (!strcmp(op, OP_EQ)) {
+                return (strcmp(reply->str, VAL_STRING(v)) == 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LT)) {
+                return (strcmp(reply->str, VAL_STRING(v)) < 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GT)) {
+                return (strcmp(reply->str, VAL_STRING(v)) > 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LEQ)) {
+                return (strcmp(reply->str, VAL_STRING(v)) <= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GEQ)) {
+                return (strcmp(reply->str, VAL_STRING(v)) >= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_NEQ)) {
+                return (strcmp(reply->str, VAL_STRING(v)) != 0) ? 0 : -1;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_STR:
+            tmpstr = (struct _str*) &(VAL_STR(v));
+            LM_DBG("comparing STR %s %s %.*s\n", reply->str, op, tmpstr->len, tmpstr->s);
+            if (!strcmp(op, OP_EQ)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) == 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LT)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) < 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GT)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) > 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LEQ)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) <= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GEQ)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) >= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_NEQ)) {
+                return (strncmp(reply->str, tmpstr->s, tmpstr->len) != 0) ? 0 : -1;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_DOUBLE:
+            d_value = atof(reply->str);
+            LM_DBG("comparing DOUBLE %f %s %f\n", d_value, op, VAL_DOUBLE(v));
+            if (!strcmp(op, OP_EQ)) {
+                return (d_value == VAL_DOUBLE(v)) ? 0 : -1;
+            } else if (!strcmp(op, OP_LT)) {
+                return (d_value < VAL_DOUBLE(v)) ? 0 : -1;
+            } else if (!strcmp(op, OP_GT)) {
+                return (d_value > VAL_DOUBLE(v)) ? 0 : -1;
+            } else if (!strcmp(op, OP_LEQ)) {
+                return (d_value <= VAL_DOUBLE(v)) ? 0 : -1;
+            } else if (!strcmp(op, OP_GEQ)) {
+                return (d_value >= VAL_DOUBLE(v)) ? 0 : -1;
+            } else if (!strcmp(op, OP_NEQ)) {
+                return (d_value != VAL_DOUBLE(v)) ? 0 : -1;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_DATETIME:
+            // TODO: insert int value to db for faster comparison!
+            strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", localtime(&(VAL_TIME(v))));
+            LM_DBG("comparing DATETIME %s %s %s\n", reply->str, op, tmp);
+            if (!strcmp(op, OP_EQ)) {
+                return (strcmp(reply->str, tmp) == 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LT)) {
+                return (strcmp(reply->str, tmp) < 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GT)) {
+                return (strcmp(reply->str, tmp) > 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_LEQ)) {
+                return (strcmp(reply->str, tmp) <= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_GEQ)) {
+                return (strcmp(reply->str, tmp) >= 0) ? 0 : -1;
+            } else if (!strcmp(op, OP_NEQ)) {
+                return (strcmp(reply->str, tmp) != 0) ? 0 : -1;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_BITMAP:
+            i_value = atoi(reply->str);
+            LM_DBG("comparing BITMAP %d %s %d\n", i_value, op, VAL_BITMAP(v));
+            if (!strcmp(op, OP_EQ)) {
+                if (i_value == VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LT)) {
+                if (i_value < VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GT)) {
+                if (i_value > VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_LEQ)) {
+                if (i_value <= VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_GEQ)) {
+                if (i_value >= VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_NEQ)) {
+                if (i_value != VAL_BITMAP(v))
+                    return 0;
+            } else if (!strcmp(op, OP_BITWISE_AND)) {
+                if (i_value & VAL_BITMAP(v))
+                    return 0;
+            } else {
+                LM_ERR("Unsupported op type '%s'\n", op);
+                return -1;
+            }
+            return -1;
+        case DB1_BLOB:
+        default:
+            LM_ERR("Unsupported val type %d\n", vtype);
+            return -1;
+    }
+}
+
+static int db_redis_convert_row(km_redis_con_t *con, db1_res_t* _r, const db_key_t *_k,
+        const db_val_t *_v, const db_op_t *_op,
+        redisReply *reply, const str *table_name, const db_key_t* _c, int _nc,
+        int *manual_keys, int manual_keys_count) {
+    db_val_t* dval;
+    db_row_t* drow;
+
+    if (reply->type != REDIS_REPLY_ARRAY) {
+        LM_ERR("Unexpected redis reply type, expecting array\n");
+        return -1;
+    }
+
+    if (!reply->elements) {
+        LM_DBG("skip empty row");
+        return 0;
+    }
+
+    // manually filter non-matching replies
+    for (size_t col = 0; col < reply->elements; ++col) {
+        if (col < manual_keys_count) {
+            int idx = manual_keys[col];
+            db_key_t k = _k[idx];
+            db_val_t v = _v[idx];
+            db_op_t o = _op[idx];
+            LM_DBG("manually filtering key '%.*s'\n",
+                    k->len, k->s);
+            if (db_redis_compare_column(k, &v, o, reply->element[col]) != 0) {
+                LM_DBG("column %lu does not match, ignore row\n", col);
+                return 0;
+            }
+        }
+    }
+
+    RES_NUM_ROWS(_r) = RES_ROW_N(_r) = RES_NUM_ROWS(_r) + 1;
+    drow = &(RES_ROWS(_r)[RES_NUM_ROWS(_r)-1]);
+
+    if (db_allocate_row(_r, drow) != 0) {
+        LM_ERR("Failed to allocate row %d\n", RES_NUM_ROWS(_r));
+        return -1;
+    }
+
+    if (reply->elements - manual_keys_count > RES_COL_N(_r)) {
+        LM_ERR("Invalid number of columns at row %d/%d, expecting %d, got %lu\n",
+                RES_NUM_ROWS(_r), RES_ROW_N(_r), RES_COL_N(_r), reply->elements - manual_keys_count);
+        return -1;
+    }
+    for (size_t col = manual_keys_count; col < reply->elements; ++col) {
+        size_t colidx = col - manual_keys_count;
+        size_t redisidx = col;
+        int coltype;
+        redisReply *col_val = reply->element[redisidx];
+        str *col_name = _c[colidx];
+
+        LM_DBG("converting column #%lu of row #%d", colidx, RES_ROW_N(_r));
+
+        if (col_val->type != REDIS_REPLY_STRING &&
+            col_val->type != REDIS_REPLY_NIL) {
+
+            LM_ERR("Invalid column value type in column '%.*s' of row %d, expecting string or null\n",
+                    col_name->len, col_name->s, RES_NUM_ROWS(_r));
+            return -1;
+        }
+
+        if (RES_NUM_ROWS(_r) == 1) {
+            coltype = db_redis_schema_get_column_type(con, table_name, col_name);
+            RES_TYPES(_r)[colidx] = coltype;
+        } else {
+            coltype = RES_TYPES(_r)[colidx];
+        }
+
+        dval = &(ROW_VALUES(drow)[colidx]);
+        VAL_TYPE(dval) = coltype;
+
+        if (col_val->type == REDIS_REPLY_NIL) {
+            VAL_NULL(dval) = 1;
+        } else {
+            if (db_str2val(coltype, dval, col_val->str, strlen(col_val->str), 1) != 0) {
+                LM_ERR("Failed to convert redis column '%.*s' to db value\n",
+                        col_name->len, col_name->s);
+                return -1;
+            }
+        }
+    }
+
+    return 0;
+}
+
+static int db_redis_perform_query(const db1_con_t* _h, km_redis_con_t *con, const db_key_t* _k,
+        const db_val_t* _v, const db_op_t *_op, const db_key_t* _c,
+        const int _n, const int _nc, db1_res_t** _r,
+        redis_key_t **keys, int *keys_count,
+        int **manual_keys, int *manual_keys_count, int do_table_scan) {
+
+    redisReply *reply = NULL;
+    redis_key_t *query_v = NULL;
+    int num_rows = 0;
+
+    *_r = db_redis_new_result();
+    if (!*_r) {
+        LM_ERR("Failed to allocate memory for result");
+        goto error;
+    }
+
+    if (db_allocate_columns(*_r, _nc) != 0) {
+        LM_ERR("Failed to allocate memory for result columns");
+        goto error;
+    }
+    RES_NUM_ROWS(*_r) = RES_ROW_N(*_r) = 0;
+    RES_COL_N(*_r) = _nc;
+
+    if (!keys_count && do_table_scan) {
+        LM_DBG("performing full table scan\n");
+        if (db_redis_scan_query_keys(con, CON_TABLE(_h), _k, _n,
+                    keys, keys_count,
+                    manual_keys, manual_keys_count) != 0) {
+            LM_ERR("failed to scan query keys\n");
+            goto error;
+        }
+    }
+
+    for (redis_key_t *key = *keys; key; key = key->next) {
+        redis_key_t *tmp = NULL;
+        str *keyname = &(key->key);
+
+        num_rows++;
+
+        LM_DBG("checking key '%s' in redis\n", keyname->s);
+
+        if (db_redis_key_add_string(&query_v, "EXISTS", 6) != 0) {
+            LM_ERR("Failed to add exists query to list\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, keyname) != 0) {
+            LM_ERR("Failed to add key name to list\n");
+            goto error;
+        }
+        if (db_redis_append_command_argv(con, query_v) != REDIS_OK) {
+            LM_ERR("Failed to append redis command\n");
+            goto error;
+        }
+        tmp = db_redis_key_unshift(&query_v);
+        if (tmp)
+            db_redis_key_free(&tmp);
+
+        // construct HMGET query
+        if (_nc + (*manual_keys_count) == 0) {
+            if (db_redis_key_prepend_string(&query_v, "HGETALL", 7) != 0) {
+                LM_ERR("Failed to add hgetall query to list\n");
+                goto error;
+            }
+        } else {
+            if (db_redis_key_prepend_string(&query_v, "HMGET", 5) != 0) {
+                LM_ERR("Failed to add hmget query to list\n");
+                goto error;
+            }
+        }
+
+        // we put the manual comparison columns first, so we can skip them
+        // easily in result, for the cost of potential duplicate column returns
+        for (int j = 0; j < *manual_keys_count; ++j) {
+            int idx = (*manual_keys)[j];
+            str *k_name = _k[idx];
+            if (db_redis_key_add_str(&query_v, k_name) != 0) {
+                LM_ERR("Failed to add manual key to query list\n");
+                goto error;
+            }
+        }
+        for (int j = 0; j < _nc; ++j) {
+            str *k_name = _c[j];
+            if (db_redis_key_add_str(&query_v, k_name) != 0) {
+                LM_ERR("Failed to add manual key to query list\n");
+                goto error;
+            }
+        }
+
+        if (db_redis_append_command_argv(con, query_v) != REDIS_OK) {
+            LM_ERR("Failed to append redis command\n");
+            goto error;
+        }
+
+        db_redis_key_free(&query_v);
+        query_v = NULL;
+    }
+
+    // we allocate best case scenario (all rows match)
+    RES_NUM_ROWS(*_r) = RES_ROW_N(*_r) = num_rows;
+    if (db_allocate_rows(*_r) != 0) {
+        LM_ERR("Failed to allocate memory for rows\n");
+        return -1;
+    }
+    RES_COL_N(*_r) = _nc;
+    // reset and increment in convert_row
+    RES_NUM_ROWS(*_r) = RES_ROW_N(*_r) = 0;
+
+    for (redis_key_t *key = *keys; key; key = key->next) {
+        // get reply for EXISTS query
+        if (db_redis_get_reply(con, (void**)&reply) != REDIS_OK) {
+            LM_ERR("Failed to get reply for query: %s\n",
+                    con->con->errstr);
+            goto error;
+        }
+        db_redis_check_reply(con, reply, error);
+        if (reply->integer == 0) {
+            LM_DBG("key does not exist, returning no row for query\n");
+            db_redis_free_reply(&reply);
+            // also free next reply, as this is a null row for the HMGET
+            db_redis_get_reply(con, (void**)&reply);
+            db_redis_check_reply(con, reply, error);
+            db_redis_free_reply(&reply);
+            continue;
+        }
+        db_redis_free_reply(&reply);
+
+        // get reply for actual HMGET query
+        if (db_redis_get_reply(con, (void**)&reply) != REDIS_OK) {
+            LM_ERR("Failed to get reply for query: %s\n",
+                    con->con->errstr);
+            goto error;
+        }
+        db_redis_check_reply(con, reply, error);
+        if (reply->type != REDIS_REPLY_ARRAY) {
+            LM_ERR("Unexpected reply, expected array\n");
+            goto error;
+        }
+        LM_DBG("dumping full query reply for row\n");
+        db_redis_dump_reply(reply);
+
+        if (db_redis_convert_row(con, *_r, _k, _v, _op, reply, CON_TABLE(_h), _c, _nc, *manual_keys, *manual_keys_count)) {
+            LM_ERR("Failed to convert redis reply for row\n");
+            goto error;
+        }
+        db_redis_free_reply(&reply);
+    }
+
+    return 0;
+
+error:
+    LM_ERR("failed to perform the query\n");
+    db_redis_key_free(&query_v);
+    if(reply)
+        db_redis_free_reply(&reply);
+    if(_r && *_r) {
+        db_redis_free_result((db1_con_t*)_h, *_r); *_r = NULL;
+    }
+    return -1;
+}
+
+static int db_redis_perform_delete(const db1_con_t* _h, km_redis_con_t *con, const db_key_t* _k,
+        const db_val_t* _v, const db_op_t *_op, const int _n,
+        redis_key_t *keys, int keys_count,
+        int *manual_keys, int manual_keys_count, int do_table_scan) {
+
+    int j = 0;
+    redis_key_t *k = NULL;
+    int type_keys_count = 0;
+    int all_type_keys_count = 0;
+
+    redisReply *reply = NULL;
+    redis_key_t *query_v = NULL;
+    redis_key_t *type_keys = NULL;
+    redis_key_t *all_type_keys = NULL;
+    db_val_t *db_vals = NULL;
+    db_key_t *db_keys = NULL;
+
+    if (!keys_count && do_table_scan) {
+        LM_DBG("performing full table scan\n");
+        if (db_redis_scan_query_keys(con, CON_TABLE(_h), _k, _n,
+                    &keys, &keys_count,
+                    &manual_keys, &manual_keys_count) != 0) {
+            LM_ERR("failed to scan query keys\n");
+            goto error;
+        }
+    }
+
+    // TODO: this should be moved to redis_connection structure
+    // and be parsed at startup:
+    //
+    // fetch list of keys in all types
+    if (db_redis_get_keys_for_all_types(con, CON_TABLE(_h),
+                &all_type_keys, &all_type_keys_count) != 0) {
+            LM_ERR("failed to get full list of type keys\n");
+            goto error;
+    }
+
+    LM_DBG("+++ delete all keys\n");
+    for (k = keys; k; k = k->next) {
+        redis_key_t *all_type_key;
+        str *key = &k->key;
+        redis_key_t *tmp = NULL;
+        int row_match;
+        LM_DBG("+++ delete key '%.*s'\n", key->len, key->s);
+
+        if (db_redis_key_add_string(&query_v, "EXISTS", 6) != 0) {
+            LM_ERR("Failed to add exists command to pre-delete query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, key) != 0) {
+            LM_ERR("Failed to add key name to pre-delete query\n");
+            goto error;
+        }
+
+        // TODO: pipeline commands!
+        reply = db_redis_command_argv(con, query_v);
+        db_redis_check_reply(con, reply, error);
+        if (reply->integer == 0) {
+            LM_DBG("key does not exist in redis, skip deleting\n");
+            db_redis_free_reply(&reply);
+            continue;
+        }
+        db_redis_free_reply(&reply);
+        tmp = db_redis_key_unshift(&query_v);
+        if (tmp)
+            db_redis_key_free(&tmp);
+
+        if (db_redis_key_prepend_string(&query_v, "HMGET", 5) != 0) {
+            LM_ERR("Failed to set hmget command to pre-delete query\n");
+            goto error;
+        }
+
+        // add all manual keys to query
+        for (j = 0; j < manual_keys_count; ++j) {
+            int idx = manual_keys[j];
+            str *col = _k[idx];
+
+            if (db_redis_key_add_str(&query_v, col) != 0) {
+                LM_ERR("Failed to add manual key to pre-delete query\n");
+                goto error;
+            }
+        }
+        // add all type keys to query
+        for (all_type_key = all_type_keys; all_type_key; all_type_key = all_type_key->next) {
+            if (db_redis_key_add_str(&query_v, &all_type_key->key) != 0) {
+                LM_ERR("Failed to add type key to pre-delete query\n");
+                goto error;
+            }
+        }
+        reply = db_redis_command_argv(con, query_v);
+        db_redis_key_free(&query_v);
+        db_redis_check_reply(con, reply, error);
+
+        LM_DBG("dumping full query reply\n");
+        db_redis_dump_reply(reply);
+
+        // manually filter non-matching replies
+        row_match = 1;
+        for (size_t col = 0; col < reply->elements; ++col) {
+            if (col < manual_keys_count) {
+                int idx = manual_keys[col];
+                db_key_t k = _k[idx];
+                db_val_t v = _v[idx];
+                db_op_t o = _op[idx];
+                LM_DBG("manually filtering key '%.*s'\n",
+                        k->len, k->s);
+                if (db_redis_compare_column(k, &v, o, reply->element[col]) != 0) {
+                    LM_DBG("column %lu does not match, ignore row\n", col);
+                    row_match = 0;
+                    break;
+                }
+            }
+        }
+        if (!row_match) {
+            db_redis_free_reply(&reply);
+            continue;
+        } else {
+            LM_DBG("row matches manual filtering, proceed with deletion\n");
+        }
+
+        db_keys = (db_key_t*) pkg_malloc(all_type_keys_count * sizeof(db_key_t));
+        if (!db_keys) {
+            LM_ERR("Failed to allocate memory for db type keys\n");
+            goto error;
+        }
+        for (j = 0, tmp = all_type_keys; tmp; ++j, tmp = tmp->next) {
+            db_keys[j] = &tmp->key;
+        }
+
+        db_vals = (db_val_t*) pkg_malloc(all_type_keys_count * sizeof(db_val_t));
+        if (!db_vals) {
+            LM_ERR("Failed to allocate memory for manual db vals\n");
+            goto error;
+        }
+
+        for (j = 0, all_type_key = all_type_keys; all_type_key; ++j, all_type_key = all_type_key->next) {
+            db_val_t *v = &(db_vals[j]);
+            str *key = &all_type_key->key;
+            char *value = reply->element[manual_keys_count + j]->str;
+            int coltype = db_redis_schema_get_column_type(con, CON_TABLE(_h), key);
+            if (value == NULL) {
+                VAL_NULL(v) = 1;
+            } else if (db_str2val(coltype, v, value, strlen(value), 0) != 0) {
+                LM_ERR("Failed to convert redis reply column to db value\n");
+                goto error;
+            }
+        }
+        if (db_redis_build_type_keys(con, CON_TABLE(_h), db_keys, db_vals, all_type_keys_count,
+                    &type_keys, &type_keys_count) != 0) {
+            LM_ERR("failed to build type keys\n");
+            goto error;
+        }
+        pkg_free(db_keys);
+        pkg_free(db_vals);
+        db_redis_free_reply(&reply);
+
+        if (db_redis_key_add_string(&query_v, "DEL", 3) != 0) {
+            LM_ERR("Failed to add del command to delete query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, key) != 0) {
+            LM_ERR("Failed to add key to delete query\n");
+            goto error;
+        }
+
+        reply = db_redis_command_argv(con, query_v);
+        db_redis_key_free(&query_v);
+        db_redis_check_reply(con, reply, error);
+        db_redis_free_reply(&reply);
+
+        for (redis_key_t *type_key = type_keys; type_key; type_key = type_key->next) {
+            if (db_redis_key_add_string(&query_v, "SREM", 4) != 0) {
+                LM_ERR("Failed to add srem command to post-delete query\n");
+                goto error;
+            }
+            if (db_redis_key_add_str(&query_v, &type_key->key) != 0) {
+                LM_ERR("Failed to add key to delete query\n");
+                goto error;
+            }
+            if (db_redis_key_add_str(&query_v, key) != 0) {
+                LM_ERR("Failed to add key to delete query\n");
+                goto error;
+            }
+            reply = db_redis_command_argv(con, query_v);
+            db_redis_key_free(&query_v);
+            db_redis_check_reply(con, reply, error);
+            db_redis_free_reply(&reply);
+        }
+
+        //db_redis_key_free(&type_keys);
+        LM_DBG("+++ done with loop '%.*s'\n", k->key.len, k->key.s);
+    }
+    pkg_free(query_v);
+    db_redis_key_free(&type_keys);
+    db_redis_key_free(&all_type_keys);
+
+    return 0;
+
+error:
+    LM_ERR("failed to perform the delete\n");
+    if(reply)
+        db_redis_free_reply(&reply);
+    if (db_keys)
+        pkg_free(db_keys);
+    if (db_vals)
+        pkg_free(db_vals);
+    db_redis_key_free(&query_v);
+    db_redis_key_free(&type_keys);
+    db_redis_key_free(&all_type_keys);
+    return -1;
+}
+
+static int db_redis_perform_update(const db1_con_t* _h, km_redis_con_t *con, const db_key_t* _k,
+        const db_val_t* _v, const db_op_t *_op, const db_key_t* _uk, const db_val_t *_uv,
+        const int _n, const int _nu,
+        redis_key_t **keys, int *keys_count,
+        int **manual_keys, int *manual_keys_count, int do_table_scan) {
+
+    redisReply *reply = NULL;
+    redis_key_t *query_v = NULL;
+    int update_queries = 0;
+
+    if (!keys_count && do_table_scan) {
+        LM_DBG("performing full table scan\n");
+        if (db_redis_scan_query_keys(con, CON_TABLE(_h), _k, _n,
+                    keys, keys_count,
+                    manual_keys, manual_keys_count) != 0) {
+            LM_ERR("failed to scan query keys\n");
+            goto error;
+        }
+    }
+
+    for (redis_key_t *key = *keys; key; key = key->next) {
+        str *keyname = &key->key;
+
+        LM_DBG("fetching row for '%.*s' from redis\n", keyname->len, keyname->s);
+
+
+        if (db_redis_key_add_string(&query_v, "EXISTS", 6) != 0) {
+            LM_ERR("Failed to set exists command to pre-update exists query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, keyname) != 0) {
+            LM_ERR("Failed to add key name to pre-update exists query\n");
+            goto error;
+        }
+        if (db_redis_append_command_argv(con, query_v) != REDIS_OK) {
+            LM_ERR("Failed to append redis command\n");
+            goto error;
+        }
+        db_redis_key_free(&query_v);
+
+        // construct HMGET query
+        if ((*manual_keys_count) == 0) {
+            if (db_redis_key_add_string(&query_v, "HGETALL", 7) != 0) {
+                LM_ERR("Failed to set hgetall command to pre-update hget query\n");
+                goto error;
+            }
+            // TODO: actually we wouldn't have to fetch it at all, but then we'd
+            // have to mark this key telling to not fetch reply of HMGET after
+            // EXISTS returns false!
+        } else {
+            if (db_redis_key_add_string(&query_v, "HMGET", 5) != 0) {
+                LM_ERR("Failed to set hgetall command to pre-update hget query\n");
+                goto error;
+            }
+        }
+        if (db_redis_key_add_str(&query_v, keyname) != 0) {
+            LM_ERR("Failed to add key name to pre-update exists query\n");
+            goto error;
+        }
+
+        for (int j = 0; j < *manual_keys_count; ++j) {
+            int idx = (*manual_keys)[j];
+            str *k_name = _k[idx];
+            if (db_redis_key_add_str(&query_v, k_name) != 0) {
+                LM_ERR("Failed to add manual key name to hget query\n");
+                goto error;
+            }
+        }
+
+        if (db_redis_append_command_argv(con, query_v) != REDIS_OK) {
+            LM_ERR("Failed to append redis command\n");
+            goto error;
+        }
+        db_redis_key_free(&query_v);
+    }
+
+/*
+    key_value = (str*)pkg_malloc(_nu * sizeof(str));
+    if (!key_value) {
+        LM_ERR("Failed to allocate memory for key buffer\n");
+        goto error;
+    }
+    memset(key_value, 0, _nu * sizeof(str));
+
+    col_value = (str*)pkg_malloc(_nu * sizeof(str));
+    if (!col_value) {
+        LM_ERR("Failed to allocate memory for column buffer\n");
+        goto error;
+    }
+    memset(col_value, 0, _nu * sizeof(str));
+    */
+
+
+    for (redis_key_t *key = *keys; key; key = key->next) {
+        int row_match;
+
+        LM_DBG("fetching replies for '%.*s' from redis\n", key->key.len, key->key.s);
+
+        // get reply for EXISTS query
+        if (db_redis_get_reply(con, (void**)&reply) != REDIS_OK) {
+            LM_ERR("Failed to get reply for query: %s\n",
+                    con->con->errstr);
+            goto error;
+        }
+        db_redis_check_reply(con, reply, error);
+        if (reply->integer == 0) {
+            LM_DBG("key does not exist, returning no row for query\n");
+            db_redis_free_reply(&reply);
+            // also free next reply, as this is a null row for the HMGET
+            LM_DBG("also fetch hmget reply after non-existent key\n");
+            db_redis_get_reply(con, (void**)&reply);
+            db_redis_check_reply(con, reply, error);
+            db_redis_free_reply(&reply);
+            LM_DBG("continue fetch reply loop\n");
+            continue;
+        }
+        db_redis_free_reply(&reply);
+
+        // get reply for actual HMGET query
+        if (db_redis_get_reply(con, (void**)&reply) != REDIS_OK) {
+            LM_ERR("Failed to get reply for query: %s\n",
+                    con->con->errstr);
+            goto error;
+        }
+        db_redis_check_reply(con, reply, error);
+        if (reply->type != REDIS_REPLY_ARRAY) {
+            LM_ERR("Unexpected reply, expected array\n");
+            goto error;
+        }
+        LM_DBG("dumping full query reply for row\n");
+        db_redis_dump_reply(reply);
+
+        // manually filter non-matching replies
+        row_match = 1;
+        for (size_t col = 0; col < reply->elements; ++col) {
+            if (col < *manual_keys_count) {
+                int idx = (*manual_keys)[col];
+                db_key_t k = _k[idx];
+                db_val_t v = _v[idx];
+                db_op_t o = _op[idx];
+                LM_DBG("manually filtering key '%.*s'\n",
+                        k->len, k->s);
+                if (db_redis_compare_column(k, &v, o, reply->element[col]) != 0) {
+                    LM_DBG("column %lu does not match, ignore row\n", col);
+                    row_match = 0;
+                    break;
+                }
+            }
+        }
+        db_redis_free_reply(&reply);
+        if (!row_match) {
+            continue;
+        } else {
+            LM_DBG("row matches manual filtering, proceed with update\n");
+        }
+
+        if (db_redis_key_add_string(&query_v, "HMSET", 5) != 0) {
+            LM_ERR("Failed to add hmset command to update query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, &key->key) != 0) {
+            LM_ERR("Failed to add key to update query\n");
+            goto error;
+        }
+
+        for (int i = 0; i < _nu; ++i) {
+            str *k = _uk[i];
+            str v = {NULL, 0};
+
+            if (db_redis_val2str(&(_uv[i]), &v) != 0) {
+                LM_ERR("Failed to convert update value to string\n");
+                goto error;
+            }
+            if (db_redis_key_add_str(&query_v, k) != 0) {
+                LM_ERR("Failed to add key to update query\n");
+                goto error;
+            }
+            if (db_redis_key_add_str(&query_v, &v) != 0) {
+                LM_ERR("Failed to add key to update query\n");
+                goto error;
+            }
+            pkg_free(v.s);
+        }
+        update_queries++;
+        if (db_redis_append_command_argv(con, query_v) != REDIS_OK) {
+            LM_ERR("Failed to append redis command\n");
+            goto error;
+        }
+
+        db_redis_key_free(&query_v);
+    }
+
+    LM_DBG("getting replies for %d queries\n", update_queries);
+
+    for (int i = 0; i < update_queries; ++i) {
+        if (db_redis_get_reply(con, (void**)&reply) != REDIS_OK) {
+            LM_ERR("Failed to get reply for query: %s\n",
+                    con->con->errstr);
+            goto error;
+        }
+        db_redis_check_reply(con, reply, error);
+        db_redis_free_reply(&reply);
+    }
+
+    LM_DBG("done performing update\n");
+
+    return 0;
+
+error:
+    LM_ERR("failed to perform the update\n");
+    if (reply)
+        db_redis_free_reply(&reply);
+    db_redis_key_free(&query_v);
+    return -1;
+}
+
+
+/*
+ * Query table for specified rows
+ * _h: structure representing database connection
+ * _k: key names
+ * _op: operators
+ * _v: values of the keys that must match
+ * _c: column names to return
+ * _n: number of key=values pairs to compare
+ * _nc: number of columns to return
+ * _o: order by the specified column
+ */
+int db_redis_query(const db1_con_t* _h, const db_key_t* _k, const db_op_t* _op,
+        const db_val_t* _v, const db_key_t* _c, const int _n, const int _nc,
+        const db_key_t _o, db1_res_t** _r)
+{
+    km_redis_con_t *con = NULL;
+    int free_op = 0;
+    int do_table_scan = 0;
+
+    redis_key_t *keys = NULL;
+    int keys_count = 0;
+    int *manual_keys = NULL;
+    int manual_keys_count = 0;
+    db_op_t *query_ops = NULL;
+
+    // TODO: implement order-by
+    // TODO: optimize mapping-based manual post-check (remove check for keys already
+    // in type query key)
+
+    con = REDIS_CON(_h);
+    if (con && con->con == NULL) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to server\n");
+            return -1;
+        }
+    }
+    if(con == NULL || con->id == NULL || con->con == NULL) {
+        LM_ERR("connection to server is null\n");
+        return -1;
+    }
+    if(CON_TABLE(_h)->s == NULL) {
+        LM_ERR("prefix (table) name not set\n");
+        return -1;
+    } else {
+        LM_DBG("querying prefix (table) '%.*s'\n",
+                CON_TABLE(_h)->len, CON_TABLE(_h)->s);
+    }
+
+    if(_r) *_r = NULL;
+
+    // check if we have a version query, and return version directly from
+    // schema instead of loading it from redis
+    if (_nc == 1 && _n >= 1 && VAL_TYPE(&_v[0]) == DB1_STR &&
+            CON_TABLE(_h)->len == strlen("version") && strncmp(CON_TABLE(_h)->s, "version", CON_TABLE(_h)->len) == 0 &&
+            _k[0]->len == strlen("table_name") && strncmp(_k[0]->s, "table_name", _k[0]->len) == 0 &&
+            _c[0]->len == strlen("table_version") && strncmp(_c[0]->s, "table_version", _c[0]->len) == 0) {
+
+        if (db_redis_return_version(_h, con, &VAL_STR(&_v[0]), _r) == 0) {
+            return 0;
+        }
+        // if we fail to return a version from the schema, go query the table, just in case
+    }
+
+    free_op = 0;
+
+    if (_op == NULL) {
+        char *op = "=";
+        free_op = 1;
+        query_ops = (db_op_t*)pkg_malloc(_n * sizeof(db_op_t));
+        if (!query_ops) {
+            LM_ERR("Failed to allocate memory for query op list\n");
+            goto error;
+        }
+        for (int i = 0; i < _n; ++i) {
+            query_ops[i] = op;
+        }
+    } else {
+        query_ops = (db_op_t*)_op;
+    }
+
+    if (_n > 0) {
+        if (db_redis_build_query_keys(con, CON_TABLE(_h), _k, _v, query_ops, _n,
+                    &keys, &keys_count, &manual_keys, &manual_keys_count, &do_table_scan) != 0) {
+            LM_ERR("failed to build query keys\n");
+            goto error;
+        }
+        if (!keys_count) {
+            if (do_table_scan) {
+                LM_DBG("unable to build query keys, falling back to full table scan\n");
+            } else {
+                LM_DBG("query keys not member of suitable mapping, skip full table scan\n");
+            }
+        }
+    } else {
+        LM_DBG("no columns given to build query keys, falling back to full table scan\n");
+        keys_count = 0;
+    }
+
+    if (db_redis_perform_query(_h, con, _k, _v, query_ops, _c, _n, _nc, _r,
+        &keys, &keys_count, &manual_keys, &manual_keys_count, do_table_scan) != 0) {
+        goto error;
+    }
+
+    LM_DBG("done performing query\n");
+
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+
+    if (manual_keys) {
+        pkg_free(manual_keys);
+    }
+
+    db_redis_consume_replies(con);
+    return 0;
+
+error:
+    LM_ERR("failed to do the query\n");
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+    if (manual_keys) {
+        pkg_free(manual_keys);
+    }
+    db_redis_consume_replies(con);
+
+
+    return -1;
+}
+
+/*
+ * Execute a raw SQL query
+ */
+int db_redis_raw_query(const db1_con_t* _h, const str* _s, db1_res_t** _r)
+{
+    LM_DBG("perform redis raw query\n");
+    return -1;
+}
+
+/*
+ * Insert a row into specified table
+ * _h: structure representing database connection
+ * _k: key names
+ * _v: values of the keys
+ * _n: number of key=value pairs
+ */
+int db_redis_insert(const db1_con_t* _h, const db_key_t* _k, const db_val_t* _v, const int _n)
+{
+    km_redis_con_t *con = NULL;
+    redis_key_t *key = NULL;
+    int keys_count = 0;
+    redis_key_t *type_keys = NULL;
+    int type_keys_count = 0;
+    redis_key_t *query_v = NULL;
+    redisReply *reply = NULL;
+
+    con = REDIS_CON(_h);
+    if (con && con->con == NULL) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to server\n");
+            return -1;
+        }
+    }
+    if(con == NULL || con->id == NULL || con->con == NULL) {
+        LM_ERR("connection to server is null\n");
+        return -1;
+    }
+    if(CON_TABLE(_h)->s == NULL) {
+        LM_ERR("prefix (table) name not set\n");
+        return -1;
+    } else {
+        LM_DBG("inserting to prefix (table) '%.*s'\n",
+                CON_TABLE(_h)->len, CON_TABLE(_h)->s);
+    }
+
+    if (db_redis_build_entry_keys(con, CON_TABLE(_h), _k, _v, _n,
+                &key, &keys_count) != 0) {
+        LM_ERR("failed to build entry keys\n");
+        goto error;
+    }
+    if (db_redis_build_type_keys(con, CON_TABLE(_h), _k, _v, _n,
+                &type_keys, &type_keys_count) != 0) {
+        LM_ERR("failed to build type keys\n");
+        goto error;
+    }
+
+    if (db_redis_key_add_string(&query_v, "HMSET", 5) != 0) {
+        LM_ERR("Failed to add hmset command to insert query\n");
+        goto error;
+    }
+    if (db_redis_key_add_str(&query_v, &key->key) != 0) {
+        LM_ERR("Failed to add key to insert query\n");
+        goto error;
+    }
+
+    for (int i = 0; i < _n; ++i) {
+        str *k = _k[i];
+        str v;
+
+        if (db_redis_key_add_str(&query_v, k) != 0) {
+            LM_ERR("Failed to add column name to insert query\n");
+            goto error;
+        }
+        if (db_redis_val2str(&(_v[i]), &v) != 0) {
+            LM_ERR("Failed to allocate memory for query value\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, &v) != 0) {
+            LM_ERR("Failed to add column value to insert query\n");
+            goto error;
+        }
+        pkg_free(v.s);
+    }
+
+    reply = db_redis_command_argv(con, query_v);
+    db_redis_key_free(&query_v);
+    db_redis_check_reply(con, reply, error);
+    db_redis_free_reply(&reply);
+
+    for (redis_key_t *k = type_keys; k; k = k->next) {
+        str *type_key = &k->key;
+
+        LM_DBG("inserting entry key '%.*s' to type map '%.*s'\n",
+            key->key.len, key->key.s, type_key->len, type_key->s);
+
+        if (db_redis_key_add_string(&query_v, "SADD", 4) != 0) {
+            LM_ERR("Failed to set sadd command to post-insert query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, type_key) != 0) {
+            LM_ERR("Failed to add map key to post-insert query\n");
+            goto error;
+        }
+        if (db_redis_key_add_str(&query_v, &key->key) != 0) {
+            LM_ERR("Failed to set entry key to post-insert query\n");
+            goto error;
+        }
+
+        reply = db_redis_command_argv(con, query_v);
+        db_redis_key_free(&query_v);
+        db_redis_check_reply(con, reply, error);
+        db_redis_free_reply(&reply);
+    }
+
+    db_redis_key_free(&key);
+    db_redis_key_free(&type_keys);
+    db_redis_consume_replies(con);
+
+    return 0;
+
+error:
+    db_redis_key_free(&key);
+    db_redis_key_free(&type_keys);
+    db_redis_key_free(&query_v);
+
+    if (reply)
+        db_redis_free_reply(&reply);
+
+    db_redis_consume_replies(con);
+
+    LM_ERR("failed to do the insert\n");
+    return -1;
+}
+
+/*
+ * Delete a row from the specified table
+ * _h: structure representing database connection
+ * _k: key names
+ * _o: operators
+ * _v: values of the keys that must match
+ * _n: number of key=value pairs
+ */
+int db_redis_delete(const db1_con_t* _h, const db_key_t* _k,
+        const db_op_t* _op, const db_val_t* _v, const int _n)
+{
+    km_redis_con_t *con = NULL;
+    redis_key_t *keys = NULL;
+    int keys_count = 0;
+    int *manual_keys = NULL;
+    int manual_keys_count = 0;
+    int free_op = 0;
+    int do_table_scan = 0;
+    db_op_t *query_ops = NULL;
+
+    // TODO: optimize mapping-based manual post-check (remove check for keys already
+    // in type query key)
+
+    con = REDIS_CON(_h);
+    if (con && con->con == NULL) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to server\n");
+            return -1;
+        }
+    }
+    if(con == NULL || con->id == NULL || con->con == NULL) {
+        LM_ERR("connection to server is null\n");
+        return -1;
+    }
+    if(CON_TABLE(_h)->s == NULL) {
+        LM_ERR("prefix (table) name not set\n");
+        return -1;
+    } else {
+        LM_DBG("deleting from prefix (table) '%.*s'\n",
+                CON_TABLE(_h)->len, CON_TABLE(_h)->s);
+    }
+
+    free_op = 0;
+
+    if (_op == NULL) {
+        char *op = "=";
+        free_op = 1;
+        query_ops = (db_op_t*)pkg_malloc(_n * sizeof(db_op_t));
+        if (!query_ops) {
+            LM_ERR("Failed to allocate memory for query op list\n");
+            goto error;
+        }
+        for (int i = 0; i < _n; ++i) {
+            query_ops[i] = op;
+        }
+    } else {
+        query_ops = (db_op_t*)_op;
+    }
+
+    if (_n > 0) {
+        if (db_redis_build_query_keys(con, CON_TABLE(_h), _k, _v, query_ops, _n,
+                    &keys, &keys_count, &manual_keys, &manual_keys_count, &do_table_scan) != 0) {
+            LM_ERR("failed to build query keys\n");
+            goto error;
+        }
+        if (!keys_count) {
+            if (do_table_scan) {
+                LM_DBG("unable to build query keys, falling back to full table scan\n");
+            } else {
+                LM_DBG("query keys not member of suitable mapping, skip full table scan\n");
+            }
+        }
+    } else {
+        LM_DBG("no columns given to build query keys, falling back to full table scan\n");
+        keys_count = 0;
+    }
+
+    if (db_redis_perform_delete(_h, con, _k, _v, query_ops, _n,
+        keys, keys_count, manual_keys, manual_keys_count, do_table_scan) != 0) {
+        goto error;
+    }
+
+    LM_DBG("done performing delete\n");
+
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+    if (manual_keys)
+        pkg_free(manual_keys);
+    db_redis_consume_replies(con);
+
+    return 0;
+
+error:
+    LM_ERR("failed to do the query\n");
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+    if (manual_keys)
+        pkg_free(manual_keys);
+    db_redis_consume_replies(con);
+    return -1;
+}
+
+/*
+ * Update some rows in the specified table
+ * _h: structure representing database connection
+ * _k: key names
+ * _op: operators
+ * _v: values of the keys that must match
+ * _uk: updated columns
+ * _uv: updated values of the columns
+ * _n: number of key=value pairs
+ * _un: number of columns to update
+ */
+int db_redis_update(const db1_con_t* _h, const db_key_t* _k,
+        const db_op_t* _op, const db_val_t* _v, const db_key_t* _uk,
+        const db_val_t* _uv, const int _n, const int _nu)
+{
+    km_redis_con_t *con = NULL;
+    int free_op = 0;
+    int do_table_scan = 0;
+
+    redis_key_t *keys = NULL;
+    int keys_count = 0;
+    int *manual_keys = NULL;
+    int manual_keys_count = 0;
+    db_op_t *query_ops = NULL;
+
+    // TODO: optimize mapping-based manual post-check (remove check for keys already
+    // in type query key)
+
+    con = REDIS_CON(_h);
+    if (con && con->con == NULL) {
+        if (db_redis_connect(con) != 0) {
+            LM_ERR("Failed to reconnect to server\n");
+            return -1;
+        }
+    }
+    if(con == NULL || con->id == NULL || con->con == NULL) {
+        LM_ERR("connection to server is null\n");
+        return -1;
+    }
+    if(CON_TABLE(_h)->s == NULL) {
+        LM_ERR("prefix (table) name not set\n");
+        return -1;
+    } else {
+        LM_DBG("updating prefix (table) '%.*s'\n",
+                CON_TABLE(_h)->len, CON_TABLE(_h)->s);
+    }
+
+    free_op = 0;
+
+    if (_op == NULL) {
+        char *op = "=";
+        free_op = 1;
+        query_ops = (db_op_t*)pkg_malloc(_n * sizeof(db_op_t));
+        if (!query_ops) {
+            LM_ERR("Failed to allocate memory for query op list\n");
+            goto error;
+        }
+        for (int i = 0; i < _n; ++i) {
+            query_ops[i] = op;
+        }
+    } else {
+        query_ops = (db_op_t*)_op;
+    }
+
+    if (_n > 0) {
+        if (db_redis_build_query_keys(con, CON_TABLE(_h), _k, _v, query_ops, _n,
+                    &keys, &keys_count, &manual_keys, &manual_keys_count, &do_table_scan) != 0) {
+            LM_ERR("failed to build query keys\n");
+            goto error;
+        }
+        if (!keys_count) {
+            if (do_table_scan) {
+                LM_DBG("unable to build query keys, falling back to full table scan\n");
+            } else {
+                LM_DBG("query keys not member of suitable mapping, skip full table scan\n");
+            }
+        }
+    } else {
+        LM_DBG("no columns given to build query keys, falling back to full table scan\n");
+        keys_count = 0;
+    }
+
+    if (db_redis_perform_update(_h, con, _k, _v, query_ops, _uk, _uv, _n, _nu,
+        &keys, &keys_count, &manual_keys, &manual_keys_count, do_table_scan) != 0) {
+        goto error;
+    }
+
+    LM_DBG("done performing update\n");
+
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+
+    if (manual_keys) {
+        pkg_free(manual_keys);
+    }
+    db_redis_consume_replies(con);
+    return 0;
+
+error:
+    LM_ERR("failed to do the query\n");
+    if (free_op && query_ops) {
+        pkg_free(query_ops);
+    }
+    db_redis_key_free(&keys);
+    if (manual_keys) {
+        pkg_free(manual_keys);
+    }
+    db_redis_consume_replies(con);
+    return -1;
+}
+
+/*
+ * Just like insert, but replace the row if it exists
+ */
+int db_redis_replace(const db1_con_t* _h, const db_key_t* _k,
+        const db_val_t* _v, const int _n, const int _un, const int _m) {
+    LM_DBG("perform redis replace\n");
+    return -1;
+}
+
+/*
+ * Store name of table that will be used by
+ * subsequent database functions
+ */
+int db_redis_use_table(db1_con_t* _h, const str* _t) {
+    return db_use_table(_h, _t);
+}
+
+int db_redis_free_result(db1_con_t* _h, db1_res_t* _r) {
+    LM_DBG("perform redis free result\n");
+
+    if(!_r)
+        return -1;
+    db_free_result(_r);
+    return 0;
+}

+ 88 - 0
src/modules/db_redis/redis_dbase.h

@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+
+#ifndef _REDIS_DBASE_H_
+#define _REDIS_DBASE_H_
+
+#include "db_redis_mod.h"
+
+/*
+ * Initialize database connection
+ */
+db1_con_t* db_redis_init(const str* _sqlurl);
+
+/*
+ * Close a database connection
+ */
+void db_redis_close(db1_con_t* _h);
+
+/*
+ * Free all memory allocated by get_result
+ */
+int db_redis_free_result(db1_con_t* _h, db1_res_t* _r);
+
+/*
+ * Do a query
+ */
+int db_redis_query(const db1_con_t* _h, const db_key_t* _k, const db_op_t* _op, const db_val_t* _v,
+const db_key_t* _c, const int _n, const int _nc, const db_key_t _o, db1_res_t** _r);
+
+/*
+ * Fetch rows from a result
+ */
+int db_redis_fetch_result(const db1_con_t* _h, db1_res_t** _r, const int nrows);
+
+/*
+ * Raw SQL query
+ */
+int db_redis_raw_query(const db1_con_t* _h, const str* _s, db1_res_t** _r);
+
+/*
+ * Insert a row into table
+ */
+int db_redis_insert(const db1_con_t* _h, const db_key_t* _k, const db_val_t* _v, const int _n);
+
+/*
+ * Delete a row from table
+ */
+int db_redis_delete(const db1_con_t* _h, const db_key_t* _k, const db_op_t* _o, const db_val_t* _v,
+const int _n);
+
+/*
+ * Update a row in table
+ */
+int db_redis_update(const db1_con_t* _h, const db_key_t* _k, const db_op_t* _o, const db_val_t* _v,
+const db_key_t* _uk, const db_val_t* _uv, const int _n, const int _un);
+
+/*
+ * Just like insert, but replace the row if it exists
+ */
+int db_redis_replace(const db1_con_t* handle, const db_key_t* keys, const db_val_t* vals,
+		const int n, const int _un, const int _m);
+
+/*
+ * Store name of table that will be used by
+ * subsequent database functions
+ */
+int db_redis_use_table(db1_con_t* _h, const str* _t);
+
+#endif  /* _REDIS_BASE_H_ */

+ 838 - 0
src/modules/db_redis/redis_table.c

@@ -0,0 +1,838 @@
+/*
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <dirent.h>
+
+#include "db_redis_mod.h"
+#include "redis_connection.h"
+#include "redis_table.h"
+
+int db_redis_key_add_string(redis_key_t **list, const char* entry, int len) {
+    redis_key_t *k;
+
+
+    k = (redis_key_t*)pkg_malloc(sizeof(redis_key_t));
+    if (!k) {
+        LM_ERR("Failed to allocate memory for key list entry\n");
+        goto err;
+    }
+    k->next = NULL;
+
+    k->key.s = (char*)pkg_malloc((len+1) * sizeof(char));
+    if (!k->key.s) {
+        LM_ERR("Failed to allocate memory for key list entry\n");
+        goto err;
+    }
+
+    memcpy(k->key.s, entry, len);
+    k->key.s[len] = '\0';
+    k->key.len = len;
+
+    if (!*list) {
+        *list = k;
+    } else {
+        redis_key_t *l = *list;
+        while (l->next)
+            l = l->next;
+        l->next = k;
+    }
+
+    return 0;
+
+err:
+    if (k)
+        pkg_free(k);
+    return -1;
+}
+
+int db_redis_key_add_str(redis_key_t **list, const str* entry) {
+    return db_redis_key_add_string(list, entry->s, entry->len);
+}
+
+int db_redis_key_prepend_string(redis_key_t **list, const char* entry, int len) {
+    redis_key_t *k;
+
+    k = (redis_key_t*)pkg_malloc(sizeof(redis_key_t));
+    if (!k) {
+        LM_ERR("Failed to allocate memory for key list entry\n");
+        goto err;
+    }
+    k->next = NULL;
+
+    k->key.s = (char*)pkg_malloc((len+1) * sizeof(char));
+    if (!k->key.s) {
+        LM_ERR("Failed to allocate memory for key list entry\n");
+        goto err;
+    }
+
+    memset(k->key.s, 0, len+1);
+    strncpy(k->key.s, entry, len);
+    k->key.len = len;
+
+    if (!*list) {
+        *list = k;
+    } else {
+        k->next = *list;
+        *list = k;
+    }
+
+    return 0;
+
+err:
+    if (k)
+        pkg_free(k);
+    return -1;
+}
+
+redis_key_t * db_redis_key_unshift(redis_key_t **list) {
+    redis_key_t *k;
+
+    k = *list;
+    *list = (*list)->next;
+    k->next = NULL;
+    return k;
+}
+
+void db_redis_key_free(redis_key_t **list) {
+    redis_key_t *l;
+    redis_key_t **head;
+
+    if (!list || !*list) {
+        return;
+    }
+    head = list;
+    do {
+        l = (*list)->next;
+        if ((*list)->key.s) {
+            pkg_free((*list)->key.s);
+            (*list)->key.s = NULL;
+            (*list)->key.len = 0;
+        }
+        pkg_free(*list);
+        *list = l;
+    } while (l);
+    *head = NULL;
+}
+
+int db_redis_key_list2arr(redis_key_t *list, char ***arr) {
+    int len = 0, i = 0;
+    redis_key_t *tmp = NULL;
+
+    *arr = NULL;
+    for (tmp = list, len = 0; tmp; tmp = tmp->next, len++);
+    if (len < 1) {
+        return 0;
+    }
+
+    *arr = (char**)pkg_malloc(len * sizeof (char*));
+    if (!*arr) {
+        LM_ERR("Failed to allocate memory for array\n");
+        return -1;
+    }
+    for (tmp = list, i = 0; tmp; tmp = tmp->next, i++) {
+        (*arr)[i] = tmp->key.s;
+    }
+    LM_DBG("returning %d entries\n", len);
+
+    return len;
+}
+
+
+int db_redis_schema_get_column_type(km_redis_con_t *con, const str *table_name, const str *col_name) {
+    struct str_hash_entry *table_e;
+    struct str_hash_entry *col_e;
+    redis_table_t *table;
+
+    table_e = str_hash_get(&con->tables, table_name->s, table_name->len);
+    if (!table_e) {
+        LM_ERR("Failed to find table '%.*s' in table hash\n",
+                table_name->len, table_name->s);
+        return -1;
+    }
+    table = (redis_table_t*)table_e->u.p;
+    col_e = str_hash_get(&table->columns, col_name->s, col_name->len);
+    if (!col_e) {
+        LM_ERR("Failed to find column '%.*s' in schema for table '%.*s'\n",
+                col_name->len, col_name->s,
+                table_name->len, table_name->s);
+        return -1;
+    }
+    return col_e->u.n;
+}
+
+void db_redis_print_all_tables(km_redis_con_t *con) {
+    struct str_hash_table *ht;
+    struct str_hash_table *col_ht;
+    struct str_hash_entry *he;
+    struct str_hash_entry *col_he;
+    struct str_hash_entry *last;
+    struct str_hash_entry *col_last;
+    redis_table_t *table;
+    redis_key_t *key;
+    redis_type_t *type;
+
+    LM_DBG("dumping all redis tables:\n");
+    ht = &con->tables;
+
+    for (int i = 0; i < ht->size; ++i) {
+        last = (&ht->table[i])->prev;
+        clist_foreach(&ht->table[i], he, next) {
+            LM_DBG("  table %.*s\n", he->key.len, he->key.s);
+            table = (redis_table_t*) he->u.p;
+
+            LM_DBG("    schema:\n");
+            col_ht = &table->columns;
+            for (int j = 0; j < col_ht->size; ++j) {
+                col_last = (&col_ht->table[j])->prev;
+                clist_foreach(&col_ht->table[j], col_he, next) {
+                    LM_DBG("      %.*s: %d\n",
+                            col_he->key.len, col_he->key.s, col_he->u.n);
+                    if (col_he == col_last) break;
+                }
+            }
+
+            LM_DBG("    entry keys:\n");
+            key = table->entry_keys;
+            while (key) {
+                LM_DBG("      %.*s\n", key->key.len, key->key.s);
+                key = key->next;
+            }
+
+            type = table->types;
+            while (type) {
+                LM_DBG("    %.*s keys:\n", type->type.len, type->type.s);
+                key = type->keys;
+                while (key) {
+                    LM_DBG("      %.*s\n", key->key.len, key->key.s);
+                    key = key->next;
+                }
+                type = type->next;
+            }
+
+            if (he == last) break;
+        }
+    }
+}
+
+void db_redis_print_table(km_redis_con_t *con, char *name) {
+    str table_name;
+    struct str_hash_entry *table_entry;
+    redis_table_t *table;
+    redis_key_t *key;
+    redis_type_t *type;
+
+    struct str_hash_table *col_ht;
+    struct str_hash_entry *col_he;
+    struct str_hash_entry *col_last;
+
+    table_name.s = name;
+    table_name.len = strlen(name);
+
+    table_entry = str_hash_get(&con->tables, table_name.s, table_name.len);
+    if (!table_entry) {
+        LM_ERR("Failed to print table '%.*s', no such table entry found\n",
+                table_name.len, table_name.s);
+        return;
+    }
+
+    table = (redis_table_t*) table_entry->u.p;
+    LM_DBG("table %.*s:\n", table_name.len, table_name.s);
+
+    LM_DBG("  schema:\n");
+    col_ht = &table->columns;
+    for (int j = 0; j < col_ht->size; ++j) {
+        col_last = (&col_ht->table[j])->prev;
+        clist_foreach(&col_ht->table[j], col_he, next) {
+            LM_DBG("    %.*s: %d\n",
+                    col_he->key.len, col_he->key.s, col_he->u.n);
+            if (col_he == col_last) break;
+        }
+    }
+
+    LM_DBG("  entry keys:\n");
+    key = table->entry_keys;
+    while (key) {
+        LM_DBG("    %.*s\n", key->key.len, key->key.s);
+        key = key->next;
+    }
+
+    type = table->types;
+    while (type) {
+        LM_DBG("  %.*s keys:\n", type->type.len, type->type.s);
+        key = type->keys;
+        while (key) {
+            LM_DBG("    %.*s\n", key->key.len, key->key.s);
+            key = key->next;
+        }
+        type = type->next;
+    }
+}
+
+void db_redis_free_tables(km_redis_con_t *con) {
+    // TODO: also free schema hash?
+    struct str_hash_table *ht;
+    struct str_hash_table *col_ht;
+    struct str_hash_entry *he;
+    struct str_hash_entry *col_he;
+    struct str_hash_entry *last;
+    struct str_hash_entry *col_last;
+    redis_table_t *table;
+    redis_key_t *key, *tmpkey;
+    redis_type_t *type, *tmptype;
+
+    ht = &con->tables;
+    for (int i = 0; i < ht->size; ++i) {
+        last = (&ht->table[i])->prev;
+        clist_foreach(&ht->table[i], he, next) {
+            table = (redis_table_t*) he->u.p;
+
+            col_ht = &table->columns;
+            for (int j = 0; j < col_ht->size; ++j) {
+                col_last = (&col_ht->table[j])->prev;
+                clist_foreach(&col_ht->table[j], col_he, next) {
+                    pkg_free(col_he->key.s);
+                    pkg_free(col_he);
+                    if (col_he == col_last) break;
+                }
+            }
+            pkg_free(col_ht->table);
+
+            key = table->entry_keys;
+            while (key) {
+                tmpkey = key;
+                key = key->next;
+                pkg_free(tmpkey);
+            }
+
+            type = table->types;
+            while (type) {
+                key = type->keys;
+                while (key) {
+                    tmpkey = key;
+                    key = key->next;
+                    pkg_free(tmpkey);
+                }
+                tmptype = type;
+                type = type->next;
+                pkg_free(tmptype);
+            }
+            pkg_free(table);
+            pkg_free(he->key.s);
+            pkg_free(he);
+
+            if (he == last) break;
+        }
+    }
+    pkg_free(ht->table);
+}
+
+static redis_key_t* db_redis_create_key(str *key) {
+    redis_key_t *e;
+    e = (redis_key_t*) pkg_malloc(sizeof(redis_key_t));
+    if (!e) {
+        LM_ERR("Failed to allocate memory for key entry\n");
+        return NULL;
+    }
+    memset(e, 0, sizeof(redis_key_t));
+    e->key.s = key->s;
+    e->key.len = key->len;
+    return e;
+}
+
+static redis_type_t* db_redis_create_type(str *type) {
+    redis_type_t *e;
+    e = (redis_type_t*) pkg_malloc(sizeof(redis_type_t));
+    if (!e) {
+        LM_ERR("Failed to allocate memory for table type\n");
+        return NULL;
+    }
+    e->type.s = type->s;
+    e->type.len = type->len;
+    e->next = NULL;
+    e->keys = NULL;
+    return e;
+}
+
+static struct str_hash_entry* db_redis_create_table(str *table) {
+    struct str_hash_entry *e;
+    redis_table_t *t;
+
+    LM_DBG("creating schema hash entry for table '%.*s'", table->len, table->s);
+
+    e = (struct str_hash_entry*) pkg_malloc(sizeof(struct str_hash_entry));
+    if (!e) {
+        LM_ERR("Failed to allocate memory for table entry\n");
+        return NULL;
+    }
+    memset(e, 0, sizeof(struct str_hash_entry));
+
+    if (pkg_str_dup(&e->key, table) != 0) {
+        LM_ERR("Failed to allocate memory for table name\n");
+        pkg_free(e);
+        return NULL;
+    }
+    e->flags = 0;
+
+    t = (redis_table_t*) pkg_malloc(sizeof(redis_table_t));
+    if (!t) {
+        LM_ERR("Failed to allocate memory for table data\n");
+        pkg_free(e->key.s);
+        pkg_free(e);
+        return NULL;
+    }
+    t->entry_keys = NULL;
+    t->types = NULL;
+
+    if (str_hash_alloc(&t->columns, REDIS_HT_SIZE) != 0) {
+        LM_ERR("Failed to allocate memory for table schema hashtable\n");
+        pkg_free(e->key.s);
+        pkg_free(e);
+        return NULL;
+    }
+    str_hash_init(&t->columns);
+
+    e->u.p = t;
+    return e;
+}
+
+static struct str_hash_entry* db_redis_create_column(str *col, str *type) {
+    struct str_hash_entry *e;
+    e = (struct str_hash_entry*) pkg_malloc(sizeof(struct str_hash_entry));
+    if (!e) {
+        LM_ERR("Failed to allocate memory for column entry\n");
+        return NULL;
+    }
+    if (pkg_str_dup(&e->key, col) != 0) {
+        LM_ERR("Failed to allocate memory for column name\n");
+        return NULL;
+    }
+    e->flags = 0;
+    switch (type->s[0]) {
+        case 's':
+        case 'S':
+            e->u.n = DB1_STRING;
+            break;
+        case 'i':
+        case 'I':
+            e->u.n = DB1_INT;
+            break;
+        case 't':
+        case 'T':
+            e->u.n = DB1_DATETIME;
+            break;
+        case 'd':
+        case 'D':
+            e->u.n = DB1_DOUBLE;
+            break;
+        case 'b':
+        case 'B':
+            e->u.n = DB1_BLOB;
+            break;
+        default:
+            LM_ERR("Invalid schema column type '%.*s', expecting one of string, int, timestamp, double, blob\n",
+                    type->len, type->s);
+            pkg_free(e);
+            return NULL;
+    }
+    return e;
+}
+
+int db_redis_parse_keys(km_redis_con_t *con) {
+    char *p;
+    char *start;
+    char *end;
+
+    str table_name;
+    str type_name;
+    str column_name;
+
+    struct str_hash_entry *table_entry;
+    redis_table_t *table;
+    redis_type_t *type;
+    redis_type_t *type_target;
+    redis_key_t *key;
+    redis_key_t **key_target;
+    redis_key_t *key_location;
+
+    enum {
+        DBREDIS_KEYS_TABLE_ST,
+        DBREDIS_KEYS_TYPE_ST,
+        DBREDIS_KEYS_COLUMN_ST,
+        DBREDIS_KEYS_END_ST
+    } state;
+
+    if (!redis_keys.len) {
+        LM_ERR("Failed to parse empty 'keys' mod-param, please define it!\n");
+        return -1;
+    }
+
+    type_target = NULL;
+    key_location = NULL;
+    end = redis_keys.s + redis_keys.len;
+    p = start = redis_keys.s;
+    state = DBREDIS_KEYS_TABLE_ST;
+    do {
+        switch(state) {
+            case DBREDIS_KEYS_TABLE_ST:
+                while(p != end && *p != '=')
+                    ++p;
+                if (p == end) {
+                    LM_ERR("Invalid table definition, expecting <table>=<definition>\n");
+                    goto err;
+                }
+                table_name.s = start;
+                table_name.len = p - start;
+                state = DBREDIS_KEYS_TYPE_ST;
+                start = ++p;
+                LM_DBG("found table name '%.*s'\n", table_name.len, table_name.s);
+
+                table_entry = str_hash_get(&con->tables, table_name.s, table_name.len);
+                if (!table_entry) {
+                    LM_ERR("No table schema found for table '%.*s', fix config by adding one to the 'schema' mod-param!\n",
+                            table_name.len, table_name.s);
+                    goto err;
+                }
+                table = table_entry->u.p;
+                break;
+            case DBREDIS_KEYS_TYPE_ST:
+                while(p != end && *p != ':')
+                    ++p;
+                if (p == end) {
+                    LM_ERR("Invalid type definition, expecting <type>:<definition>\n");
+                    goto err;
+                }
+                type_name.s = start;
+                type_name.len = p - start;
+                state = DBREDIS_KEYS_COLUMN_ST;
+                start = ++p;
+                LM_DBG("found type name '%.*s' for table '%.*s'\n",
+                        type_name.len, type_name.s,
+                        table_name.len, table_name.s);
+                if (type_name.len == REDIS_DIRECT_PREFIX_LEN &&
+                        !strncmp(type_name.s, REDIS_DIRECT_PREFIX, type_name.len)) {
+                    key_target = &table->entry_keys;
+                } else {
+                    type = db_redis_create_type(&type_name);
+                    if (!type) goto err;
+                    if (!table->types) {
+                        table->types = type_target = type;
+                    } else {
+                        type_target->next = type;
+                        type_target = type_target->next;
+                    }
+                    key_target = &type->keys;
+                }
+                break;
+            case DBREDIS_KEYS_COLUMN_ST:
+                while(p != end && *p != ',' && *p != '&' && *p != ';')
+                    ++p;
+                if (p == end) {
+                    state = DBREDIS_KEYS_END_ST;
+                } else if (*p == ',') {
+                    state = DBREDIS_KEYS_COLUMN_ST;
+                } else if (*p == '&') {
+                    state = DBREDIS_KEYS_TYPE_ST;
+                } else if (*p == ';') {
+                    state = DBREDIS_KEYS_TABLE_ST;
+                }
+                column_name.s = start;
+                column_name.len = p - start;
+                start = ++p;
+                /*
+                LM_DBG("found column name '%.*s' in type '%.*s' for table '%.*s'\n",
+                        column_name.len, column_name.s,
+                        type_name.len, type_name.s,
+                        table_name.len, table_name.s);
+                */
+                key = db_redis_create_key(&column_name);
+                if (!key) goto err;
+                if (*key_target == NULL) {
+                    *key_target = key_location = key;
+                } else {
+                    key_location->next = key;
+                    key_location = key_location->next;
+                }
+                break;
+            case DBREDIS_KEYS_END_ST:
+                LM_DBG("done parsing keys definition\n");
+                return 0;
+
+
+        }
+    } while (p != end);
+
+    return 0;
+
+err:
+    db_redis_free_tables(con);
+    return -1;
+}
+
+
+int db_redis_parse_schema(km_redis_con_t *con) {
+    DIR *srcdir;
+    FILE *fin;
+    struct dirent* dent;
+    char *dir_name;
+
+    str table_name;
+    str column_name;
+    str type_name;
+
+    struct str_hash_entry *table_entry;
+    struct str_hash_entry *column_entry;
+    redis_table_t *table;
+
+    char full_path[_POSIX_PATH_MAX + 1];
+    int path_len;
+    struct stat fstat;
+    char c;
+
+    enum {
+        DBREDIS_SCHEMA_COLUMN_ST,
+        DBREDIS_SCHEMA_TYPE_ST,
+        DBREDIS_SCHEMA_VERSION_ST,
+        DBREDIS_SCHEMA_END_ST,
+    } state;
+
+    char buf[4096];
+    char *bufptr;
+
+
+    srcdir = NULL;
+    fin = NULL;
+    dir_name = NULL;
+
+    //LM_DBG("parsing schema '%.*s'\n", redis_schema.len, redis_schema.s);
+    if (!redis_schema_path.len) {
+        LM_ERR("Failed to parse empty 'schema_path' mod-param, please define it!\n");
+        return -1;
+    }
+
+    dir_name = (char*)pkg_malloc((redis_schema_path.len + 1) * sizeof(char));
+    strncpy(dir_name, redis_schema_path.s, redis_schema_path.len);
+    dir_name[redis_schema_path.len] = '\0';
+    srcdir = opendir(dir_name);
+    if (!srcdir) {
+        LM_ERR("Failed to open schema directory '%s'\n", dir_name);
+        goto err;
+    }
+
+    if (str_hash_alloc(&con->tables, REDIS_HT_SIZE) != 0) {
+        LM_ERR("Failed to allocate memory for tables hashtable\n");
+        goto err;
+    }
+    str_hash_init(&con->tables);
+
+    while((dent = readdir(srcdir)) != NULL) {
+        path_len = redis_schema_path.len;
+        if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) {
+            continue;
+        }
+        if ((path_len + strlen(dent->d_name) + 1) > _POSIX_PATH_MAX) {
+            LM_WARN("Redis schema path '%.*s/%s' is too long, skipping...\n",
+                redis_schema_path.len, redis_schema_path.s, dent->d_name);
+            continue;
+        }
+        snprintf(full_path, sizeof(full_path), "%s/%s", dir_name, dent->d_name);
+
+        if (stat(full_path, &fstat) < 0) {
+            LM_ERR("Failed to stat schema file %s\n", full_path);
+            continue;
+        }
+        if (!S_ISREG(fstat.st_mode)) {
+            LM_DBG("skipping schema file '%s' as it's not a regular file\n", full_path);
+            continue;
+        }
+
+        LM_DBG("reading schema full path '%s'\n", full_path);
+
+        fin = fopen(full_path, "r");
+        if (!fin) {
+            LM_ERR("Failed to open redis schema file '%s'\n", full_path);
+            continue;
+        }
+
+        table_name.s = dent->d_name;
+        table_name.len = strlen(table_name.s);
+        table_entry = str_hash_get(&con->tables, table_name.s, table_name.len);
+        if (table_entry) {
+            // should not happen, as this would require two files with same name
+            LM_WARN("Found duplicate table schema definition '%.*s', skipping...\n",
+                    table_name.len, table_name.s);
+            fclose(fin);
+            continue;
+        }
+        table_entry = db_redis_create_table(&table_name);
+        if (!table_entry) goto err;
+        str_hash_add(&con->tables, table_entry);
+        table = table_entry->u.p;
+
+        state = DBREDIS_SCHEMA_COLUMN_ST;
+        memset(buf, 0, sizeof(buf));
+        bufptr = buf;
+        do {
+            if (bufptr - buf > sizeof(buf)) {
+                LM_ERR("Schema line too long in file %s\n", full_path);
+                goto err;
+            }
+
+            c = fgetc(fin);
+
+            if (c == '\r')
+                continue;
+            //LM_DBG("parsing char %c, buf is '%s' at pos %lu\n", c, buf, bufpos);
+            switch(state) {
+                case DBREDIS_SCHEMA_COLUMN_ST:
+                    if (c == EOF) {
+                        LM_ERR("Unexpected end of file in schema column name of file %s\n", full_path);
+                        goto err;
+                    }
+                    if(c != '\n' && c != '/') {
+                        *bufptr = c;
+                        bufptr++;
+                        continue;
+                    }
+                    if (c == '\n') {
+                        if (bufptr == buf) {
+                            // trailing comma, skip
+                            state = DBREDIS_SCHEMA_VERSION_ST;
+                            continue;
+                        } else {
+                            LM_ERR("Invalid column definition, expecting <column>/<type>\n");
+                            goto err;
+                        }
+                    }
+                    column_name.s = buf;
+                    column_name.len = bufptr - buf;
+                    bufptr++;
+                    state = DBREDIS_SCHEMA_TYPE_ST;
+                    LM_DBG("found column name '%.*s'\n", column_name.len, column_name.s);
+                    break;
+                case DBREDIS_SCHEMA_TYPE_ST:
+                    if (c == EOF) {
+                        LM_ERR("Unexpected end of file in schema column type of file %s\n", full_path);
+                        goto err;
+                    }
+                    if(c != '\n' && c != ',') {
+                        *bufptr = c;
+                        bufptr++;
+                        continue;
+                    }
+                    type_name.s = buf + column_name.len + 1;
+                    type_name.len = bufptr - type_name.s;
+
+                    if (c == '\n') {
+                        state = DBREDIS_SCHEMA_VERSION_ST;
+                    } else {
+                        state = DBREDIS_SCHEMA_COLUMN_ST;
+                    }
+                    /*
+                    LM_DBG("found column type '%.*s' with len %d for column name '%.*s' in table '%.*s'\n",
+                            type_name.len, type_name.s,
+                            type_name.len,
+                            column_name.len, column_name.s,
+                            table_name.len, table_name.s);
+                    */
+                    column_entry = str_hash_get(&table->columns, column_name.s, column_name.len);
+                    if (column_entry) {
+                        LM_ERR("Found duplicate column definition '%.*s' in schema definition of table '%.*s', remove one from schema!\n",
+                                column_name.len, column_name.s,
+                                table_name.len, table_name.s);
+                        goto err;
+                    }
+                    column_entry = db_redis_create_column(&column_name, &type_name);
+                    if (!column_entry) {
+                        goto err;
+                    }
+                    str_hash_add(&table->columns, column_entry);
+                    memset(buf, 0, sizeof(buf));
+                    bufptr = buf;
+                    break;
+                case DBREDIS_SCHEMA_VERSION_ST:
+                    if (c != '\n' && c != EOF) {
+                        *bufptr = c;
+                        bufptr++;
+                        continue;
+                    }
+                    *bufptr = '\0';
+                    table->version = atoi(buf);
+                    state = DBREDIS_SCHEMA_END_ST;
+                    break;
+                case DBREDIS_SCHEMA_END_ST:
+                    goto fileend;
+                    break;
+            }
+        } while (c != EOF);
+
+fileend:
+        fclose(fin);
+        fin = NULL;
+    }
+
+
+    closedir(srcdir);
+    pkg_free(dir_name);
+
+    return 0;
+err:
+    if (fin)
+        fclose(fin);
+    if (srcdir)
+        closedir(srcdir);
+    if (dir_name)
+        pkg_free(dir_name);
+
+    db_redis_free_tables(con);
+    return -1;
+}
+
+int db_redis_keys_spec(char *spec) {
+    size_t len = strlen(spec);
+
+    if (redis_keys.len == 0) {
+        redis_keys.s = (char*)pkg_malloc(len * sizeof(char));
+        if (!redis_keys.s) {
+            LM_ERR("Failed to allocate memory for keys spec\n");
+            goto err;
+        }
+    } else {
+        redis_keys.s = (char*)pkg_realloc(redis_keys.s, redis_keys.len + 1 + len);
+        if (!redis_keys.s) {
+            LM_ERR("Failed to reallocate memory for keys spec\n");
+            goto err;
+        }
+        redis_keys.s[redis_keys.len] = ';';
+        redis_keys.len++;
+    }
+
+    strncpy(redis_keys.s + redis_keys.len, spec, len);
+    redis_keys.len += len;
+
+    return 0;
+
+err:
+    if (redis_keys.len) {
+        pkg_free(redis_keys.s);
+    }
+    return -1;
+}

+ 66 - 0
src/modules/db_redis/redis_table.h

@@ -0,0 +1,66 @@
+/**
+ * Copyright (C) 2018 Andreas Granig (sipwise.com)
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version
+ *
+ * Kamailio is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+
+#ifndef _REDIS_TABLE_H_
+#define _REDIS_TABLE_H_
+
+#include "db_redis_mod.h"
+#include "redis_connection.h"
+
+typedef struct redis_key redis_key_t;
+struct redis_key {
+    str key;
+    redis_key_t *next;
+};
+
+typedef struct redis_type redis_type_t;
+struct redis_type {
+    str type;
+    redis_type_t *next;
+    redis_key_t *keys;
+};
+
+typedef struct redis_table redis_table_t;
+struct redis_table {
+	int version;
+    redis_key_t *entry_keys;
+    redis_type_t *types;
+    struct str_hash_table columns;
+};
+
+int db_redis_schema_get_column_type(km_redis_con_t *con, const str *table_name, const str *col_name);
+void db_redis_print_all_tables(km_redis_con_t *con);
+void db_redis_print_table(km_redis_con_t *con, char *name);
+void db_redis_free_tables(km_redis_con_t *con);
+int db_redis_parse_schema(km_redis_con_t *con);
+int db_redis_parse_keys(km_redis_con_t *con);
+
+int db_redis_key_add_string(redis_key_t* *list, const char* entry, int len);
+int db_redis_key_add_str(redis_key_t **list, const str* entry);
+int db_redis_key_prepend_string(redis_key_t **list, const char* entry, int len);
+int db_redis_key_list2arr(redis_key_t *list, char ***arr);
+redis_key_t * db_redis_key_unshift(redis_key_t **list);
+void db_redis_key_free(redis_key_t **list);
+
+int db_redis_keys_spec(char *spec);
+
+#endif /* _REDIS_TABLE_H_ */

+ 2 - 0
utils/kamctl/db_redis/kamailio/acc

@@ -0,0 +1,2 @@
+id/int,method/string,from_tag/string,to_tag/string,callid/string,sip_code/string,sip_reason/string,time/int,
+5

+ 2 - 0
utils/kamctl/db_redis/kamailio/acc_cdrs

@@ -0,0 +1,2 @@
+id/int,start_time/int,end_time/int,duration/double,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/active_watchers

@@ -0,0 +1,2 @@
+id/int,presentity_uri/string,watcher_username/string,watcher_domain/string,to_user/string,to_domain/string,event/string,event_id/string,to_tag/string,from_tag/string,callid/string,local_cseq/int,remote_cseq/int,contact/string,record_route/string,expires/int,status/int,reason/string,version/int,socket_info/string,local_contact/string,from_user/string,from_domain/string,updated/int,updated_winfo/int,flags/int,user_agent/string,
+12

+ 2 - 0
utils/kamctl/db_redis/kamailio/address

@@ -0,0 +1,2 @@
+id/int,grp/int,ip_addr/string,mask/int,port/int,tag/string,
+6

+ 2 - 0
utils/kamctl/db_redis/kamailio/aliases

@@ -0,0 +1,2 @@
+id/int,ruid/string,username/string,domain/string,contact/string,received/string,path/string,expires/int,q/double,callid/string,cseq/int,last_modified/int,flags/int,cflags/int,user_agent/string,socket/string,methods/int,instance/string,reg_id/int,server_id/int,connection_id/int,keepalive/int,partition/int,
+8

+ 2 - 0
utils/kamctl/db_redis/kamailio/carrier_name

@@ -0,0 +1,2 @@
+id/int,carrier/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/carrierfailureroute

@@ -0,0 +1,2 @@
+id/int,carrier/int,domain/int,scan_prefix/string,host_name/string,reply_code/string,flags/int,mask/int,next_domain/int,description/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/carrierroute

@@ -0,0 +1,2 @@
+id/int,carrier/int,domain/int,scan_prefix/string,flags/int,mask/int,prob/double,strip/int,rewrite_host/string,rewrite_prefix/string,rewrite_suffix/string,description/string,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/cpl

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,cpl_xml/string,cpl_bin/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/dbaliases

@@ -0,0 +1,2 @@
+id/int,alias_username/string,alias_domain/string,username/string,domain/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/dialog

@@ -0,0 +1,2 @@
+id/int,hash_entry/int,hash_id/int,callid/string,from_uri/string,from_tag/string,to_uri/string,to_tag/string,caller_cseq/string,callee_cseq/string,caller_route_set/string,callee_route_set/string,caller_contact/string,callee_contact/string,caller_sock/string,callee_sock/string,state/int,start_time/int,timeout/int,sflags/int,iflags/int,toroute_name/string,req_uri/string,xdata/string,
+7

+ 2 - 0
utils/kamctl/db_redis/kamailio/dialog_vars

@@ -0,0 +1,2 @@
+id/int,hash_entry/int,hash_id/int,dialog_key/string,dialog_value/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/dialplan

@@ -0,0 +1,2 @@
+id/int,dpid/int,pr/int,match_op/int,match_exp/string,match_len/int,subst_exp/string,repl_exp/string,attrs/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/dispatcher

@@ -0,0 +1,2 @@
+id/int,setid/int,destination/string,flags/int,priority/int,attrs/string,description/string,
+4

+ 2 - 0
utils/kamctl/db_redis/kamailio/domain

@@ -0,0 +1,2 @@
+id/int,domain/string,did/string,last_modified/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/domain_attrs

@@ -0,0 +1,2 @@
+id/int,did/string,name/string,type/int,value/string,last_modified/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/domain_name

@@ -0,0 +1,2 @@
+id/int,domain/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/domainpolicy

@@ -0,0 +1,2 @@
+id/int,rule/string,type/string,att/string,val/string,description/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/dr_gateways

@@ -0,0 +1,2 @@
+gwid/int,type/int,address/string,strip/int,pri_prefix/string,attrs/string,description/string,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/dr_groups

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,groupid/int,description/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/dr_gw_lists

@@ -0,0 +1,2 @@
+id/int,gwlist/string,description/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/dr_rules

@@ -0,0 +1,2 @@
+ruleid/int,groupid/string,prefix/string,timerec/string,priority/int,routeid/string,gwlist/string,description/string,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/globalblacklist

@@ -0,0 +1,2 @@
+id/int,prefix/string,whitelist/int,description/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/grp

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,grp/string,last_modified/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/htable

@@ -0,0 +1,2 @@
+id/int,key_name/string,key_type/int,value_type/int,key_value/string,expires/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/imc_members

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,room/string,flag/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/imc_rooms

@@ -0,0 +1,2 @@
+id/int,name/string,domain/string,flag/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/lcr_gw

@@ -0,0 +1,2 @@
+id/int,lcr_id/int,gw_name/string,ip_addr/string,hostname/string,port/int,params/string,uri_scheme/int,transport/int,strip/int,prefix/string,tag/string,flags/int,defunct/int,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/lcr_rule

@@ -0,0 +1,2 @@
+id/int,lcr_id/int,prefix/string,from_uri/string,request_uri/string,stopper/int,enabled/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/lcr_rule_target

@@ -0,0 +1,2 @@
+id/int,lcr_id/int,rule_id/int,gw_id/int,priority/int,weight/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/location

@@ -0,0 +1,2 @@
+id/int,ruid/string,username/string,domain/string,contact/string,received/string,path/string,expires/int,q/double,callid/string,cseq/int,last_modified/int,flags/int,cflags/int,user_agent/string,socket/string,methods/int,instance/string,reg_id/int,server_id/int,connection_id/int,keepalive/int,partition/int,
+8

+ 2 - 0
utils/kamctl/db_redis/kamailio/location_attrs

@@ -0,0 +1,2 @@
+id/int,ruid/string,username/string,domain/string,aname/string,atype/int,avalue/string,last_modified/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/matrix

@@ -0,0 +1,2 @@
+first/int,second/int,res/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/missed_calls

@@ -0,0 +1,2 @@
+id/int,method/string,from_tag/string,to_tag/string,callid/string,sip_code/string,sip_reason/string,time/int,
+4

+ 2 - 0
utils/kamctl/db_redis/kamailio/mohqcalls

@@ -0,0 +1,2 @@
+id/int,mohq_id/int,call_id/string,call_status/int,call_from/string,call_contact/string,call_time/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/mohqueues

@@ -0,0 +1,2 @@
+id/int,name/string,uri/string,mohdir/string,mohfile/string,debug/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/mtree

@@ -0,0 +1,2 @@
+id/int,tprefix/string,tvalue/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/mtrees

@@ -0,0 +1,2 @@
+id/int,tname/string,tprefix/string,tvalue/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/pdt

@@ -0,0 +1,2 @@
+id/int,sdomain/string,prefix/string,domain/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/pl_pipes

@@ -0,0 +1,2 @@
+id/int,pipeid/string,algorithm/string,plimit/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/presentity

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,event/string,etag/string,expires/int,received_time/int,body/string,sender/string,priority/int,
+4

+ 2 - 0
utils/kamctl/db_redis/kamailio/pua

@@ -0,0 +1,2 @@
+id/int,pres_uri/string,pres_id/string,event/int,expires/int,desired_expires/int,flag/int,etag/string,tuple_id/string,watcher_uri/string,call_id/string,to_tag/string,from_tag/string,cseq/int,record_route/string,contact/string,remote_contact/string,version/int,extra_headers/string,
+7

+ 2 - 0
utils/kamctl/db_redis/kamailio/purplemap

@@ -0,0 +1,2 @@
+id/int,sip_user/string,ext_user/string,ext_prot/string,ext_pass/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/re_grp

@@ -0,0 +1,2 @@
+id/int,reg_exp/string,group_id/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/rls_presentity

@@ -0,0 +1,2 @@
+id/int,rlsubs_did/string,resource_uri/string,content_type/string,presence_state/string,expires/int,updated/int,auth_state/int,reason/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/rls_watchers

@@ -0,0 +1,2 @@
+id/int,presentity_uri/string,to_user/string,to_domain/string,watcher_username/string,watcher_domain/string,event/string,event_id/string,to_tag/string,from_tag/string,callid/string,local_cseq/int,remote_cseq/int,contact/string,record_route/string,expires/int,status/int,reason/string,version/int,socket_info/string,local_contact/string,from_user/string,from_domain/string,updated/int,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/rtpengine

@@ -0,0 +1,2 @@
+setid/int,url/string,weight/int,disabled/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/rtpproxy

@@ -0,0 +1,2 @@
+id/int,setid/string,url/string,flags/int,weight/int,description/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/sca_subscriptions

@@ -0,0 +1,2 @@
+id/int,subscriber/string,aor/string,event/int,expires/int,state/int,app_idx/int,call_id/string,from_tag/string,to_tag/string,record_route/string,notify_cseq/int,subscribe_cseq/int,server_id/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/silo

@@ -0,0 +1,2 @@
+id/int,src_addr/string,dst_addr/string,username/string,domain/string,inc_time/int,exp_time/int,snd_time/int,ctype/string,body/string,extra_hdrs/string,callid/string,status/int,
+8

+ 2 - 0
utils/kamctl/db_redis/kamailio/sip_trace

@@ -0,0 +1,2 @@
+id/int,time_stamp/int,time_us/int,callid/string,traced_user/string,msg/string,method/string,status/string,fromip/string,toip/string,fromtag/string,totag/string,direction/string,
+4

+ 2 - 0
utils/kamctl/db_redis/kamailio/speed_dial

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,sd_username/string,sd_domain/string,new_uri/string,fname/string,lname/string,description/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/subscriber

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,password/string,email_address/string,ha1/string,ha1b/string,rpid/string,
+6

+ 2 - 0
utils/kamctl/db_redis/kamailio/topos_d

@@ -0,0 +1,2 @@
+id/int,rectime/int,s_method/string,s_cseq/string,a_callid/string,a_uuid/string,b_uuid/string,a_contact/string,b_contact/string,as_contact/string,bs_contact/string,a_tag/string,b_tag/string,a_rr/string,b_rr/string,s_rr/string,iflags/int,a_uri/string,b_uri/string,r_uri/string,a_srcaddr/string,b_srcaddr/string,a_socket/string,b_socket/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/topos_t

@@ -0,0 +1,2 @@
+id/int,rectime/int,s_method/string,s_cseq/string,a_callid/string,a_uuid/string,b_uuid/string,direction/int,x_via/string,x_vbranch/string,x_rr/string,y_rr/string,s_rr/string,x_uri/string,a_contact/string,b_contact/string,as_contact/string,bs_contact/string,x_tag/string,a_tag/string,b_tag/string,a_srcaddr/string,b_srcaddr/string,a_socket/string,b_socket/string,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/trusted

@@ -0,0 +1,2 @@
+id/int,src_ip/string,proto/string,from_pattern/string,ruri_pattern/string,tag/string,priority/int,
+6

+ 2 - 0
utils/kamctl/db_redis/kamailio/uacreg

@@ -0,0 +1,2 @@
+id/int,l_uuid/string,l_username/string,l_domain/string,r_username/string,r_domain/string,realm/string,auth_username/string,auth_password/string,auth_proxy/string,expires/int,flags/int,reg_delay/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_credentials

@@ -0,0 +1,2 @@
+id/int,auth_username/string,did/string,realm/string,password/string,flags/int,ha1/string,ha1b/string,uid/string,
+7

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_domain

@@ -0,0 +1,2 @@
+id/int,did/string,domain/string,flags/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_domain_attrs

@@ -0,0 +1,2 @@
+id/int,did/string,name/string,type/int,value/string,flags/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_global_attrs

@@ -0,0 +1,2 @@
+id/int,name/string,type/int,value/string,flags/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_uri

@@ -0,0 +1,2 @@
+id/int,uid/string,did/string,username/string,flags/int,scheme/string,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_uri_attrs

@@ -0,0 +1,2 @@
+id/int,username/string,did/string,name/string,value/string,type/int,flags/int,scheme/string,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/uid_user_attrs

@@ -0,0 +1,2 @@
+id/int,uid/string,name/string,value/string,type/int,flags/int,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/uri

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,uri_user/string,last_modified/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/userblacklist

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,prefix/string,whitelist/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/usr_preferences

@@ -0,0 +1,2 @@
+id/int,uuid/string,username/string,domain/string,attribute/string,type/int,value/string,last_modified/int,
+2

+ 2 - 0
utils/kamctl/db_redis/kamailio/version

@@ -0,0 +1,2 @@
+table_name/string,table_version/int,
+1

+ 2 - 0
utils/kamctl/db_redis/kamailio/watchers

@@ -0,0 +1,2 @@
+id/int,presentity_uri/string,watcher_username/string,watcher_domain/string,event/string,status/int,reason/string,inserted_time/int,
+3

+ 2 - 0
utils/kamctl/db_redis/kamailio/xcap

@@ -0,0 +1,2 @@
+id/int,username/string,domain/string,doc/string,doc_type/int,etag/string,source/int,doc_uri/string,port/int,
+4