You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kamailio/modules/db_postgres/pg_cmd.c

578 lines
15 KiB

/*
* $Id$
*
* PostgreSQL Database Driver for SER
*
* Portions Copyright (C) 2001-2003 FhG FOKUS
* Copyright (C) 2003 August.Net Services, LLC
* Portions Copyright (C) 2005-2008 iptelorg GmbH
*
* This file is part of SER, a free SIP server.
*
* SER 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
*
* For a license to use the ser software under conditions other than those
* described here, or to purchase support for this software, please contact
* iptel.org by e-mail at the following addresses: info@iptel.org
*
* SER 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., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/** \addtogroup postgres
* @{
*/
/** \file
* Implementation of functions related to database commands.
*/
#include "pg_cmd.h"
#include "pg_sql.h"
#include "pg_fld.h"
#include "pg_con.h"
#include "pg_mod.h"
#include "pg_uri.h"
#include "pg_res.h"
#include "../../mem/mem.h"
#include "../../dprint.h"
#include "../../ut.h"
#include <string.h>
/** A global counter used to generate unique command names.
* This variable implements a global counter which is used to generate unique
* names for server-side commands.
*/
static int server_query_no = 0;
static int upload_cmd(db_cmd_t* cmd);
static void free_pg_params(struct pg_params* cmd);
/** Destroys a pg_cmd structure.
* This function frees all memory used by pg_cmd structure.
* @param cmd A pointer to generic db_cmd command being freed.
* @param payload A pointer to pg_cmd structure to be freed.
*/
static void pg_cmd_free(db_cmd_t* cmd, struct pg_cmd* payload)
{
db_drv_free(&payload->gen);
if (payload->sql_cmd.s) pkg_free(payload->sql_cmd.s);
free_pg_params(&payload->params);
if (payload->name) pkg_free(payload->name);
if (payload->types) PQclear(payload->types);
pkg_free(payload);
}
/** Generate a unique name for a server-side PostgreSQL command.
* This function generates a unique name for each command that will be used to
* identify the prepared statement on the server. The name has only has to be
* unique within a connection to the server so we just keep a global counter
* and the name will be that number converted to text.
*
* @param cmd A command whose name is to be generated
* @return A string allocated using pkg_malloc containing the name or NULL on
* error.
*/
static int gen_cmd_name(db_cmd_t* cmd)
{
struct pg_cmd* pcmd;
char* c;
int len;
pcmd = DB_GET_PAYLOAD(cmd);
c = int2str(server_query_no, &len);
pcmd->name = pkg_malloc(len + 1);
if (pcmd->name == NULL) {
ERR("postgres: No memory left\n");
return -1;
}
memcpy(pcmd->name, c, len);
pcmd->name[len] = '\0';
server_query_no++;
return 0;
}
/** Creates parameter data structures for PQexecPrepared.
* This function creates auxiliary data structures that will be used to pass
* parameter value and types to PQexecPrepared. The function only allocates
* memory buffers and determines oids of parameters, actual values will be
* assigned by another function at runtime.
* @param cmd A command where the data strctures will be created.
* @retval 0 on success.
* @retval A negative number on error.
*/
static int create_pg_params(db_cmd_t* cmd)
{
int num;
struct pg_cmd* pcmd;
pcmd = DB_GET_PAYLOAD(cmd);
num = cmd->match_count + cmd->vals_count;
if (num == 0) return 0;
pcmd->params.val = (const char**)pkg_malloc(sizeof(const char*) * num);
pcmd->params.len = (int*)pkg_malloc(sizeof(int) * num);
pcmd->params.fmt = (int*)pkg_malloc(sizeof(int) * num);
if (!pcmd->params.val ||
!pcmd->params.len || !pcmd->params.fmt) {
ERR("postgres: No memory left\n");
goto error;
}
memset(pcmd->params.val, '\0', sizeof(const char*) * num);
memset(pcmd->params.len, '\0', sizeof(int) * num);
memset(pcmd->params.fmt, '\0', sizeof(int) * num);
pcmd->params.n = num;
return 0;
error:
free_pg_params(&pcmd->params);
return -1;
}
/**
* Free all memory used for PQexecParam parameters. That is
* the arrays of Oids, values, lengths, and formats supplied
* to PostgreSQL client API functions like PQexecParams.
*/
static void free_pg_params(struct pg_params* params)
{
if (params == NULL) return;
if (params->val) pkg_free(params->val);
params->val = NULL;
if (params->len) pkg_free(params->len);
params->len = NULL;
if (params->fmt) pkg_free(params->fmt);
params->fmt = NULL;
}
/** Verify field type compatibility.
* This function verifies the types of all parameters of a database command
* with the types of corresponding fields on the server to make sure that they
* can be converted.
* @param cmd A command structure whose parameters are to be checked.
* @retval 0 on success.
* @retval A negative number if at least one field type does not match.
* @todo Store oid and length as part of pg_fld, instead of the arrays used
* as parameters to PQ functions
*/
static int check_types(db_cmd_t* cmd)
{
struct pg_con* pcon;
/* FIXME: The function should take the connection as one of parameters */
pcon = DB_GET_PAYLOAD(cmd->ctx->con[db_payload_idx]);
if (pg_check_fld2pg(cmd->match, pcon->oid)) return -1;
if (pg_check_fld2pg(cmd->vals, pcon->oid)) return -1;
if (pg_check_pg2fld(cmd->result, pcon->oid)) return -1;
return 0;
}
static int get_types(db_cmd_t* cmd)
{
struct pg_cmd* pcmd;
struct pg_con* pcon;
int i, n;
pg_type_t *types;
pcmd = DB_GET_PAYLOAD(cmd);
/* FIXME */
pcon = DB_GET_PAYLOAD(cmd->ctx->con[db_payload_idx]);
types = pcon->oid;
pcmd->types = PQdescribePrepared(pcon->con, pcmd->name);
if (PQresultStatus(pcmd->types) != PGRES_COMMAND_OK) {
ERR("postgres: Error while obtaining description of prepared statement\n");
return -1;
}
/* adapted from check_result() in db_mysql */
n = PQnfields(pcmd->types);
if (cmd->result == NULL) {
/* The result set parameter of db_cmd function was empty, that
* means the command is select * and we have to create the array
* of result fields in the cmd structure manually.
*/
cmd->result = db_fld(n + 1);
cmd->result_count = n;
for(i = 0; i < cmd->result_count; i++) {
struct pg_fld *f;
if (pg_fld(cmd->result + i, cmd->table.s) < 0) goto error;
f = DB_GET_PAYLOAD(cmd->result + i);
f->name = pkg_malloc(strlen(PQfname(pcmd->types, i))+1);
if (f->name == NULL) {
ERR("postgres: Out of private memory\n");
goto error;
}
strcpy(f->name, PQfname(pcmd->types, i));
cmd->result[i].name = f->name;
}
} else {
if (cmd->result_count != n) {
BUG("postgres: Number of fields in PQresult does not match number of parameters in DB API\n");
goto error;
}
}
/* Now iterate through all the columns in the result set and replace
* any occurrence of DB_UNKNOWN type with the type of the column
* retrieved from the database and if no column name was provided then
* update it from the database as well.
*/
for(i = 0; i < cmd->result_count; i++) {
Oid type = PQftype(pcmd->types, i);
if (cmd->result[i].type != DB_NONE) continue;
if ((type == types[PG_INT2].oid) || (type == types[PG_INT4].oid) || (type == types[PG_INT8].oid))
cmd->result[i].type = DB_INT;
else if (type == types[PG_FLOAT4].oid)
cmd->result[i].type = DB_FLOAT;
else if (type == types[PG_FLOAT8].oid)
cmd->result[i].type = DB_DOUBLE;
else if ((type == types[PG_TIMESTAMP].oid) || (type == types[PG_TIMESTAMPTZ].oid))
cmd->result[i].type = DB_DATETIME;
else if ((type == types[PG_VARCHAR].oid) || (type == types[PG_CHAR].oid) || (type == types[PG_TEXT].oid))
cmd->result[i].type = DB_STR;
else if ((type == types[PG_BIT].oid) || (type == types[PG_VARBIT].oid))
cmd->result[i].type = DB_BITMAP;
else if (type == types[PG_BYTE].oid)
cmd->result[i].type = DB_BLOB;
else
{
ERR("postgres: Unsupported PostgreSQL column type: %d, table: %s, column: %s\n",
type, cmd->table.s, PQfname(pcmd->types, i));
goto error;
}
}
return 0;
error:
return -1;
}
int pg_cmd(db_cmd_t* cmd)
{
struct pg_cmd* pcmd;
pcmd = (struct pg_cmd*)pkg_malloc(sizeof(struct pg_cmd));
if (pcmd == NULL) {
ERR("postgres: No memory left\n");
goto error;
}
memset(pcmd, '\0', sizeof(struct pg_cmd));
if (db_drv_init(&pcmd->gen, pg_cmd_free) < 0) goto error;
switch(cmd->type) {
case DB_PUT:
if (build_insert_sql(&pcmd->sql_cmd, cmd) < 0) goto error;
break;
case DB_DEL:
if (build_delete_sql(&pcmd->sql_cmd, cmd) < 0) goto error;
break;
case DB_GET:
if (build_select_sql(&pcmd->sql_cmd, cmd) < 0) goto error;
break;
case DB_UPD:
if (build_update_sql(&pcmd->sql_cmd, cmd) < 0) goto error;
break;
case DB_SQL:
pcmd->sql_cmd.s = (char*)pkg_malloc(cmd->table.len + 1);
if (pcmd->sql_cmd.s == NULL) {
ERR("postgres: Out of private memory\n");
goto error;
}
memcpy(pcmd->sql_cmd.s,cmd->table.s, cmd->table.len);
pcmd->sql_cmd.s[cmd->table.len] = '\0';
pcmd->sql_cmd.len = cmd->table.len;
break;
}
DB_SET_PAYLOAD(cmd, pcmd);
/* Create parameter arrays for PostgreSQL API functions */
if (create_pg_params(cmd) < 0) goto error;
/* Generate a unique name for the command on the server */
if (gen_cmd_name(cmd) != 0) goto error;
/* Upload the command to the server */
if (upload_cmd(cmd) != 0) goto error;
/* Obtain the description of the uploaded command, this includes
* information about result and parameter fields */
if (get_types(cmd) != 0) goto error;
/* Update fields based on the information retrieved from the */
if (pg_resolve_param_oids(cmd->vals, cmd->match,
cmd->vals_count, cmd->match_count,
pcmd->types))
goto error;
if (pg_resolve_result_oids(cmd->result, cmd->result_count, pcmd->types))
goto error;
if (check_types(cmd)) goto error;
return 0;
error:
if (pcmd) {
DB_SET_PAYLOAD(cmd, NULL);
free_pg_params(&pcmd->params);
if (pcmd->types) PQclear(pcmd->types);
if (pcmd->name) pkg_free(pcmd->name);
if (pcmd->sql_cmd.s) pkg_free(pcmd->sql_cmd.s);
db_drv_free(&pcmd->gen);
pkg_free(pcmd);
}
return -1;
}
int pg_getopt(db_cmd_t* cmd, char* optname, va_list ap)
{
long long* id;
if (!strcasecmp("last_id", optname)) {
id = va_arg(ap, long long*);
if (id == NULL) {
BUG("postgres: NULL pointer passed to 'last_id' option\n");
goto error;
}
return -1;
} else {
return 1;
}
return 0;
error:
return -1;
}
int pg_setopt(db_cmd_t* cmd, char* optname, va_list ap)
{
return 1;
}
/** Uploads a database command to PostgreSQL server.
* This function uploads a pre-compiled database command to PostgreSQL
* server using PQprepare.
* @param cmd A database command
* @retval 0 on success.
* @retval A negative number on error.
*/
static int upload_cmd(db_cmd_t* cmd)
{
struct pg_cmd* pcmd;
struct pg_con* pcon;
PGresult* res;
int st;
pcmd = DB_GET_PAYLOAD(cmd);
/* FIXME: The function should take the connection as one of parameters */
pcon = DB_GET_PAYLOAD(cmd->ctx->con[db_payload_idx]);
DBG("postgres: Uploading command '%s': '%s'\n", pcmd->name,
pcmd->sql_cmd.s);
res = PQprepare(pcon->con, pcmd->name, pcmd->sql_cmd.s, (cmd->match_count + cmd->vals_count), NULL);
st = PQresultStatus(res);
if (st != PGRES_COMMAND_OK && st != PGRES_NONFATAL_ERROR &&
st != PGRES_TUPLES_OK) {
ERR("postgres: Error while uploading command to server: %d, %s",
st, PQresultErrorMessage(res));
ERR("postgres: Command: '%s'\n", pcmd->sql_cmd.s);
PQclear(res);
return -1;
}
PQclear(res);
return 0;
}
int pg_cmd_exec(db_res_t* res, db_cmd_t* cmd)
{
PGresult* tmp;
int i, err, stat;
db_con_t* con;
struct pg_cmd* pcmd;
struct pg_con* pcon;
struct pg_uri* puri;
struct pg_res* pres;
/* First things first: retrieve connection info from the currently active
* connection and also mysql payload from the database command
*/
con = cmd->ctx->con[db_payload_idx];
pcmd = DB_GET_PAYLOAD(cmd);
pcon = DB_GET_PAYLOAD(con);
puri = DB_GET_PAYLOAD(con->uri);
for(i = 0; i <= pg_retries; i++) {
/* Convert parameters from DB-API format to the format accepted
* by PostgreSQL */
if (pg_fld2pg(&pcmd->params, 0, pcon->oid, cmd->match, pcon->flags) != 0)
return 1;
if (pg_fld2pg(&pcmd->params, cmd->match_count,
pcon->oid, cmd->vals, pcon->flags) != 0) return 1;
/* Execute the statement */
tmp = PQexecPrepared(pcon->con, pcmd->name,
pcmd->params.n,
pcmd->params.val, pcmd->params.len,
pcmd->params.fmt, 1);
if (!tmp) {
ERR("postgres: PQexecPrepared returned no result\n");
continue;
}
switch(PQresultStatus(tmp)) {
case PGRES_COMMAND_OK:
case PGRES_NONFATAL_ERROR:
case PGRES_TUPLES_OK:
if (res) {
pres = DB_GET_PAYLOAD(res);
pres->res = tmp;
pres->rows = PQntuples(tmp);
} else {
PQclear(tmp);
}
return 0;
default:
break;
}
ERR("postgres: Command on server %s failed: %s: %s\n",
puri->host, PQresStatus(PQresultStatus(tmp)),
PQresultErrorMessage(tmp));
PQclear(tmp);
/* Command failed, first of all determine the status of the connection
* to server */
if (PQstatus(pcon->con) != CONNECTION_OK) {
INFO("postgres: Connection to server %s disconnected, attempting reconnect\n",
puri->host);
pg_con_disconnect(con);
if (pg_con_connect(con)) {
INFO("postgres: Failed to reconnect server %s, giving up\n",
puri->host);
return -1;
}
INFO("postgres: Successfully reconnected server on %s\n",
puri->host);
}
/* Connection is either connected or has been successfully reconnected,
* now figure out if the prepared command on the server still exist
*/
tmp = PQdescribePrepared(pcon->con, pcmd->name);
if (tmp == NULL) {
ERR("postgres: PQdescribePrepared returned no result\n");
continue;
}
stat = PQresultStatus(tmp);
PQclear(tmp);
switch (stat) {
case PGRES_COMMAND_OK:
case PGRES_NONFATAL_ERROR:
case PGRES_TUPLES_OK:
INFO("postgres: Command %s on server %s still exists, reusing\n",
pcmd->name, puri->host);
/* Command is there, retry */
continue;
default:
break;
}
/* Upload again */
INFO("postgres: Command %s on server %s missing, uploading\n",
pcmd->name, puri->host);
err = upload_cmd(cmd);
if (err < 0) {
continue;
} else if (err > 0) {
/* DB API error, this is a serious problem such
* as memory allocation failure, bail out
*/
return 1;
}
}
INFO("postgres: Failed to execute command %s on server %s, giving up\n",
pcmd->name, puri->host);
return -1;
}
int pg_cmd_first(db_res_t* res)
{
struct pg_res* pres;
pres = DB_GET_PAYLOAD(res);
if (pres->rows <= 0) return 1; /* Empty table */
pres->row = 0;
return pg_cmd_next(res);
}
int pg_cmd_next(db_res_t* res)
{
struct pg_res* pres;
struct pg_con* pcon;
pres = DB_GET_PAYLOAD(res);
pcon = DB_GET_PAYLOAD(res->cmd->ctx->con[db_payload_idx]);
if (pres->row >= pres->rows) return 1;
if (pg_pg2fld(res->cmd->result, pres->res, pres->row, pcon->oid, pcon->flags)) return -1;
res->cur_rec->fld = res->cmd->result;
pres->row++;
return 0;
}
/** @} */