Fix output delimiters and add prefix parameter to func_odbc #7025 (Corydon76)
[asterisk/asterisk.git] / funcs / func_odbc.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (c) 2005, 2006 Tilghman Lesher
5  *
6  * Tilghman Lesher <func_odbc__200508@the-tilghman.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*!
20  * \file
21  *
22  * \brief ODBC lookups
23  *
24  * \author Tilghman Lesher <func_odbc__200508@the-tilghman.com>
25  */
26
27 /*** MODULEINFO
28         <depend>unixodbc</depend>
29  ***/
30
31 #include <sys/types.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <unistd.h>
35 #include <string.h>
36
37 #include "asterisk.h"
38
39 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
40
41 #include "asterisk/module.h"
42 #include "asterisk/file.h"
43 #include "asterisk/logger.h"
44 #include "asterisk/options.h"
45 #include "asterisk/channel.h"
46 #include "asterisk/pbx.h"
47 #include "asterisk/module.h"
48 #include "asterisk/config.h"
49 #include "asterisk/res_odbc.h"
50 #include "asterisk/app.h"
51
52 static char *tdesc = "ODBC lookups";
53
54 static char *config = "func_odbc.conf";
55
56 struct acf_odbc_query {
57         AST_LIST_ENTRY(acf_odbc_query) list;
58         char dsn[30];
59         char sql_read[2048];
60         char sql_write[2048];
61         struct ast_custom_function *acf;
62 };
63
64 AST_LIST_HEAD_STATIC(queries, acf_odbc_query);
65
66 #ifdef NEEDTRACE
67 static void acf_odbc_error(SQLHSTMT stmt, int res)
68 {
69         char state[10] = "", diagnostic[256] = "";
70         SQLINTEGER nativeerror = 0;
71         SQLSMALLINT diagbytes = 0;
72         SQLGetDiagRec(SQL_HANDLE_STMT, stmt, 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes);
73         ast_log(LOG_WARNING, "SQL return value %d: error %s: %s (len %d)\n", res, state, diagnostic, diagbytes);
74 }
75 #endif
76
77 /*
78  * Master control routine
79  */
80 static int acf_odbc_write(struct ast_channel *chan, char *cmd, char *s, const char *value)
81 {
82         struct odbc_obj *obj;
83         struct acf_odbc_query *query;
84         char *t, *arg, buf[2048]="", varname[15];
85         int res, argcount=0, valcount=0, i, retry=0;
86         struct ast_channel *ast;
87         SQLHSTMT stmt;
88         SQLINTEGER nativeerror=0, numfields=0, rows=0;
89         SQLSMALLINT diagbytes=0;
90         unsigned char state[10], diagnostic[256];
91 #ifdef NEEDTRACE
92         SQLINTEGER enable = 1;
93         char *tracefile = "/tmp/odbc.trace";
94 #endif
95
96         AST_LIST_LOCK(&queries);
97         AST_LIST_TRAVERSE(&queries, query, list) {
98                 if (!strcmp(query->acf->name, cmd)) {
99                         break;
100                 }
101         }
102
103         if (!query) {
104                 ast_log(LOG_ERROR, "No such function '%s'\n", cmd);
105                 AST_LIST_UNLOCK(&queries);
106                 return -1;
107         }
108
109         obj = odbc_request_obj(query->dsn, 0);
110
111         if (!obj) {
112                 ast_log(LOG_ERROR, "No such DSN registered (or out of connections): %s (check res_odbc.conf)\n", query->dsn);
113                 AST_LIST_UNLOCK(&queries);
114                 return -1;
115         }
116
117         /* Parse our arguments */
118         t = value ? ast_strdupa(value) : "";
119
120         if (!s || !t) {
121                 ast_log(LOG_ERROR, "Out of memory\n");
122                 AST_LIST_UNLOCK(&queries);
123                 return -1;
124         }
125
126         /* XXX You might be tempted to change this section into using
127          * pbx_builtin_pushvar_helper().  However, note that if you try
128          * to set a NULL (like for VALUE), then nothing gets set, and the
129          * value doesn't get masked out.  Even worse, when you subsequently
130          * try to remove the value you just set, you'll wind up unsetting
131          * the previous value (which is wholly undesireable).  Hence, this
132          * has to remain the way it is done here. XXX
133          */
134
135         /* Save old arguments as variables in a fake channel */
136         ast = ast_channel_alloc(0);
137         while ((arg = strsep(&s, "|"))) {
138                 argcount++;
139                 snprintf(varname, sizeof(varname), "ARG%d", argcount);
140                 pbx_builtin_setvar_helper(ast, varname, pbx_builtin_getvar_helper(chan, varname));
141                 pbx_builtin_setvar_helper(chan, varname, arg);
142         }
143
144         /* Parse values, just like arguments */
145         while ((arg = strsep(&t, "|"))) {
146                 valcount++;
147                 snprintf(varname, sizeof(varname), "VAL%d", valcount);
148                 pbx_builtin_setvar_helper(ast, varname, pbx_builtin_getvar_helper(chan, varname));
149                 pbx_builtin_setvar_helper(chan, varname, arg);
150         }
151
152         /* Additionally set the value as a whole */
153         /* Note that pbx_builtin_setvar_helper will quite happily take a NULL for the 3rd argument */
154         pbx_builtin_setvar_helper(ast, "VALUE", pbx_builtin_getvar_helper(chan, "VALUE"));
155         pbx_builtin_setvar_helper(chan, "VALUE", value);
156
157         pbx_substitute_variables_helper(chan, query->sql_write, buf, sizeof(buf) - 1);
158
159         /* Restore prior values */
160         for (i=1; i<=argcount; i++) {
161                 snprintf(varname, sizeof(varname), "ARG%d", argcount);
162                 pbx_builtin_setvar_helper(chan, varname, pbx_builtin_getvar_helper(ast, varname));
163         }
164
165         for (i=1; i<=valcount; i++) {
166                 snprintf(varname, sizeof(varname), "VAL%d", argcount);
167                 pbx_builtin_setvar_helper(chan, varname, pbx_builtin_getvar_helper(ast, varname));
168         }
169         pbx_builtin_setvar_helper(chan, "VALUE", pbx_builtin_getvar_helper(ast, "VALUE"));
170
171         ast_channel_free(ast);
172         AST_LIST_UNLOCK(&queries);
173
174 retry_write:
175 #ifdef NEEDTRACE
176         SQLSetConnectAttr(obj->con, SQL_ATTR_TRACE, &enable, SQL_IS_INTEGER);
177         SQLSetConnectAttr(obj->con, SQL_ATTR_TRACEFILE, tracefile, strlen(tracefile));
178 #endif
179
180         res = SQLAllocHandle (SQL_HANDLE_STMT, obj->con, &stmt);
181         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
182                 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
183                 pbx_builtin_setvar_helper(chan, "ODBCROWS", "-1");
184                 return -1;
185         }
186
187         res = SQLPrepare(stmt, (unsigned char *)buf, SQL_NTS);
188         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
189                 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", buf);
190                 SQLFreeHandle (SQL_HANDLE_STMT, stmt);
191                 pbx_builtin_setvar_helper(chan, "ODBCROWS", "-1");
192                 return -1;
193         }
194
195         res = SQLExecute(stmt);
196         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
197                 if (res == SQL_ERROR) {
198                         SQLGetDiagField(SQL_HANDLE_STMT, stmt, 1, SQL_DIAG_NUMBER, &numfields, SQL_IS_INTEGER, &diagbytes);
199                         for (i = 0; i <= numfields; i++) {
200                                 SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i + 1, state, &nativeerror, diagnostic, sizeof(diagnostic), &diagbytes);
201                                 ast_log(LOG_WARNING, "SQL Execute returned an error %d: %s: %s (%d)\n", res, state, diagnostic, diagbytes);
202                                 if (i > 10) {
203                                         ast_log(LOG_WARNING, "Oh, that was good.  There are really %d diagnostics?\n", (int)numfields);
204                                         break;
205                                 }
206                         }
207                 }
208                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
209                 odbc_release_obj(obj);
210                 /* All handles are now invalid (after a disconnect), so we gotta redo all handles */
211                 obj = odbc_request_obj("asterisk", 1);
212                 if (!retry) {
213                         retry = 1;
214                         goto retry_write;
215                 }
216                 rows = -1;
217         } else {
218                 /* Rows affected */
219                 SQLRowCount(stmt, &rows);
220         }
221
222         /* Output the affected rows, for all cases.  In the event of failure, we
223          * flag this as -1 rows.  Note that this is different from 0 affected rows
224          * which would be the case if we succeeded in our query, but the values did
225          * not change. */
226         snprintf(varname, sizeof(varname), "%d", (int)rows);
227         pbx_builtin_setvar_helper(chan, "ODBCROWS", varname);
228
229         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
230                 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", buf);
231         }
232
233         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
234
235         return 0;
236 }
237
238 static int acf_odbc_read(struct ast_channel *chan, char *cmd, char *s, char *buf, size_t len)
239 {
240         struct odbc_obj *obj;
241         struct acf_odbc_query *query;
242         char *arg, sql[2048] = "", varname[15];
243         int count=0, res, x, buflen = 0;
244         SQLHSTMT stmt;
245         SQLSMALLINT colcount=0;
246         SQLINTEGER indicator;
247 #ifdef NEEDTRACE
248         SQLINTEGER enable = 1;
249         char *tracefile = "/tmp/odbc.trace";
250 #endif
251
252         AST_LIST_LOCK(&queries);
253         AST_LIST_TRAVERSE(&queries, query, list) {
254                 if (!strcmp(query->acf->name, cmd)) {
255                         break;
256                 }
257         }
258
259         if (!query) {
260                 ast_log(LOG_ERROR, "No such function '%s'\n", cmd);
261                 AST_LIST_UNLOCK(&queries);
262                 return -1;
263         }
264
265         obj = odbc_request_obj(query->dsn, 0);
266
267         if (!obj) {
268                 ast_log(LOG_ERROR, "No such DSN registered (or out of connections): %s (check res_odbc.conf)\n", query->dsn);
269                 AST_LIST_UNLOCK(&queries);
270                 return -1;
271         }
272
273 #ifdef NEEDTRACE
274         SQLSetConnectAttr(obj->con, SQL_ATTR_TRACE, &enable, SQL_IS_INTEGER);
275         SQLSetConnectAttr(obj->con, SQL_ATTR_TRACEFILE, tracefile, strlen(tracefile));
276 #endif
277
278         while ((arg = strsep(&s, "|"))) {
279                 count++;
280                 snprintf(varname, sizeof(varname), "ARG%d", count);
281                 /* arg is by definition non-NULL, so this works, here */
282                 pbx_builtin_pushvar_helper(chan, varname, arg);
283         }
284
285         pbx_substitute_variables_helper(chan, query->sql_read, sql, sizeof(sql) - 1);
286
287         /* Restore prior values */
288         for (x = 1; x <= count; x++) {
289                 snprintf(varname, sizeof(varname), "ARG%d", x);
290                 pbx_builtin_setvar_helper(chan, varname, NULL);
291         }
292
293         AST_LIST_UNLOCK(&queries);
294
295         res = SQLAllocHandle (SQL_HANDLE_STMT, obj->con, &stmt);
296         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
297                 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
298                 return -1;
299         }
300
301         res = SQLPrepare(stmt, (unsigned char *)sql, SQL_NTS);
302         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
303                 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", sql);
304                 SQLFreeHandle (SQL_HANDLE_STMT, stmt);
305                 return -1;
306         }
307
308         res = odbc_smart_execute(obj, stmt);
309         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
310                 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql);
311                 SQLFreeHandle (SQL_HANDLE_STMT, stmt);
312                 return -1;
313         }
314
315         res = SQLNumResultCols(stmt, &colcount);
316         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
317                 ast_log(LOG_WARNING, "SQL Column Count error!\n[%s]\n\n", sql);
318                 SQLFreeHandle (SQL_HANDLE_STMT, stmt);
319                 return -1;
320         }
321
322         *buf = '\0';
323
324         res = SQLFetch(stmt);
325         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
326                 if (res == SQL_NO_DATA) {
327                         if (option_verbose > 3) {
328                                 ast_verbose(VERBOSE_PREFIX_4 "Found no rows [%s]\n", sql);
329                         }
330                 } else if (option_verbose > 3) {
331                         ast_log(LOG_WARNING, "Error %d in FETCH [%s]\n", res, sql);
332                 }
333                 goto acf_out;
334         }
335
336         for (x = 0; x < colcount; x++) {
337                 int i;
338                 char coldata[256];
339
340                 buflen = strlen(buf);
341                 res = SQLGetData(stmt, x + 1, SQL_CHAR, coldata, sizeof(coldata), &indicator);
342                 if (indicator == SQL_NULL_DATA) {
343                         coldata[0] = '\0';
344                         res = SQL_SUCCESS;
345                 }
346
347                 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
348                         ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql);
349                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
350                         return -1;
351                 }
352
353                 /* Copy data, encoding '\' and ',' for the argument parser */
354                 for (i = 0; i < sizeof(coldata); i++) {
355                         if (coldata[i] == '\\' || coldata[i] == ',') {
356                                 buf[buflen++] = '\\';
357                         }
358                         buf[buflen++] = coldata[i];
359
360                         if (buflen >= len - 2)
361                                 break;
362
363                         if (coldata[i] == '\0')
364                                 break;
365                 }
366
367                 buf[buflen - 1] = ',';
368         }
369         /* Trim trailing comma */
370         buf[buflen - 1] = '\0';
371
372 acf_out:
373         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
374         return 0;
375 }
376
377 static int acf_escape(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len)
378 {
379         char *out = buf;
380
381         for (; *data && out - buf < len; data++) {
382                 if (*data == '\'') {
383                         *out = '\'';
384                         out++;
385                 }
386                 *out++ = *data;
387         }
388         *out = '\0';
389
390         return 0;
391 }
392
393 static struct ast_custom_function escape_function = {
394         .name = "SQL_ESC",
395         .synopsis = "Escapes single ticks for use in SQL statements",
396         .syntax = "SQL_ESC(<string>)",
397         .desc =
398 "Used in SQL templates to escape data which may contain single ticks (') which\n"
399 "are otherwise used to delimit data.  For example:\n"
400 "SELECT foo FROM bar WHERE baz='${SQL_ESC(${ARG1})}'\n",
401         .read = acf_escape,
402         .write = NULL,
403 };
404
405 static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_query **query)
406 {
407         char *tmp;
408
409         if (!cfg || !catg) {
410                 return -1;
411         }
412
413         *query = ast_calloc(1, sizeof(struct acf_odbc_query));
414         if (! (*query))
415                 return -1;
416
417         if ((tmp = ast_variable_retrieve(cfg, catg, "dsn"))) {
418                 ast_copy_string((*query)->dsn, tmp, sizeof((*query)->dsn));
419         } else {
420                 return -1;
421         }
422
423         if ((tmp = ast_variable_retrieve(cfg, catg, "read"))) {
424                 ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read));
425         }
426
427         if ((tmp = ast_variable_retrieve(cfg, catg, "write"))) {
428                 ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write));
429         }
430
431         (*query)->acf = ast_calloc(1, sizeof(struct ast_custom_function));
432         if (! (*query)->acf) {
433                 free(*query);
434                 return -1;
435         }
436
437         if ((tmp = ast_variable_retrieve(cfg, catg, "prefix")) && !ast_strlen_zero(tmp)) {
438                 asprintf((char **)&((*query)->acf->name), "%s_%s", tmp, catg);
439         } else {
440                 asprintf((char **)&((*query)->acf->name), "ODBC_%s", catg);
441         }
442
443         if (!((*query)->acf->name)) {
444                 free((*query)->acf);
445                 free(*query);
446                 return -1;
447         }
448
449         asprintf((char **)&((*query)->acf->syntax), "%s(<arg1>[...[,<argN>]])", (*query)->acf->name);
450
451         if (!((*query)->acf->syntax)) {
452                 free((char *)(*query)->acf->name);
453                 free((*query)->acf);
454                 free(*query);
455                 return -1;
456         }
457
458         (*query)->acf->synopsis = "Runs the referenced query with the specified arguments";
459         if (!ast_strlen_zero((*query)->sql_read) && !ast_strlen_zero((*query)->sql_write)) {
460                 asprintf((char **)&((*query)->acf->desc),
461                                         "Runs the following query, as defined in func_odbc.conf, performing\n"
462                                         "substitution of the arguments into the query as specified by ${ARG1},\n"
463                                         "${ARG2}, ... ${ARGn}.  When setting the function, the values are provided\n"
464                                         "either in whole as ${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n"
465                                         "\nRead:\n%s\n\nWrite:\n%s\n",
466                                         (*query)->sql_read,
467                                         (*query)->sql_write);
468         } else if (!ast_strlen_zero((*query)->sql_read)) {
469                 asprintf((char **)&((*query)->acf->desc),
470                                         "Runs the following query, as defined in func_odbc.conf, performing\n"
471                                         "substitution of the arguments into the query as specified by ${ARG1},\n"
472                                         "${ARG2}, ... ${ARGn}.  This function may only be read, not set.\n\nSQL:\n%s\n",
473                                         (*query)->sql_read);
474         } else if (!ast_strlen_zero((*query)->sql_write)) {
475                 asprintf((char **)&((*query)->acf->desc),
476                                         "Runs the following query, as defined in func_odbc.conf, performing\n"
477                                         "substitution of the arguments into the query as specified by ${ARG1},\n"
478                                         "${ARG2}, ... ${ARGn}.  The values are provided either in whole as\n"
479                                         "${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n"
480                                         "This function may only be set.\nSQL:\n%s\n",
481                                         (*query)->sql_write);
482         }
483
484         /* Could be out of memory, or could be we have neither sql_read nor sql_write */
485         if (! ((*query)->acf->desc)) {
486                 free((char *)(*query)->acf->syntax);
487                 free((char *)(*query)->acf->name);
488                 free((*query)->acf);
489                 free(*query);
490                 return -1;
491         }
492
493         if (ast_strlen_zero((*query)->sql_read)) {
494                 (*query)->acf->read = NULL;
495         } else {
496                 (*query)->acf->read = acf_odbc_read;
497         }
498
499         if (ast_strlen_zero((*query)->sql_write)) {
500                 (*query)->acf->write = NULL;
501         } else {
502                 (*query)->acf->write = acf_odbc_write;
503         }
504
505         return 0;
506 }
507
508 static int free_acf_query(struct acf_odbc_query *query)
509 {
510         if (query) {
511                 if (query->acf) {
512                         if (query->acf->name)
513                                 free((char *)query->acf->name);
514                         if (query->acf->syntax)
515                                 free((char *)query->acf->syntax);
516                         if (query->acf->desc)
517                                 free((char *)query->acf->desc);
518                         free(query->acf);
519                 }
520                 free(query);
521         }
522         return 0;
523 }
524
525 static int odbc_load_module(void)
526 {
527         int res = 0;
528         struct ast_config *cfg;
529         char *catg;
530
531         AST_LIST_LOCK(&queries);
532
533         cfg = ast_config_load(config);
534         if (!cfg) {
535                 ast_log(LOG_WARNING, "Unable to load config for func_odbc: %s\n", config);
536                 AST_LIST_UNLOCK(&queries);
537                 return -1;
538         }
539
540         for (catg = ast_category_browse(cfg, NULL);
541              catg;
542              catg = ast_category_browse(cfg, catg)) {
543                 struct acf_odbc_query *query = NULL;
544
545                 if (init_acf_query(cfg, catg, &query)) {
546                         ast_log(LOG_ERROR, "Out of memory\n");
547                         free_acf_query(query);
548                 } else {
549                         AST_LIST_INSERT_HEAD(&queries, query, list);
550                         ast_custom_function_register(query->acf);
551                 }
552         }
553
554         ast_config_destroy(cfg);
555         ast_custom_function_register(&escape_function);
556
557         AST_LIST_UNLOCK(&queries);
558         return res;
559 }
560
561 static int odbc_unload_module(void)
562 {
563         struct acf_odbc_query *query;
564
565         AST_LIST_LOCK(&queries);
566         while (!AST_LIST_EMPTY(&queries)) {
567                 query = AST_LIST_REMOVE_HEAD(&queries, list);
568                 ast_custom_function_unregister(query->acf);
569                 free_acf_query(query);
570         }
571
572         ast_custom_function_unregister(&escape_function);
573
574         /* Allow any threads waiting for this lock to pass (avoids a race) */
575         AST_LIST_UNLOCK(&queries);
576         AST_LIST_LOCK(&queries);
577
578         AST_LIST_UNLOCK(&queries);
579         return 0;
580 }
581
582 static int reload(void *mod)
583 {
584         int res = 0;
585         struct ast_config *cfg;
586         struct acf_odbc_query *oldquery;
587         char *catg;
588
589         AST_LIST_LOCK(&queries);
590
591         while (!AST_LIST_EMPTY(&queries)) {
592                 oldquery = AST_LIST_REMOVE_HEAD(&queries, list);
593                 ast_custom_function_unregister(oldquery->acf);
594                 free_acf_query(oldquery);
595         }
596
597         cfg = ast_config_load(config);
598         if (!cfg) {
599                 ast_log(LOG_WARNING, "Unable to load config for func_odbc: %s\n", config);
600                 goto reload_out;
601         }
602
603         for (catg = ast_category_browse(cfg, NULL);
604              catg;
605              catg = ast_category_browse(cfg, catg)) {
606                 struct acf_odbc_query *query = NULL;
607
608                 if (init_acf_query(cfg, catg, &query)) {
609                         ast_log(LOG_ERROR, "Cannot initialize query %s\n", catg);
610                 } else {
611                         AST_LIST_INSERT_HEAD(&queries, query, list);
612                         ast_custom_function_register(query->acf);
613                 }
614         }
615
616         ast_config_destroy(cfg);
617 reload_out:
618         AST_LIST_UNLOCK(&queries);
619         return res;
620 }
621
622 static int unload_module(void *mod)
623 {
624         return odbc_unload_module();
625 }
626
627 static int load_module(void *mod)
628 {
629         return odbc_load_module();
630 }
631
632 static const char *description(void)
633 {
634         return tdesc;
635 }
636
637 /* XXX need to revise usecount - set if query_lock is set */
638
639 static const char *key(void)
640 {
641         return ASTERISK_GPL_KEY;
642 }
643
644 STD_MOD(MOD_1, reload, NULL, NULL);
645