Integrate functionality tested on svncommunity users back into trunk
authorTilghman Lesher <tilghman@meg.abyt.es>
Thu, 28 Dec 2006 20:13:00 +0000 (20:13 +0000)
committerTilghman Lesher <tilghman@meg.abyt.es>
Thu, 28 Dec 2006 20:13:00 +0000 (20:13 +0000)
git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@49030 65c4cc65-6c06-0410-ace0-fbb531ad65f3

configs/func_odbc.conf.sample
funcs/func_odbc.c
funcs/func_strings.c

index c9f9d5d..2405952 100644 (file)
 ; If you have data which may potentially contain single ticks, you may wish
 ; to use the dialplan function SQL_ESC() to escape the data prior to its
 ; inclusion in the SQL statement.
+;
+;
+; The following variables are available in this configuration file:
+;
+; readhandle   A comma-separated list of DSNs (from res_odbc.conf) to use when
+;              executing the readsql statement.  Each DSN is tried, in
+;              succession, until the statement succeeds.  You may specify up to
+;              5 DSNs per function class.  If not specified, it will default to
+;              the value of writehandle or dsn, if specified.
+; writehandle  A comma-separated list of DSNs (from res_odbc.conf) to use when
+;              executing the writesql statement.  The same rules apply as to
+;              readhandle.  "dsn" is a synonym for "writehandle".
+; readsql      The statement to execute when reading from the function class.
+; writesql     The statement to execute when writing to the function class.
+; prefix       Normally, all function classes are prefixed with "ODBC" to keep
+;              them uniquely named.  You may choose to change this prefix, which
+;              may be useful to segregate a collection of certain function
+;              classes from others.
+; escapecommas This option may be used to turn off the default behavior of
+;              escaping commas which occur within a field.  If commas are
+;              escaped (the default behavior), then fields containing commas
+;              will be treated as a single value when assigning to ARRAY() or
+;              HASH().  If commas are not escaped, then values will be separated
+;              at the comma within fields.  Please note that turning this option
+;              off is incompatible with the functionality of HASH().
 
 
 ; ODBC_SQL - Allow an SQL statement to be built entirely in the dialplan
 [SQL]
 dsn=mysql1
-read=${ARG1}
+readsql=${ARG1}
 
 ; ODBC_ANTIGF - A blacklist.
 [ANTIGF]
-dsn=mysql1
-read=SELECT COUNT(*) FROM exgirlfriends WHERE callerid='${SQL_ESC(${ARG1})}'
+dsn=mysql1,mysql2   ; Use mysql1 as the primary handle, but fall back to mysql2
+                    ; if mysql1 is down.  Supports up to 5 comma-separated
+                    ; DSNs.  "dsn" may also be specified as "readhandle" and
+                    ; "writehandle", if it is important to separate reads and
+                    ; writes to different databases.
+readsql=SELECT COUNT(*) FROM exgirlfriends WHERE callerid='${SQL_ESC(${ARG1})}'
 
 ; ODBC_PRESENCE - Retrieve and update presence
 [PRESENCE]
 dsn=mysql1
-read=SELECT location FROM presence WHERE id='${SQL_ESC(${ARG1})}'
-write=UPDATE presence SET location='${SQL_ESC(${VAL1})}' WHERE id='${SQL_ESC(${ARG1})}'
-;prefix=OFFICE         ; Changes this function from ODBC_PRESENCE to OFFICE_PRESENCE
-;escapecommas=no       ; Normally, commas within a field are escaped such that each
-                       ; field may be separated into individual variables with ARRAY.
-                       ; This option turns that behavior off [default=yes].
+readsql=SELECT location FROM presence WHERE id='${SQL_ESC(${ARG1})}'
+writesql=UPDATE presence SET location='${SQL_ESC(${VAL1})}' WHERE id='${SQL_ESC(${ARG1})}'
+;prefix=OFFICE      ; Changes this function from ODBC_PRESENCE to OFFICE_PRESENCE
+;escapecommas=no    ; Normally, commas within a field are escaped such that each
+                    ; field may be separated into individual variables with ARRAY.
+                    ; This option turns that behavior off [default=yes].
 
index 529a342..bc50451 100644 (file)
@@ -37,6 +37,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include <stdlib.h>
 #include <unistd.h>
 #include <string.h>
+#include <errno.h>
 
 #include "asterisk/module.h"
 #include "asterisk/file.h"
@@ -57,7 +58,8 @@ enum {
 
 struct acf_odbc_query {
        AST_LIST_ENTRY(acf_odbc_query) list;
-       char dsn[30];
+       char readhandle[5][30];
+       char writehandle[5][30];
        char sql_read[2048];
        char sql_write[2048];
        unsigned int flags;
@@ -96,7 +98,7 @@ static int acf_odbc_write(struct ast_channel *chan, char *cmd, char *s, const ch
        struct odbc_obj *obj;
        struct acf_odbc_query *query;
        char *t, buf[2048]="", varname[15];
-       int i;
+       int i, dsn;
        AST_DECLARE_APP_ARGS(values,
                AST_APP_ARG(field)[100];
        );
@@ -119,14 +121,6 @@ static int acf_odbc_write(struct ast_channel *chan, char *cmd, char *s, const ch
                return -1;
        }
 
-       obj = ast_odbc_request_obj(query->dsn, 0);
-
-       if (!obj) {
-               ast_log(LOG_ERROR, "No database handle available with the name of '%s' (check res_odbc.conf)\n", query->dsn);
-               AST_LIST_UNLOCK(&queries);
-               return -1;
-       }
-
        /* Parse our arguments */
        t = value ? ast_strdupa(value) : "";
 
@@ -169,7 +163,15 @@ static int acf_odbc_write(struct ast_channel *chan, char *cmd, char *s, const ch
 
        AST_LIST_UNLOCK(&queries);
 
-       stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, buf);
+       for (dsn = 0; dsn < 5; dsn++) {
+               if (!ast_strlen_zero(query->writehandle[dsn])) {
+                       obj = ast_odbc_request_obj(query->writehandle[dsn], 0);
+                       if (obj)
+                               stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, buf);
+               }
+               if (stmt)
+                       break;
+       }
 
        if (stmt) {
                /* Rows affected */
@@ -195,14 +197,15 @@ static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf
 {
        struct odbc_obj *obj;
        struct acf_odbc_query *query;
-       char sql[2048] = "", varname[15];
-       int res, x, buflen = 0, escapecommas;
+       char sql[2048] = "", varname[15], colnames[2048] = "";
+       int res, x, buflen = 0, escapecommas, dsn;
        AST_DECLARE_APP_ARGS(args,
                AST_APP_ARG(field)[100];
        );
        SQLHSTMT stmt;
        SQLSMALLINT colcount=0;
        SQLLEN indicator;
+       SQLSMALLINT collength;
 
        AST_LIST_LOCK(&queries);
        AST_LIST_TRAVERSE(&queries, query, list) {
@@ -217,14 +220,6 @@ static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf
                return -1;
        }
 
-       obj = ast_odbc_request_obj(query->dsn, 0);
-
-       if (!obj) {
-               ast_log(LOG_ERROR, "No such DSN registered (or out of connections): %s (check res_odbc.conf)\n", query->dsn);
-               AST_LIST_UNLOCK(&queries);
-               return -1;
-       }
-
        AST_STANDARD_APP_ARGS(args, s);
        for (x = 0; x < args.argc; x++) {
                snprintf(varname, sizeof(varname), "ARG%d", x + 1);
@@ -244,10 +239,20 @@ static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf
 
        AST_LIST_UNLOCK(&queries);
 
-       stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, sql);
+       for (dsn = 0; dsn < 5; dsn++) {
+               if (!ast_strlen_zero(query->writehandle[dsn])) {
+                       obj = ast_odbc_request_obj(query->writehandle[dsn], 0);
+                       if (obj)
+                               stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, sql);
+               }
+               if (stmt)
+                       break;
+       }
 
        if (!stmt) {
-               ast_odbc_release_obj(obj);
+               ast_log(LOG_ERROR, "Unable to execute query [%s]\n", sql);
+               if (obj)
+                       ast_odbc_release_obj(obj);
                return -1;
        }
 
@@ -278,8 +283,33 @@ static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf
        }
 
        for (x = 0; x < colcount; x++) {
-               int i;
-               char coldata[256];
+               int i, namelen;
+               char coldata[256], colname[256];
+
+               res = SQLDescribeCol(stmt, x + 1, (unsigned char *)colname, sizeof(colname), &collength, NULL, NULL, NULL, NULL);
+               if (((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) || collength == 0) {
+                       snprintf(colname, sizeof(colname), "field%d", x);
+               }
+
+               if (!ast_strlen_zero(colnames))
+                       strncat(colnames, ",", sizeof(colnames) - 1);
+               namelen = strlen(colnames);
+
+               /* Copy data, encoding '\' and ',' for the argument parser */
+               for (i = 0; i < sizeof(colname); i++) {
+                       if (escapecommas && (colname[i] == '\\' || colname[i] == ',')) {
+                               colnames[namelen++] = '\\';
+                       }
+                       colnames[namelen++] = colname[i];
+
+                       if (namelen >= sizeof(colnames) - 2) {
+                               colnames[namelen >= sizeof(colnames) ? sizeof(colnames) - 1 : namelen] = '\0';
+                               break;
+                       }
+
+                       if (colname[i] == '\0')
+                               break;
+               }
 
                buflen = strlen(buf);
                res = SQLGetData(stmt, x + 1, SQL_CHAR, coldata, sizeof(coldata), &indicator);
@@ -315,6 +345,8 @@ static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf
        /* Trim trailing comma */
        buf[buflen - 1] = '\0';
 
+       pbx_builtin_setvar_helper(chan, "~ODBCFIELDS~", colnames);
+
        SQLFreeHandle(SQL_HANDLE_STMT, stmt);
        ast_odbc_release_obj(obj);
        return 0;
@@ -351,27 +383,68 @@ static struct ast_custom_function escape_function = {
 static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_query **query)
 {
        const char *tmp;
+       int i;
 
        if (!cfg || !catg) {
-               return -1;
+               return EINVAL;
        }
 
        *query = ast_calloc(1, sizeof(struct acf_odbc_query));
        if (! (*query))
-               return -1;
+               return ENOMEM;
+
+       if (((tmp = ast_variable_retrieve(cfg, catg, "writehandle"))) || ((tmp = ast_variable_retrieve(cfg, catg, "dsn")))) {
+               char *tmp2 = ast_strdupa(tmp);
+               AST_DECLARE_APP_ARGS(write,
+                       AST_APP_ARG(dsn)[5];
+               );
+               AST_NONSTANDARD_APP_ARGS(write, tmp2, ',');
+               for (i = 0; i < 5; i++) {
+                       if (!ast_strlen_zero(write.dsn[i]))
+                               ast_copy_string((*query)->writehandle[i], write.dsn[i], sizeof((*query)->writehandle[i]));
+               }
+       }
 
-       if ((tmp = ast_variable_retrieve(cfg, catg, "dsn"))) {
-               ast_copy_string((*query)->dsn, tmp, sizeof((*query)->dsn));
+       if ((tmp = ast_variable_retrieve(cfg, catg, "readhandle"))) {
+               char *tmp2 = ast_strdupa(tmp);
+               AST_DECLARE_APP_ARGS(read,
+                       AST_APP_ARG(dsn)[5];
+               );
+               AST_NONSTANDARD_APP_ARGS(read, tmp2, ',');
+               for (i = 0; i < 5; i++) {
+                       if (!ast_strlen_zero(read.dsn[i]))
+                               ast_copy_string((*query)->readhandle[i], read.dsn[i], sizeof((*query)->readhandle[i]));
+               }
        } else {
-               return -1;
-       }
+               /* If no separate readhandle, then use the writehandle for reading */
+               for (i = 0; i < 5; i++) {
+                       if (!ast_strlen_zero((*query)->writehandle[i]))
+                               ast_copy_string((*query)->readhandle[i], (*query)->writehandle[i], sizeof((*query)->readhandle[i]));
+               }
+       }
 
        if ((tmp = ast_variable_retrieve(cfg, catg, "read"))) {
+               ast_log(LOG_WARNING, "Parameter 'read' is deprecated for category %s.  Please use 'readsql' instead.\n", catg);
+               ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read));
+       } else if ((tmp = ast_variable_retrieve(cfg, catg, "readsql")))
                ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read));
+
+       if (!ast_strlen_zero((*query)->sql_read) && ast_strlen_zero((*query)->readhandle[0])) {
+               free(*query);
+               ast_log(LOG_ERROR, "There is SQL, but no ODBC class to be used for reading: %s\n", catg);
+               return EINVAL;
        }
 
        if ((tmp = ast_variable_retrieve(cfg, catg, "write"))) {
+               ast_log(LOG_WARNING, "Parameter 'write' is deprecated for category %s.  Please use 'writesql' instead.\n", catg);
                ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write));
+       } else if ((tmp = ast_variable_retrieve(cfg, catg, "writesql")))
+               ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write));
+
+       if (!ast_strlen_zero((*query)->sql_write) && ast_strlen_zero((*query)->writehandle[0])) {
+               free(*query);
+               ast_log(LOG_ERROR, "There is SQL, but no ODBC class to be used for writing: %s\n", catg);
+               return EINVAL;
        }
 
        /* Allow escaping of embedded commas in fields to be turned off */
@@ -384,7 +457,7 @@ static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_qu
        (*query)->acf = ast_calloc(1, sizeof(struct ast_custom_function));
        if (! (*query)->acf) {
                free(*query);
-               return -1;
+               return ENOMEM;
        }
 
        if ((tmp = ast_variable_retrieve(cfg, catg, "prefix")) && !ast_strlen_zero(tmp)) {
@@ -396,7 +469,7 @@ static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_qu
        if (!((*query)->acf->name)) {
                free((*query)->acf);
                free(*query);
-               return -1;
+               return ENOMEM;
        }
 
        asprintf((char **)&((*query)->acf->syntax), "%s(<arg1>[...[,<argN>]])", (*query)->acf->name);
@@ -405,7 +478,7 @@ static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_qu
                free((char *)(*query)->acf->name);
                free((*query)->acf);
                free(*query);
-               return -1;
+               return ENOMEM;
        }
 
        (*query)->acf->synopsis = "Runs the referenced query with the specified arguments";
@@ -432,15 +505,21 @@ static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_qu
                                        "${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n"
                                        "This function may only be set.\nSQL:\n%s\n",
                                        (*query)->sql_write);
+       } else {
+               free((char *)(*query)->acf->syntax);
+               free((char *)(*query)->acf->name);
+               free((*query)->acf);
+               free(*query);
+               ast_log(LOG_WARNING, "Section %s was found, but there was no SQL to execute.  Ignoring.\n", catg);
+               return EINVAL;
        }
 
-       /* Could be out of memory, or could be we have neither sql_read nor sql_write */
        if (! ((*query)->acf->desc)) {
                free((char *)(*query)->acf->syntax);
                free((char *)(*query)->acf->name);
                free((*query)->acf);
                free(*query);
-               return -1;
+               return ENOMEM;
        }
 
        if (ast_strlen_zero((*query)->sql_read)) {
@@ -475,7 +554,7 @@ static int free_acf_query(struct acf_odbc_query *query)
        return 0;
 }
 
-static int odbc_load_module(void)
+static int load_module(void)
 {
        int res = 0;
        struct ast_config *cfg;
@@ -494,10 +573,15 @@ static int odbc_load_module(void)
             catg;
             catg = ast_category_browse(cfg, catg)) {
                struct acf_odbc_query *query = NULL;
-
-               if (init_acf_query(cfg, catg, &query)) {
-                       ast_log(LOG_ERROR, "Out of memory\n");
-                       free_acf_query(query);
+               int err;
+
+               if ((err = init_acf_query(cfg, catg, &query))) {
+                       if (err == ENOMEM)
+                               ast_log(LOG_ERROR, "Out of memory\n");
+                       else if (err == EINVAL)
+                               ast_log(LOG_ERROR, "Invalid parameters for category %s\n", catg);
+                       else
+                               ast_log(LOG_ERROR, "%s (%d)\n", strerror(err), err);
                } else {
                        AST_LIST_INSERT_HEAD(&queries, query, list);
                        ast_custom_function_register(query->acf);
@@ -505,15 +589,16 @@ static int odbc_load_module(void)
        }
 
        ast_config_destroy(cfg);
-       ast_custom_function_register(&escape_function);
+       res |= ast_custom_function_register(&escape_function);
 
        AST_LIST_UNLOCK(&queries);
        return res;
 }
 
-static int odbc_unload_module(void)
+static int unload_module(void)
 {
        struct acf_odbc_query *query;
+       int res = 0;
 
        AST_LIST_LOCK(&queries);
        while (!AST_LIST_EMPTY(&queries)) {
@@ -522,10 +607,11 @@ static int odbc_unload_module(void)
                free_acf_query(query);
        }
 
-       ast_custom_function_unregister(&escape_function);
+       res |= ast_custom_function_unregister(&escape_function);
 
        /* Allow any threads waiting for this lock to pass (avoids a race) */
        AST_LIST_UNLOCK(&queries);
+       usleep(1);
        AST_LIST_LOCK(&queries);
 
        AST_LIST_UNLOCK(&queries);
@@ -572,16 +658,6 @@ reload_out:
        return res;
 }
 
-static int unload_module(void)
-{
-       return odbc_unload_module();
-}
-
-static int load_module(void)
-{
-       return odbc_load_module();
-}
-
 /* XXX need to revise usecount - set if query_lock is set */
 
 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "ODBC lookups",
index 47b7806..2b1bc17 100644 (file)
@@ -155,6 +155,37 @@ static struct ast_custom_function regex_function = {
        .read = regex,
 };
 
+#define HASH_PREFIX    "~HASH~%s~"
+#define HASH_FORMAT    HASH_PREFIX "%s~"
+
+static char *app_clearhash = "ClearHash";
+static char *syn_clearhash = "Clear the keys from a specified hashname";
+static char *desc_clearhash =
+"ClearHash(<hashname>)\n"
+"  Clears all keys out of the specified hashname\n";
+
+/* This function probably should migrate to main/pbx.c, as pbx_builtin_clearvar_prefix() */
+static void clearvar_prefix(struct ast_channel *chan, const char *prefix)
+{
+       struct ast_var_t *var;
+       int len = strlen(prefix);
+       AST_LIST_TRAVERSE_SAFE_BEGIN(&chan->varshead, var, entries) {
+               if (strncasecmp(prefix, ast_var_name(var), len) == 0) {
+                       AST_LIST_REMOVE_CURRENT(&chan->varshead, entries);
+                       free(var);
+               }
+       }
+       AST_LIST_TRAVERSE_SAFE_END
+}
+
+static int exec_clearhash(struct ast_channel *chan, void *data)
+{
+       char prefix[80];
+       snprintf(prefix, sizeof(prefix), HASH_PREFIX, data ? (char *)data : "null");
+       clearvar_prefix(chan, prefix);
+       return 0;
+}
+
 static int array(struct ast_channel *chan, char *cmd, char *var,
                 const char *value)
 {
@@ -164,13 +195,23 @@ static int array(struct ast_channel *chan, char *cmd, char *var,
        AST_DECLARE_APP_ARGS(arg2,
                             AST_APP_ARG(val)[100];
        );
-       char *value2;
-       int i;
+       char *origvar = "", *value2, varname[256];
+       int i, ishash = 0;
 
        value2 = ast_strdupa(value);
        if (!var || !value2)
                return -1;
 
+       if (!strcmp(cmd, "HASH")) {
+               const char *var2 = pbx_builtin_getvar_helper(chan, "~ODBCFIELDS~");
+               origvar = var;
+               if (var2)
+                       var = ast_strdupa(var2);
+               else
+                       return -1;
+               ishash = 1;
+       }
+
        /* The functions this will generally be used with are SORT and ODBC_*, which
         * both return comma-delimited lists.  However, if somebody uses literal lists,
         * their commas will be translated to vertical bars by the load, and I don't
@@ -194,17 +235,139 @@ static int array(struct ast_channel *chan, char *cmd, char *var,
                        ast_log(LOG_DEBUG, "array set value (%s=%s)\n", arg1.var[i],
                                arg2.val[i]);
                if (i < arg2.argc) {
-                       pbx_builtin_setvar_helper(chan, arg1.var[i], arg2.val[i]);
+                       if (ishash) {
+                               snprintf(varname, sizeof(varname), HASH_FORMAT, origvar, arg1.var[i]);
+                               pbx_builtin_setvar_helper(chan, varname, arg2.val[i]);
+                       } else {
+                               pbx_builtin_setvar_helper(chan, arg1.var[i], arg2.val[i]);
+                       }
                } else {
                        /* We could unset the variable, by passing a NULL, but due to
                         * pushvar semantics, that could create some undesired behavior. */
-                       pbx_builtin_setvar_helper(chan, arg1.var[i], "");
+                       if (ishash) {
+                               snprintf(varname, sizeof(varname), HASH_FORMAT, origvar, arg1.var[i]);
+                               pbx_builtin_setvar_helper(chan, varname, "");
+                       } else {
+                               pbx_builtin_setvar_helper(chan, arg1.var[i], "");
+                       }
                }
        }
 
        return 0;
 }
 
+static int hashkeys_read(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len)
+{
+       struct ast_var_t *newvar;
+       int plen;
+       char prefix[80];
+       snprintf(prefix, sizeof(prefix), HASH_PREFIX, data);
+       plen = strlen(prefix);
+
+       memset(buf, 0, len);
+       AST_LIST_TRAVERSE(&chan->varshead, newvar, entries) {
+               if (strncasecmp(prefix, ast_var_name(newvar), plen) == 0) {
+                       /* Copy everything after the prefix */
+                       strncat(buf, ast_var_name(newvar) + plen, len);
+                       /* Trim the trailing ~ */
+                       buf[strlen(buf) - 1] = ',';
+               }
+       }
+       /* Trim the trailing comma */
+       buf[strlen(buf) - 1] = '\0';
+       return 0;
+}
+
+static int hash_write(struct ast_channel *chan, char *cmd, char *var, const char *value)
+{
+       char varname[256];
+       AST_DECLARE_APP_ARGS(arg,
+               AST_APP_ARG(hashname);
+               AST_APP_ARG(hashkey);
+       );
+
+       if (!strchr(var, '|')) {
+               /* Single argument version */
+               return array(chan, "HASH", var, value);
+       }
+
+       AST_STANDARD_APP_ARGS(arg, var);
+       snprintf(varname, sizeof(varname), HASH_FORMAT, arg.hashname, arg.hashkey);
+       pbx_builtin_setvar_helper(chan, varname, value);
+
+       return 0;
+}
+
+static int hash_read(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len)
+{
+       char varname[256];
+       const char *varvalue;
+       AST_DECLARE_APP_ARGS(arg,
+               AST_APP_ARG(hashname);
+               AST_APP_ARG(hashkey);
+       );
+
+       AST_STANDARD_APP_ARGS(arg, data);
+       if (arg.argc == 2) {
+               snprintf(varname, sizeof(varname), HASH_FORMAT, arg.hashname, arg.hashkey);
+               varvalue = pbx_builtin_getvar_helper(chan, varname);
+               if (varvalue)
+                       ast_copy_string(buf, varvalue, len);
+               else
+                       *buf = '\0';
+       } else if (arg.argc == 1) {
+               char colnames[4096];
+               int i;
+               AST_DECLARE_APP_ARGS(arg2,
+                       AST_APP_ARG(col)[100];
+               );
+
+               /* Get column names, in no particular order */
+               hashkeys_read(chan, "HASHKEYS", arg.hashname, colnames, sizeof(colnames));
+               pbx_builtin_setvar_helper(chan, "~ODBCFIELDS~", colnames);
+
+               AST_NONSTANDARD_APP_ARGS(arg2, colnames, ',');
+               *buf = '\0';
+
+               /* Now get the corresponding column values, in exactly the same order */
+               for (i = 0; i < arg2.argc; i++) {
+                       snprintf(varname, sizeof(varname), HASH_FORMAT, arg.hashname, arg2.col[i]);
+                       varvalue = pbx_builtin_getvar_helper(chan, varname);
+                       strncat(buf, varvalue, len);
+                       strncat(buf, ",", len);
+               }
+
+               /* Strip trailing comma */
+               buf[strlen(buf) - 1] = '\0';
+       }
+
+       return 0;
+}
+
+static struct ast_custom_function hash_function = {
+       .name = "HASH",
+       .synopsis = "Implementation of a dialplan associative array",
+       .syntax = "HASH(hashname[|hashkey])",
+       .write = hash_write,
+       .read = hash_read,
+       .desc =
+               "In two argument mode, gets and sets values to corresponding keys within a named\n"
+               "associative array.  The single-argument mode will only work when assigned to from\n"
+               "a function defined by func_odbc.so.\n",
+};
+
+static struct ast_custom_function hashkeys_function = {
+       .name = "HASHKEYS",
+       .synopsis = "Retrieve the keys of a HASH()",
+       .syntax = "HASHKEYS(<hashname>)",
+       .read = hashkeys_read,
+       .desc =
+               "Returns a comma-delimited list of the current keys of an associative array\n"
+               "defined by the HASH() function.  Note that if you iterate over the keys of\n"
+               "the result, adding keys during iteration will cause the result of the HASHKEYS\n"
+               "function to change.\n",
+};
+
 static struct ast_custom_function array_function = {
        .name = "ARRAY",
        .synopsis = "Allows setting multiple variables at once",
@@ -589,6 +752,9 @@ static int unload_module(void)
        res |= ast_custom_function_unregister(&eval_function);
        res |= ast_custom_function_unregister(&keypadhash_function);
        res |= ast_custom_function_unregister(&sprintf_function);
+       res |= ast_custom_function_unregister(&hashkeys_function);
+       res |= ast_custom_function_unregister(&hash_function);
+       res |= ast_unregister_application(app_clearhash);
 
        return res;
 }
@@ -608,6 +774,9 @@ static int load_module(void)
        res |= ast_custom_function_register(&eval_function);
        res |= ast_custom_function_register(&keypadhash_function);
        res |= ast_custom_function_register(&sprintf_function);
+       res |= ast_custom_function_register(&hashkeys_function);
+       res |= ast_custom_function_register(&hash_function);
+       res |= ast_register_application(app_clearhash, exec_clearhash, syn_clearhash, desc_clearhash);
 
        return res;
 }