 /*
  * fb.c
  * A module to access the Firebird database from Ruby.
  * Fork of interbase.c to fb.c by Brent Rowland.
  * All changes, improvements and associated bugs Copyright (C) 2006 Brent Rowland and Target Training International.
  * License to all changes, improvements and bugs is granted under the same terms as the original and/or
  * the Ruby license, whichever is most applicable.
  * Based on interbase.c
  *
  *                               Copyright (C) 1999 by NaCl inc.
  *                               Copyright (C) 1997,1998 by RIOS Corporation
  *
  * Permission to use, copy, modify, and distribute this software and its
  * documentation for any purpose and without fee is hereby granted, provided
  * that the above copyright notice appear in all copies.
  * RIOS Corporation makes no representations about the suitability of
  * this software for any purpose.  It is provided "as is" without express
  * or implied warranty.  By use of this software the user agrees to
  * indemnify and hold harmless RIOS Corporation from any  claims or
  * liability for loss arising out of such use.
  */

#include "ruby.h"
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <ibase.h>
#include <float.h>
#include <time.h>

#define	SQLDA_COLSINIT	50
#define	SQLCODE_NOMORE	100
#define	TPBBUFF_ALLOC	64
#define	CMND_DELIMIT	" \t\n\r\f"
#define	LIST_DELIMIT	", \t\n\r\f"
#define	META_NAME_MAX	31

/* Statement type */
#define	STATEMENT_DDL	1
#define	STATEMENT_DML	0

/* Execute process flag */
#define	EXECF_EXECDML	0
#define	EXECF_SETPARM	1

static VALUE rb_mFb;
static VALUE rb_cFbDatabase;
static VALUE rb_cFbConnection;
static VALUE rb_cFbCursor;
static VALUE rb_cFbSqlType;
static VALUE rb_cFbGlobal;
static VALUE rb_eFbError;
static VALUE rb_sFbField;
static VALUE rb_sFbIndex;

static VALUE rb_cDate;

static char isc_info_stmt[] = { isc_info_sql_stmt_type };
static char isc_info_buff[16];
static char isc_tpb_0[] = {
    isc_tpb_version1,		isc_tpb_write,
    isc_tpb_concurrency,	isc_tpb_nowait
};

/* structs */

/* DB handle and TR parameter block list structure */
typedef struct
{
	isc_db_handle	*dbb_ptr ;
	long		 tpb_len ;
	char		*tpb_ptr ;
} ISC_TEB ; /* transaction existence block */

/* InterBase varchar structure */
typedef	struct
{
	short vary_length;
	char  vary_string[1];
} VARY;

struct FbConnection {
	isc_db_handle db;		/* DB handle */
	isc_tr_handle transact; /* transaction handle */
	VALUE cursor;
	unsigned short dialect;
	unsigned short db_dialect;
	short downcase_names;
	int dropped;
	struct FbConnection *next;
};

static struct FbConnection *fb_connection_list;

struct FbCursor {
	int open;
	isc_tr_handle auto_transact;
	isc_stmt_handle stmt;
	XSQLDA *i_sqlda;
	XSQLDA *o_sqlda;
	char *i_buffer;
	long  i_buffer_size;
	char *o_buffer;
	long  o_buffer_size;
	VALUE fields_ary;
	VALUE fields_hash;
	VALUE connection;
};

typedef struct trans_opts
{
	char *option1;
	char *option2;
	char  optval;
	short position;
	struct trans_opts *sub_opts;
} trans_opts;

/* global data */
static isc_tr_handle global_transact = 0;	/* transaction handle */
static int connection_count = 0;

/* global utilities */

#define	ALIGN(n, b)	((n + b - 1) & ~(b - 1))
#define	UPPER(c)	(((c) >= 'a' && (c)<= 'z') ? (c) - 'a' + 'A' : (c))
#define	FREE(p)		if (p)	{ xfree(p); p = 0; }
#define	SETNULL(p)	if (p && strlen(p) == 0)	{ p = 0; }

static long calculate_buffsize(XSQLDA *sqlda)
{
	XSQLVAR *var;
	long cols;
	short dtp;
	long offset;
	long alignment;
	long length;
	long count;

	cols = sqlda->sqld;
	for (var = sqlda->sqlvar, offset = 0,count = 0; count < cols; var++,count++) {
		length = alignment = var->sqllen;
		dtp = var->sqltype & ~1;

		if (dtp == SQL_TEXT) {
			alignment = 1;
		} else if (dtp == SQL_VARYING) {
			length += sizeof(short);
			alignment = sizeof(short);
		}

		offset = ALIGN(offset, alignment);
		offset += length;
		offset = ALIGN(offset, sizeof(short));
		offset += sizeof(short);
	}

	return offset;
}

static VALUE fb_error_msg(long *isc_status)
{
	char msg[512];
	VALUE result = rb_str_new(NULL, 0);
	while (isc_interprete(msg, &isc_status))
	{
		result = rb_str_cat(result, msg, strlen(msg));
		result = rb_str_cat(result, "\n", strlen("\n"));
	}
	return result;
}

static VALUE fb_mktime(struct tm *tm)
{
	return rb_funcall(
		rb_cTime, rb_intern("utc"), 6,
		INT2FIX(tm->tm_year), INT2FIX(tm->tm_mon), INT2FIX(tm->tm_mday),
		INT2FIX(tm->tm_hour), INT2FIX(tm->tm_min), INT2FIX(tm->tm_sec));
}

static VALUE fb_mkdate(struct tm *tm)
{
	return rb_funcall(
		rb_cDate, rb_intern("civil"), 3,
		INT2FIX(1900 + tm->tm_year), INT2FIX(tm->tm_mon + 1), INT2FIX(tm->tm_mday));		
}

static int responds_like_date(VALUE obj)
{
	return rb_respond_to(obj, rb_intern("year")) && 
		rb_respond_to(obj, rb_intern("month")) && 
		rb_respond_to(obj, rb_intern("day"));
}
static void tm_from_date(struct tm *tm, VALUE date)
{
	VALUE year, month, day;

	if (!responds_like_date(date)) {
		VALUE s = rb_funcall(date, rb_intern("to_s"), 0);
		date = rb_funcall(rb_cDate, rb_intern("parse"), 1, s);
	}
	year = rb_funcall(date, rb_intern("year"), 0);
	month = rb_funcall(date, rb_intern("month"), 0);
	day = rb_funcall(date, rb_intern("day"), 0);
	memset(tm, 0, sizeof(struct tm));
	tm->tm_year = FIX2INT(year) - 1900;
	tm->tm_mon = FIX2INT(month) - 1;
	tm->tm_mday = FIX2INT(day);
}

static VALUE fb_sql_type_from_code(int code, int subtype)
{
	char *sql_type = NULL;
	switch(code) {
		case SQL_TEXT:
		case blr_text:
			sql_type = "CHAR";
			break;
		case SQL_VARYING:
		case blr_varying:
			sql_type = "VARCHAR";
			break;
		case SQL_SHORT:
		case blr_short:
			switch (subtype) {
				case 0:		sql_type = "SMALLINT";	break;
				case 1:		sql_type = "NUMERIC";	break;
				case 2:		sql_type = "DECIMAL";	break;
			}
			break;
		case SQL_LONG:
		case blr_long:
			switch (subtype) {
				case 0:		sql_type = "INTEGER";	break;
				case 1:		sql_type = "NUMERIC";	break;
				case 2:		sql_type = "DECIMAL";	break;
			}
			break;
		case SQL_FLOAT:
		case blr_float:
			sql_type = "FLOAT";
			break;
		case SQL_DOUBLE:
		case blr_double:
			switch (subtype) {
				case 0:		sql_type = "DOUBLE PRECISION"; break;
				case 1:		sql_type = "NUMERIC";	break;
				case 2:		sql_type = "DECIMAL";	break;
			}
			break;
		case SQL_D_FLOAT:
		case blr_d_float:
			sql_type = "DOUBLE PRECISION";
			break;
		case SQL_TIMESTAMP:
		case blr_timestamp:
			sql_type = "TIMESTAMP";
			break;
		case SQL_BLOB:
		case blr_blob:
			sql_type = "BLOB";
			break;
		case SQL_ARRAY:
			sql_type = "ARRAY";
			break;
		case SQL_QUAD:
		case blr_quad:
			sql_type = "DECIMAL";
			break;
		case SQL_TYPE_TIME:
		case blr_sql_time:
			sql_type = "TIME";
			break;
		case SQL_TYPE_DATE:
		case blr_sql_date:
			sql_type = "DATE";
			break;
		case SQL_INT64:
		case blr_int64:
			switch (subtype) {
				case 0:		sql_type = "BIGINT";	break;
				case 1:		sql_type = "NUMERIC";	break;
				case 2:		sql_type = "DECIMAL";	break;
			}
			break;
		default:
			printf("Unknown: %d, %d\n", code, subtype);
			sql_type = "UNKNOWN";
			break;
	}
	return rb_str_new2(sql_type);
}

static void fb_error_check(long *isc_status)
{
	short code = isc_sqlcode(isc_status);

	if (code != 0) {
		char buf[1024];
		VALUE exc, msg, msg1, msg2;

		isc_sql_interprete(code, buf, 1024);
		msg1 = rb_str_new2(buf);
		msg2 = fb_error_msg(isc_status);
		msg = rb_str_cat(msg1, "\n", strlen("\n"));
		msg = rb_str_concat(msg, msg2);

		exc = rb_exc_new3(rb_eFbError, msg);
		rb_iv_set(exc, "error_code", INT2FIX(code));
		rb_exc_raise(exc);
	}
}

static void fb_error_check_warn(long *isc_status)
{
	short code = isc_sqlcode(isc_status);
	if (code != 0) {
		char buf[1024];
		isc_sql_interprete(code, buf, 1024);
		rb_warning("%s(%d)", buf, code);
	}
}

static XSQLDA* sqlda_alloc(long cols)
{
	XSQLDA *sqlda;

	sqlda = (XSQLDA*)xmalloc(XSQLDA_LENGTH(cols));
#ifdef SQLDA_CURRENT_VERSION
	sqlda->version = SQLDA_CURRENT_VERSION;
#else
	sqlda->version = SQLDA_VERSION1;
#endif
	sqlda->sqln = cols;
	sqlda->sqld = 0;
	return sqlda;
}

static VALUE cursor_close _((VALUE));
static VALUE cursor_drop _((VALUE));
static VALUE cursor_execute _((int, VALUE*, VALUE));
static VALUE cursor_fetchall _((int, VALUE*, VALUE));

static void fb_cursor_mark();
static void fb_cursor_free();

/* connection utilities */
static void fb_connection_check(struct FbConnection *fb_connection)
{
	if (fb_connection->db == 0) {
		rb_raise(rb_eFbError, "closed db connection");
	}
}

static void global_close_cursors()
{
	struct FbConnection *list = fb_connection_list;
	int i;

	while (list) {
		for (i = 0; i < RARRAY(list->cursor)->len; i++) {
			cursor_close(RARRAY(list->cursor)->ptr[i]);
		}
		list = list->next;
	}
}

static void fb_connection_close_cursors(struct FbConnection *fb_connection)
{
	int i;

	for (i = 0; i < RARRAY(fb_connection->cursor)->len; i++) {
		cursor_close(RARRAY(fb_connection->cursor)->ptr[i]);
	}
}

static void fb_connection_drop_cursors(struct FbConnection *fb_connection)
{
	int i;

	for (i = 0; i < RARRAY(fb_connection->cursor)->len; i++) {
		cursor_drop(RARRAY(fb_connection->cursor)->ptr[i]);
	}
	RARRAY(fb_connection->cursor)->len = 0;
}

static void fb_connection_remove(struct FbConnection *fb_connection)
{
	if (fb_connection_list != NULL) {
		if (fb_connection_list == fb_connection) {
			fb_connection_list = fb_connection_list->next;
		} else {
			struct FbConnection *list = fb_connection_list;
			while (list->next) {
				if (list->next == fb_connection) {
					list->next = fb_connection->next;
					break;
				}
				list = list->next;
			}
		}
		fb_connection->db = 0;
		connection_count--;
	}
}

static void fb_connection_disconnect(struct FbConnection *fb_connection)
{
	long isc_status[20];

	if (fb_connection->transact) {
		isc_commit_transaction(isc_status, &fb_connection->transact);
		fb_error_check(isc_status);
	}
	if (fb_connection->dropped) {
		isc_drop_database(isc_status, &fb_connection->db);
	} else {
		isc_detach_database(isc_status, &fb_connection->db);
	}
	fb_error_check(isc_status);
	fb_connection_remove(fb_connection);
}

static void fb_connection_disconnect_warn(struct FbConnection *fb_connection)
{
	long isc_status[20];

	if (fb_connection->transact) {
		isc_commit_transaction(isc_status, &fb_connection->transact);
		fb_error_check_warn(isc_status);
	}
	isc_detach_database(isc_status, &fb_connection->db);
	fb_error_check_warn(isc_status);
	fb_connection_remove(fb_connection);
}

static void fb_connection_mark(struct FbConnection *fb_connection)
{
	rb_gc_mark(fb_connection->cursor);
}

static void fb_connection_free(struct FbConnection *fb_connection)
{
	if (fb_connection->db) {
		fb_connection_disconnect_warn(fb_connection);
	}
	xfree(fb_connection);
}

static struct FbConnection* fb_connection_check_retrieve(VALUE data)
{
	if (TYPE(data) != T_DATA || RDATA(data)->dfree != (void *)fb_connection_free) {
		rb_raise(rb_eTypeError,
			"Wrong argument type %s (expected Fb::Connection)",
			rb_class2name(CLASS_OF(data)));
	}
	return (struct FbConnection*)RDATA(data)->data;
}

static unsigned short fb_connection_db_SQL_Dialect(struct FbConnection *fb_connection)
{
	long isc_status[20];
	long dialect;
	long length;
	char db_info_command = isc_info_db_SQL_dialect;

	/* Get the db SQL Dialect */
	isc_database_info(isc_status, &fb_connection->db,
			1, &db_info_command,
			sizeof(isc_info_buff), isc_info_buff);
	fb_error_check(isc_status);

	if (isc_info_buff[0] == isc_info_db_SQL_dialect) {
		length = isc_vax_integer(&isc_info_buff[1], 2);
		dialect = isc_vax_integer(&isc_info_buff[3], (short)length);
	} else {
		dialect = 1;
	}
	return dialect;
}

static unsigned short fb_connection_dialect(struct FbConnection *fb_connection)
{
	return fb_connection->dialect;
}

static unsigned short fb_connection_db_dialect(struct FbConnection *fb_connection)
{
	return fb_connection->db_dialect;
}

/* Transaction option list */

static trans_opts	rcom_opt_S[] =
{
	"NO",			"RECORD_VERSION",	isc_tpb_no_rec_version,	-1,	0,
	"RECORD_VERSION",	0,			isc_tpb_rec_version,	-1,	0,
	"*",			0,			isc_tpb_no_rec_version,	-1,	0,
	0,			0,			0,			0,	0
};


static trans_opts	read_opt_S[] =
{
	"WRITE",	0,	isc_tpb_write,		1,	0,
	"ONLY",		0,	isc_tpb_read,		1,	0,
	"COMMITTED",	0,	isc_tpb_read_committed,	2,	rcom_opt_S,
	0,		0,	0,			0,	0
};


static trans_opts	snap_opt_S[] =
{
	"TABLE",	"STABILITY",	isc_tpb_consistency,	2,	0,
	"*",		0,		isc_tpb_concurrency,	2,	0,
	0,			0,	0,			0,	0
};


static trans_opts	isol_opt_S[] =
{
	"SNAPSHOT",	0,		0,			0,	snap_opt_S,
	"READ",		"COMMITTED",	isc_tpb_read_committed,	2,	rcom_opt_S,
	0,		0,		0,			0,	0
};


static trans_opts	trans_opt_S[] =
{
	"READ",		0,		0,		0,	read_opt_S,
	"WAIT",		0,		isc_tpb_wait,	3,	0,
	"NO",		"WAIT",		isc_tpb_nowait,	3,	0,
	"ISOLATION",	"LEVEL",	0,		0,	isol_opt_S,
	"SNAPSHOT",	0,		0,		0,	snap_opt_S,
	"RESERVING",	0,		-1,		0,	0,
	0,		0,		0,		0,	0
};

/* Name1	Name2		Option value	    Position	Sub-option */

#define	RESV_TABLEEND	"FOR"
#define	RESV_SHARED	"SHARED"
#define	RESV_PROTECTD	"PROTECTED"
#define	RESV_READ	"READ"
#define	RESV_WRITE	"WRITE"
#define	RESV_CONTINUE	','

static char* trans_parseopts(VALUE opt, int *tpb_len)
{
	char *s, *trans;
	long used;
	long size;
	char *tpb;
	trans_opts *curr_p;
	trans_opts *target_p;
	char *check1_p;
	char *check2_p;
	int count;
	int next_c;
	char check_f[4];
	char *resv_p;
	char *resend_p;
	char *tblend_p;
	int tbl_len;
	int res_first;
	int res_count;
	int ofs;
	char sp_prm;
	char rw_prm;
	int cont_f;
	char *desc = 0;

	/* Initialize */
	s = STR2CSTR(opt);
	trans = ALLOCA_N(char, strlen(s)+1);
	strcpy(trans, s);
	s = trans;
	while (*s) {
		*s = UPPER(*s);
		s++;
	}

	used = 0;
	size = 0;
	tpb = NULL;
	memset((void *)check_f, 0, sizeof(check_f));

	/* Set the default transaction option */
	tpb = (char*)xmalloc(TPBBUFF_ALLOC);
	size = TPBBUFF_ALLOC;
	memcpy((void*)tpb, (void*)isc_tpb_0, sizeof(isc_tpb_0));
	used = sizeof(isc_tpb_0);

	/* Analize the transaction option strings */
	curr_p = trans_opt_S;
	check1_p = strtok(trans, CMND_DELIMIT);
	if (check1_p) {
		check2_p = strtok(0, CMND_DELIMIT);
	} else {
		check2_p = 0;
	}
	while (curr_p) {
		target_p = 0;
		next_c = 0;
		for (count = 0; curr_p[count].option1; count++) {
			if (!strcmp(curr_p[count].option1, "*")) {
				target_p = &curr_p[count];
				break;
			} else if (check1_p && !strcmp(check1_p, curr_p[count].option1)) {
				if (!curr_p[count].option2) {
					next_c = 1;
					target_p = &curr_p[count];
					break;
				} else if (check2_p && !strcmp(check2_p, curr_p[count].option2)) {
					next_c = 2;
					target_p = &curr_p[count];
					break;
				}
			}
		}

		if (!target_p) {
			desc = "Illegal transaction option was specified";
			goto error;
		}

		/* Set the transaction option */
		if (target_p->optval > '\0') {
			if (target_p->position > 0) {
				if (check_f[target_p->position]) {
					desc = "Duplicate transaction option was specified";
					goto error;
				}
				tpb[target_p->position] = target_p->optval;
				check_f[target_p->position] = 1;
			} else {
				if (used + 1 > size) {
					tpb = (char *)realloc(tpb, size + TPBBUFF_ALLOC);
					size += TPBBUFF_ALLOC;
				}
				tpb[used] = target_p->optval;
				used++;
			}
		} else if (target_p->optval) {		/* RESERVING ... FOR */
			if (check_f[0]) {
				desc = "Duplicate transaction option was specified";
				goto error;
			}
			resv_p = check2_p;
			if (!resv_p || !strcmp(resv_p, RESV_TABLEEND)) {
				desc = "RESERVING needs table name list";
				goto error;
			}
			while (resv_p) {
				res_first = used;
				res_count = 0;
				resend_p = strtok(0, CMND_DELIMIT);
				while (resend_p) {
					if (!strcmp(resend_p, RESV_TABLEEND)) {
						break;
					}
					resend_p = strtok(0, CMND_DELIMIT);
				}

				if (!resend_p) {
					desc = "Illegal transaction option was specified";
					goto error;
				}

				while (resv_p < resend_p) {
					if (*resv_p == '\0' || (ofs = strspn(resv_p, LIST_DELIMIT)) < 0) {
						resv_p++;
					} else {
						resv_p = &resv_p[ofs];
						tblend_p = strpbrk(resv_p, LIST_DELIMIT);
						if (tblend_p) {
							tbl_len = tblend_p - resv_p;
						} else {
							tbl_len = strlen(resv_p);
						}
						if (tbl_len > META_NAME_MAX) {
							desc = "Illegal table name was specified";
							goto error;
						}

						if (tbl_len > 0) {
							if (used + tbl_len + 3 > size) {
								tpb = (char*)xrealloc(tpb, size+TPBBUFF_ALLOC);
								size += TPBBUFF_ALLOC;
							}
							tpb[used+1] = (char)tbl_len;
							memcpy((void *)&tpb[used+2],resv_p, tbl_len);
							used += tbl_len + 3;
							res_count++;
						}
						resv_p += tbl_len;
					}
				}

				resv_p = strtok(0, CMND_DELIMIT);
				if (resv_p && !strcmp(resv_p, RESV_SHARED)) {
					sp_prm = isc_tpb_shared;
				} else if (resv_p && !strcmp(resv_p, RESV_PROTECTD)) {
					sp_prm = isc_tpb_protected;
				} else {
					desc = "RESERVING needs {SHARED|PROTECTED} {READ|WRITE}";
					goto error;
				}

				cont_f = 0;
				resv_p = strtok(0, CMND_DELIMIT);
				if (resv_p) {
					if (resv_p[strlen(resv_p)-1] == RESV_CONTINUE) {
						cont_f = 1;
						resv_p[strlen(resv_p)-1] = '\0';
					} else {
						tblend_p = strpbrk(resv_p, LIST_DELIMIT);
						if (tblend_p) {
							cont_f = 2;
							*tblend_p = '\0';
						}
					}
				}

				if (resv_p && !strcmp(resv_p, RESV_READ)) {
					rw_prm = isc_tpb_lock_read;
				} else if (resv_p && !strcmp(resv_p, RESV_WRITE)) {
					rw_prm = isc_tpb_lock_write;
				} else {
					desc = "RESERVING needs {SHARED|PROTECTED} {READ|WRITE}";
					goto error;
				}

				ofs = res_first;
				for (count = 0; count < res_count; count++) {
					tpb[ofs++] = rw_prm;
					ofs += tpb[ofs] + 1;
					tpb[ofs++] = sp_prm;
				}

				if (cont_f == 1) {
					resv_p = strtok(0, CMND_DELIMIT);
					if (!resv_p) {
						desc = "Unexpected end of command";
						goto error;
					}
				}
				if (cont_f == 2) {
					resv_p = tblend_p + 1;
				} else {
					resv_p = strtok(0, CMND_DELIMIT);
					if (resv_p) {
						if ((int)strlen(resv_p) == 1 && resv_p[0] == RESV_CONTINUE) {
							resv_p = strtok(0, CMND_DELIMIT);
							if (!resv_p) {
								desc = "Unexpected end of command";
								goto error;
							}
						} else if (resv_p[0] == RESV_CONTINUE) {
							resv_p++;
						} else {
							next_c = 1;
							check2_p = resv_p;
							resv_p = 0;
						}
					} else {
						next_c = 0;
						check1_p = check2_p = 0;
					}
				}
			}

			check_f[0] = 1;
		}


		/* Set the next check list */
		curr_p = target_p->sub_opts;

		for (count = 0; count < next_c; count++) {
			check1_p = check2_p;
			if (check2_p) {
				check2_p = strtok(0, CMND_DELIMIT);
			}
		}

		if (check1_p && !curr_p) {
			curr_p = trans_opt_S;
		}
	}

	/* Set the results */
	*tpb_len = used;
	return tpb;

error:
	xfree(tpb);
	rb_raise(rb_eFbError, desc);
}

static void set_teb_vec(ISC_TEB *vec, struct FbConnection *fb_connection, char *tpb, int len)
{
	vec->dbb_ptr = &fb_connection->db;
	if (tpb) {
		vec->tpb_ptr = tpb;
		vec->tpb_len = len;
	} else {
		vec->tpb_ptr = 0;
		vec->tpb_len = 0;
	}
}

static void global_transaction_start(VALUE opt, int argc, VALUE *argv)
{
	long isc_status[20];
	struct FbConnection *fb_connection;
	ISC_TEB *teb_vec = ALLOCA_N(ISC_TEB, connection_count);
	ISC_TEB *vec = teb_vec;
	char *tpb = 0;
	short n;
	int tpb_len;

	if (global_transact) {
		rb_raise(rb_eFbError, "The transaction has been already started");
	}

	if (!NIL_P(opt)) {
		tpb = trans_parseopts(opt, &tpb_len);
	}

	if (argc > connection_count) {
		rb_raise(rb_eFbError, "Too many databases specified for the transaction");
	}
	if (argc == 0) {
		n = connection_count;
		for (fb_connection = fb_connection_list; fb_connection; fb_connection = fb_connection->next) {
			set_teb_vec(vec, fb_connection, tpb, tpb_len);
			vec++;
		}
	} else {
		for (n = 0; n < argc; n++) {
			fb_connection = fb_connection_check_retrieve(argv[n]);
			set_teb_vec(vec, fb_connection, tpb, tpb_len);
			vec++;
		}
	}

	isc_start_multiple(isc_status, &global_transact, n, teb_vec);
	xfree(tpb);
	fb_error_check(isc_status);
}

/* call-seq:
 *   commit() -> nil
 *
 * Commit the current (global) transaction.
 */
static VALUE global_commit()
{
	long isc_status[20];

	if (global_transact) {
		global_close_cursors();
		isc_commit_transaction(isc_status, &global_transact);
		fb_error_check(isc_status);
	}
	return Qnil;
}

/* call-seq:
 *   rollback() -> nil
 *
 * Rollback the current (global) transaction.
 */
static VALUE global_rollback()
{
	long isc_status[20];

	if (global_transact) {
		global_close_cursors();
		isc_rollback_transaction(isc_status, &global_transact);
		fb_error_check(isc_status);
	}
	return Qnil;
}

/* call-seq:
 *   transaction(options, *connections) -> true
 *   transaction(options, *connections) { } -> block result
 *
 * Start a (global) transaction.
 */
static VALUE global_transaction(int argc, VALUE *argv, VALUE self)
{
	VALUE opt = Qnil;

	if (argc > 0) {
		opt = *argv++;
		argc--;
	}
	global_transaction_start(opt, argc, argv);

	if (rb_block_given_p()) {
		int state;
		VALUE result = rb_protect(rb_yield, 0, &state);
		if (state) {
			global_rollback();
			return rb_funcall(rb_mKernel, rb_intern("raise"), 0);
		} else {
			global_commit();
			return result;
		}
	} else {
		return Qtrue;
   	}
}

/* call-seq:
 *   transaction_started()? -> true or false
 *
 * Returns true if a transaction is currently active.
 */
static VALUE global_transaction_started()
{
	return global_transact ? Qtrue : Qfalse;
}

static void fb_connection_transaction_start(struct FbConnection *fb_connection, VALUE opt)
{
	long isc_status[20];
	char *tpb = 0;
	int tpb_len;

	if (fb_connection->transact) {
		rb_raise(rb_eFbError, "A transaction has been already started");
	}

	if (!NIL_P(opt)) {
		tpb = trans_parseopts(opt, &tpb_len);
	} else {
		tpb_len = 0;
		tpb = NULL;
	}

	isc_start_transaction(isc_status, &fb_connection->transact, 1, &fb_connection->db, tpb_len, tpb);
	xfree(tpb);
	fb_error_check(isc_status);
}

static void fb_connection_commit(struct FbConnection *fb_connection)
{
	long isc_status[20];

	if (fb_connection->transact) {
		fb_connection_close_cursors(fb_connection);
		isc_commit_transaction(isc_status, &fb_connection->transact);
		fb_error_check(isc_status);
	}
}

static void fb_connection_rollback(struct FbConnection *fb_connection)
{
	long isc_status[20];

	if (fb_connection->transact) {
		fb_connection_close_cursors(fb_connection);
		isc_rollback_transaction(isc_status, &fb_connection->transact);
		fb_error_check(isc_status);
	}
}

/* call-seq:
 *   transaction(options) -> true
 *   transaction(options) { } -> block result
 *
 * Start a transaction for this connection.
 */
static VALUE connection_transaction(int argc, VALUE *argv, VALUE self)
{
	struct FbConnection *fb_connection;
	VALUE opt = Qnil;

	rb_scan_args(argc, argv, "01", &opt);
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	fb_connection_transaction_start(fb_connection, opt);

	if (rb_block_given_p()) {
		int state;
		VALUE result = rb_protect(rb_yield, 0, &state);
		if (state) {
			fb_connection_rollback(fb_connection);
			return rb_funcall(rb_mKernel, rb_intern("raise"), 0);
		} else {
			fb_connection_commit(fb_connection);
			return result;
		}
	} else {
		return Qtrue;
   	}
}

/* call-seq:
 *   transaction_started()? -> true or false
 *
 * Returns true if a transaction is currently active.
 */
static VALUE connection_transaction_started(VALUE self)
{
	struct FbConnection *fb_connection;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	return fb_connection->transact ? Qtrue : Qfalse;
}

/* call-seq:
 *   commit() -> nil
 *
 * Commit the current transaction.
 */
static VALUE connection_commit(VALUE self)
{
	struct FbConnection *fb_connection;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	fb_connection_commit(fb_connection);
	return Qnil;
}

/* call-seq:
 *   rollback() -> nil
 *
 * Rollback the current transaction.
 */
static VALUE connection_rollback(VALUE self)
{
	struct FbConnection *fb_connection;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	fb_connection_rollback(fb_connection);
	return Qnil;
}

/*
 * call-seq:
 *   open?() -> true or false
 *
 * Current connection status.
 */

static VALUE connection_is_open(VALUE self)
{
	struct FbConnection *fb_connection;

	Data_Get_Struct(self, struct FbConnection, fb_connection);
	return (fb_connection->db == 0) ? Qfalse : Qtrue;
}

/* call-seq:
 *   to_s() -> String
 *
 * Return current database connection string and either (OPEN) or (CLOSED).
 */
static VALUE connection_to_s(VALUE self)
{
	VALUE is_open = connection_is_open(self);
	VALUE status = (is_open == Qtrue) ? rb_str_new2(" (OPEN)") : rb_str_new2(" (CLOSED)");
	VALUE database = rb_iv_get(self, "@database");
	VALUE s = rb_str_dup(database);
	return rb_str_concat(s, status);
}

/* call-seq:
 *   cursor() -> Cursor
 *
 * Creates a +Cursor+ for the +Connection+ and allocates a statement.
 * This function is no longer published.
 */
static VALUE connection_cursor(VALUE self)
{
	long isc_status[20];
	VALUE c;
	struct FbConnection *fb_connection;
	struct FbCursor *fb_cursor;

	Data_Get_Struct(self, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	c = Data_Make_Struct(rb_cFbCursor, struct FbCursor, fb_cursor_mark, fb_cursor_free, fb_cursor);
	fb_cursor->connection = self;
	fb_cursor->fields_ary = Qnil;
	fb_cursor->fields_hash = Qnil;
	fb_cursor->open = Qfalse;
	fb_cursor->stmt = 0;
	fb_cursor->i_sqlda = sqlda_alloc(SQLDA_COLSINIT);
	fb_cursor->o_sqlda = sqlda_alloc(SQLDA_COLSINIT);
	fb_cursor->i_buffer = NULL;
	fb_cursor->i_buffer_size = 0;
	fb_cursor->o_buffer = NULL;
	fb_cursor->o_buffer_size = 0;
	isc_dsql_alloc_statement2(isc_status, &fb_connection->db, &fb_cursor->stmt);
	fb_error_check(isc_status);

	return c;
}

/* call-seq:
 *   execute(sql, *args) -> Cursor or rows affected
 *   execute(sql, *args) {|cursor| } -> block result
 *
 * Allocates a +Cursor+ and executes the +sql+ statement, matching up the
 * parameters in +args+ with the place holders in the statement, represented by ?'s.
 *
 * If the sql statement returns a result set and a block is provided, the cursor is
 * yielded to the block before being automatically closed.
 *
 * If the sql statement returns a result set and a block is not provided, a +Cursor+
 * object is returned.
 *
 * If the sql statement performs an INSERT, UPDATE or DELETE, the number of rows
 * affected is returned.  Other statements, such as schema updates, return -1.
 *
 * +*args+ can be one or more tuples of parameters or an array of tuples (arrays),
 * in which case the statement will be executed once for each tuple of parameters.
 *
 * If no transaction is currently active, a transaction is automatically started
 * and is closed when the cursor is closed.  Note that if additional statements
 * yielding cursors are started before the first cursor is closed, these cursors will
 * also be closed when the first one is closed and its transaction committed.
 */
static VALUE connection_execute(int argc, VALUE *argv, VALUE self)
{
	VALUE cursor = connection_cursor(self);
	VALUE val = cursor_execute(argc, argv, cursor);

	if (NIL_P(val)) {
		if (rb_block_given_p()) {
			return rb_ensure(rb_yield,cursor,cursor_close,cursor);
   		} else {
			return cursor;
   		}
	} else {
		cursor_drop(cursor);
	}
	return val;
}

/* call-seq:
 *   query(:array, sql, *arg) -> Array of Arrays or nil
 *   query(:hash, sql, *arg) -> Array of Hashes or nil
 *   query(sql, *args) -> Array of Arrays or nil
 *
 * For queries returning a result set, an array is returned, containing
 * either a list of Arrays or Hashes, one for each row.
 *
 * If the sql statement performs an INSERT, UPDATE or DELETE, the number of rows
 * affected is returned.  Other statements, such as schema updates, return -1.
 *
 * If no transaction is currently active, a transaction is automatically started
 * and committed.  Otherwise, the statement executes within the context of the
 * current transaction.
 */
static VALUE connection_query(int argc, VALUE *argv, VALUE self)
{
	VALUE format;
	VALUE cursor;
	VALUE result;

	if (argc >= 1 && TYPE(argv[0]) == T_SYMBOL) {
		format = argv[0];
		argc--; argv++;
	} else {
		format = ID2SYM(rb_intern("array"));
	}
	cursor = connection_cursor(self);
	result = cursor_execute(argc, argv, cursor);
	if (NIL_P(result)) {
		result = cursor_fetchall(1, &format, cursor);
		cursor_close(cursor);
	}

	return result;
}

/* call-seq:
 *   close() -> nil
 *
 * Closes connection to database.  All open cursors are dropped.
 */
static VALUE connection_close(VALUE self)
{
	struct FbConnection *fb_connection;

	Data_Get_Struct(self, struct FbConnection, fb_connection);

	if (fb_connection->dropped) return Qnil;

	fb_connection_check(fb_connection);
	fb_connection_disconnect(fb_connection);
	fb_connection_drop_cursors(fb_connection);

	return Qnil;
}

/* call-seq:
 *   drop() -> nil
 *
 * Drops connected database.  All open cursors are dropped.
 */
static VALUE connection_drop(VALUE self)
{
	struct FbConnection *fb_connection;

	Data_Get_Struct(self, struct FbConnection, fb_connection);
	fb_connection->dropped = 1;
	fb_connection_disconnect(fb_connection);
	fb_connection_drop_cursors(fb_connection);

	return Qnil;
}

/* call-seq:
 *   dialect() -> int
 *
 * Returns dialect of connection.
 */
static VALUE connection_dialect(VALUE self)
{
	struct FbConnection *fb_connection;

	Data_Get_Struct(self, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	return INT2FIX(fb_connection->dialect);
}

/* call-seq:
 *   db_dialect() -> int
 *
 * Returns database dialect.
 */
static VALUE connection_db_dialect(VALUE self)
{
	struct FbConnection *fb_connection;

	Data_Get_Struct(self, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	return INT2FIX(fb_connection->db_dialect);
}

static void fb_cursor_check(struct FbCursor *fb_cursor)
{
	if (fb_cursor->stmt == 0) {
		rb_raise(rb_eFbError, "dropped db cursor");
	}
	if (!fb_cursor->open) {
		rb_raise(rb_eFbError, "closed db cursor");
	}
}

static void fb_cursor_drop(struct FbCursor *fb_cursor)
{
	long isc_status[20];
	if (fb_cursor->open) {
		isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_close);
		fb_error_check(isc_status);
	}
	isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_drop);
	fb_error_check(isc_status);
}

static void fb_cursor_drop_warn(struct FbCursor *fb_cursor)
{
	long isc_status[20];
	if (fb_cursor->open) {
		isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_close);
		fb_error_check_warn(isc_status);
	}
	isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_drop);
	fb_error_check_warn(isc_status);
}

static void fb_cursor_mark(struct FbCursor *fb_cursor)
{
	rb_gc_mark(fb_cursor->connection);
	rb_gc_mark(fb_cursor->fields_ary);
	rb_gc_mark(fb_cursor->fields_hash);
}

static void fb_cursor_free(struct FbCursor *fb_cursor)
{
	if (fb_cursor->stmt) {
		fb_cursor_drop_warn(fb_cursor);
	}
	xfree(fb_cursor->i_sqlda);
	xfree(fb_cursor->o_sqlda);
	xfree(fb_cursor->i_buffer);
	xfree(fb_cursor->o_buffer);
	xfree(fb_cursor);
}

struct time_object {
    struct timeval tv;
    struct tm tm;
    int gmt;
    int tm_got;
};

#define GetTimeval(obj, tobj) \
    Data_Get_Struct(obj, struct time_object, tobj)

static void fb_cursor_set_inputparams(struct FbCursor *fb_cursor, int argc, VALUE *argv)
{
	struct FbConnection *fb_connection;
	long count;
	long offset;
	long type;
	short dtp;
	VALUE obj;
	long lvalue;
	long alignment;
	double dvalue;
	double dcheck;
	VARY *vary;
	XSQLVAR *var;

	isc_blob_handle blob_handle;
	ISC_QUAD blob_id;
	static char blob_items[] = { isc_info_blob_max_segment };
	char blob_info[16];
	char *p;
	long length;
	struct time_object *tobj;
	struct tm tms;

	long isc_status[20];

	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);

	/* Check the number of parameters */
	if (fb_cursor->i_sqlda->sqld != argc) {
		rb_raise(rb_eFbError, "statement requires %d items; %d given", fb_cursor->i_sqlda->sqld, argc);
	}

	/* Get the parameters */
	for (count = 0,offset = 0; count < argc; count++) {
		obj = argv[count];

		type = TYPE(obj);

		/* Convert the data type for InterBase */
		var = &fb_cursor->i_sqlda->sqlvar[count];
		if (!NIL_P(obj)) {
			dtp = var->sqltype & ~1;		/* Erase null flag */
			alignment = var->sqllen;

			switch (dtp) {
				case SQL_TEXT :
					alignment = 1;
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					obj = rb_obj_as_string(obj);
					if (RSTRING(obj)->len > var->sqllen) {
						rb_raise(rb_eRangeError, "CHAR overflow: %d bytes exceeds %d byte(s) allowed.",
							RSTRING(obj)->len, var->sqllen);
					}
					memcpy(var->sqldata, RSTRING(obj)->ptr, RSTRING(obj)->len);
					var->sqllen = RSTRING(obj)->len;
					offset += var->sqllen + 1;
					break;

				case SQL_VARYING :
					alignment = sizeof(short);
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					vary = (VARY *)var->sqldata;
					obj = rb_obj_as_string(obj);
					if (RSTRING(obj)->len > var->sqllen) {
						rb_raise(rb_eRangeError, "VARCHAR overflow: %d bytes exceeds %d byte(s) allowed.",
							RSTRING(obj)->len, var->sqllen);
					}
					memcpy(vary->vary_string, RSTRING(obj)->ptr, RSTRING(obj)->len);
					vary->vary_length = RSTRING(obj)->len;
					offset += vary->vary_length + sizeof(short);
					break;

				case SQL_SHORT :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					lvalue = NUM2LONG(obj);
					if (lvalue < SHRT_MIN || lvalue > SHRT_MAX) {
						rb_raise(rb_eRangeError, "short integer overflow");
					}
					*(short *)var->sqldata = lvalue;
					offset += alignment;
					break;

				case SQL_LONG :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					lvalue = NUM2LONG(obj);
					*(long *)var->sqldata = lvalue;
					offset += alignment;
					break;

				case SQL_FLOAT :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					dvalue = NUM2DBL(obj);
					if (dvalue >= 0.0) {
						dcheck = dvalue;
					} else {
						dcheck = dvalue * -1;
					}
					if (dcheck != 0.0 && (dcheck < FLT_MIN || dcheck > FLT_MAX)) {
						rb_raise(rb_eRangeError, "float overflow");
					}
					*(float *)var->sqldata = dvalue;
					offset += alignment;
					break;

				case SQL_DOUBLE :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					dvalue = NUM2DBL(obj);
					*(double *)var->sqldata = dvalue;
					offset += alignment;
					break;
#if HAVE_LONG_LONG
				case SQL_INT64 :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					*(ISC_INT64 *)var->sqldata = NUM2LL(obj);
					offset += alignment;
					break;
#endif
				case SQL_BLOB :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					obj = rb_obj_as_string(obj);

					blob_handle = NULL;
					isc_create_blob2(
						isc_status,&fb_connection->db,&fb_connection->transact,
						&blob_handle,&blob_id,0,NULL);
					fb_error_check(isc_status);
					length = RSTRING(obj)->len;
					p = RSTRING(obj)->ptr;
					while (length >= 4096) {
						isc_put_segment(isc_status,&blob_handle,4096,p);
						fb_error_check(isc_status);
						p += 4096;
						length -= 4096;
					}
					if (length) {
						isc_put_segment(isc_status,&blob_handle,length,p);
						fb_error_check(isc_status);
					}
					isc_close_blob(isc_status,&blob_handle);
					fb_error_check(isc_status);

					*(ISC_QUAD *)var->sqldata = blob_id;
					offset += alignment;
					break;

				case SQL_TIMESTAMP :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					GetTimeval(obj, tobj);
					isc_encode_timestamp(&tobj->tm, (ISC_TIMESTAMP *)var->sqldata);
					offset += alignment;
					break;

				case SQL_TYPE_TIME :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					GetTimeval(obj, tobj);
					isc_encode_sql_time(&tobj->tm, (ISC_TIME *)var->sqldata);
					offset += alignment;
					break;

				case SQL_TYPE_DATE :
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					tm_from_date(&tms, obj);
					isc_encode_sql_date(&tms, (ISC_DATE *)var->sqldata);
					offset += alignment;
					break;

#if 0
				case SQL_ARRAY :
					/* Not supported now
					offset = ALIGN(offset, alignment);
					var->sqldata = (char *)(fb_cursor->i_buffer + offset);
					if (get_arrayvalue(self, type, obj, var))
						return(STATUS_ABNORMAL);
					offset += alignment;
					break;
					*/
					rb_raise(rb_eArgError, "Arrays not supported");
					break;
#endif

				default :
					rb_raise(rb_eFbError, "Specified table includes unsupported datatype (%d)", dtp);
			}

			if (var->sqltype & 1) {
				offset = ALIGN(offset, sizeof(short));
				var->sqlind = (short *)(fb_cursor->i_buffer + offset);
				*var->sqlind = 0;
				offset += sizeof(short);
			}
		} else if (var->sqltype & 1) {
			var->sqldata = 0;
			offset = ALIGN(offset, sizeof(short));
			var->sqlind = (short *)(fb_cursor->i_buffer + offset);
			*var->sqlind = -1;
			offset += sizeof(short);
		} else {
			rb_raise(rb_eFbError, "specified column is not permitted to be null");
		}
	}
}

static void fb_cursor_execute_withparams(struct FbCursor *fb_cursor, int argc, VALUE *argv)
{
	struct FbConnection *fb_connection;
	long isc_status[20];

	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	/* Check the first object type of the parameters */
	if (argc >= 1 && TYPE(argv[0]) == T_ARRAY) {
		int i;
		VALUE obj;
		VALUE ary = argv[0];
		if (RARRAY(ary)->len > 0 && TYPE(RARRAY(ary)->ptr[0]) == T_ARRAY) {
			for (i = 0; i < RARRAY(ary)->len; i++) {
				obj = rb_ary_entry(ary, i);
				fb_cursor_execute_withparams(fb_cursor, 1, &obj);
			}
		} else {
			for (i = 0; i < argc; i++) {
				obj = argv[i];

				/* Set the input parameters */
				Check_Type(obj, T_ARRAY);
				fb_cursor_set_inputparams(fb_cursor, RARRAY(obj)->len, RARRAY(obj)->ptr);

				/* Execute SQL statement */
				isc_dsql_execute2(isc_status, &fb_connection->transact, &fb_cursor->stmt, SQLDA_VERSION1, fb_cursor->i_sqlda, NULL);
				fb_error_check(isc_status);
			}
		}
	} else {
		/* Set the input parameters */
		fb_cursor_set_inputparams(fb_cursor, argc, argv);

		/* Execute SQL statement */
		isc_dsql_execute2(isc_status, &fb_connection->transact, &fb_cursor->stmt, SQLDA_VERSION1, fb_cursor->i_sqlda, NULL);
		fb_error_check(isc_status);
	}
}

static VALUE precision_from_sqlvar(XSQLVAR *sqlvar)
{
	switch(sqlvar->sqltype & ~1) {
		case SQL_TEXT:		return Qnil;
		case SQL_VARYING:	return Qnil;
		case SQL_SHORT:
			switch (sqlvar->sqlsubtype) {
				case 0:		return INT2FIX(0);
				case 1:		return INT2FIX(4);
				case 2:		return INT2FIX(4);
			}
			break;
		case SQL_LONG:
			switch (sqlvar->sqlsubtype) {
				case 0:		return INT2FIX(0);
				case 1:		return INT2FIX(9);
				case 2:		return INT2FIX(9);
			}
			break;
		case SQL_FLOAT:		return Qnil;
		case SQL_DOUBLE:
		case SQL_D_FLOAT:
			switch (sqlvar->sqlsubtype) {
				case 0:		return Qnil;
				case 1:		return INT2FIX(15);
				case 2:		return INT2FIX(15);
			}
			break;
		case SQL_TIMESTAMP:	return Qnil;
		case SQL_BLOB:		return Qnil;
		case SQL_ARRAY:		return Qnil;
		case SQL_QUAD:		return Qnil;
		case SQL_TYPE_TIME:	return Qnil;
		case SQL_TYPE_DATE:	return Qnil;
		case SQL_INT64:
			switch (sqlvar->sqlsubtype) {
				case 0:		return INT2FIX(0);
				case 1:		return INT2FIX(18);
				case 2:		return INT2FIX(18);
			}
			break;
	}
	return Qnil;
}

static VALUE fb_cursor_fields_ary(XSQLDA *sqlda, short downcase_names)
{
	long cols;
	long count;
	XSQLVAR *var;
	short dtp;
	VALUE ary;
	VALUE re_lowercase;
	ID id_matches, id_downcase_bang;

	cols = sqlda->sqld;
	if (cols == 0) {
		return Qnil;
	}

	if (downcase_names) {
		re_lowercase = rb_reg_regcomp(rb_str_new2("[[:lower:]]"));
		id_matches = rb_intern("=~");
		id_downcase_bang = rb_intern("downcase!");
	}

	ary = rb_ary_new();
	for (count = 0; count < cols; count++) {
		VALUE field;
		VALUE name, type_code, sql_type, sql_subtype, display_size, internal_size, precision, scale, nullable;

		var = &sqlda->sqlvar[count];
		dtp = var->sqltype & ~1;

		if (var->aliasname_length) { /* aliasname always present? */
			name = rb_tainted_str_new(var->aliasname, var->aliasname_length);
		} else {
			name = rb_tainted_str_new(var->sqlname, var->sqlname_length);
		}
		if (downcase_names && rb_funcall(re_lowercase, id_matches, 1, name) == Qnil) {
			rb_funcall(name, id_downcase_bang, 0);
		}
		rb_str_freeze(name);
		type_code = INT2NUM((long)(var->sqltype & ~1));
		sql_type = fb_sql_type_from_code(dtp, var->sqlsubtype);
		sql_subtype = INT2FIX(var->sqlsubtype);
		display_size = INT2NUM((long)var->sqllen);
		if (dtp == SQL_VARYING) {
			internal_size = INT2NUM((long)var->sqllen + sizeof(short));
		} else {
			internal_size = INT2NUM((long)var->sqllen);
		}
		precision = precision_from_sqlvar(var);
		scale = INT2NUM((long)var->sqlscale);
		nullable = (var->sqltype & 1) ? Qtrue : Qfalse;

		field = rb_struct_new(rb_sFbField, name, sql_type, sql_subtype, display_size, internal_size, precision, scale, nullable, type_code);
		rb_ary_push(ary, field);
	}
	rb_ary_freeze(ary);
	return ary;
}

static VALUE fb_cursor_fields_hash(VALUE fields_ary)
{
	int i;
	VALUE hash = rb_hash_new();

	for (i = 0; i < RARRAY(fields_ary)->len; i++) {
		VALUE field = rb_ary_entry(fields_ary, i);
		VALUE name = rb_struct_aref(field, LONG2NUM(0));
		rb_hash_aset(hash, name, field);
	}

	return hash;
}

static void fb_cursor_fetch_prep(struct FbCursor *fb_cursor)
{
	struct FbConnection *fb_connection;
	long cols;
	long count;
	XSQLVAR *var;
	short dtp;
	long length;
	long alignment;
	long offset;
	long isc_status[20];

	fb_cursor_check(fb_cursor);

	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	/* Check if open cursor */
	if (!fb_cursor->open) {
		rb_raise(rb_eFbError, "The cursor has not been open. Use execute(query)");
	}
	/* Describe output SQLDA */
	isc_dsql_describe(isc_status, &fb_cursor->stmt, 1, fb_cursor->o_sqlda);
	fb_error_check(isc_status);

	/* Set the output SQLDA */
	cols = fb_cursor->o_sqlda->sqld;
	for (var = fb_cursor->o_sqlda->sqlvar, offset = 0, count = 0; count < cols; var++, count++) {
		length = alignment = var->sqllen;
		dtp = var->sqltype & ~1;

		if (dtp == SQL_TEXT) {
			alignment = 1;
		} else if (dtp == SQL_VARYING) {
			length += sizeof(short);
			alignment = sizeof(short);
		}
		offset = ALIGN(offset, alignment);
		var->sqldata = (char*)(fb_cursor->o_buffer + offset);
		offset += length;
		offset = ALIGN(offset, sizeof(short));
		var->sqlind = (short*)(fb_cursor->o_buffer + offset);
		offset += sizeof(short);
	}
}

static VALUE fb_cursor_fetch(struct FbCursor *fb_cursor)
{
	struct FbConnection *fb_connection;
	long cols;
	VALUE ary;
	long count;
	XSQLVAR *var;
	long dtp;
	VALUE val;
	VARY *vary;
	double ratio;
	double dval;
	long scnt;
	struct tm tms;

	isc_blob_handle blob_handle;
	ISC_QUAD blob_id;
	unsigned short actual_seg_len;
	time_t t;
	static char blob_items[] = {
		isc_info_blob_max_segment,
		isc_info_blob_num_segments,
		isc_info_blob_total_length
	};
	char blob_info[32];
	char *p, item;
	short length;
	unsigned short max_segment;
	ISC_LONG num_segments;
	ISC_LONG total_length;

	long isc_status[20];

	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	/* Fetch one row */
	if (isc_dsql_fetch(isc_status, &fb_cursor->stmt, 1, fb_cursor->o_sqlda) == SQLCODE_NOMORE) {
		return Qnil;
	}
	fb_error_check(isc_status);

	/* Create the result tuple object */
	cols = fb_cursor->o_sqlda->sqld;
	ary = rb_ary_new2(cols);

	/* Create the result objects for each columns */
	for (count = 0; count < cols; count++) {
		var = &fb_cursor->o_sqlda->sqlvar[count];
		dtp = var->sqltype & ~1;

		/* Check if column is null */

		if ((var->sqltype & 1) && (*var->sqlind < 0)) {
			val = Qnil;
		} else {
			/* Set the column value to the result tuple */

			switch (dtp) {
				case SQL_TEXT:
					val = rb_tainted_str_new(var->sqldata, var->sqllen);
					break;

				case SQL_VARYING:
					vary = (VARY*)var->sqldata;
					val = rb_tainted_str_new(vary->vary_string, vary->vary_length);
					break;

				case SQL_SHORT:
					if (var->sqlscale < 0) {
						ratio = 1;
						for (scnt = 0; scnt > var->sqlscale; scnt--)
							ratio *= 10;
						dval = (double)*(short*)var->sqldata/ratio;
						val = rb_float_new(dval);
					} else {
						val = INT2NUM((long)*(short*)var->sqldata);
					}
					break;

				case SQL_LONG:
					if (var->sqlscale < 0) {
						ratio = 1;
						for (scnt = 0; scnt > var->sqlscale; scnt--)
						ratio *= 10;
						dval = (double)*(long*)var->sqldata/ratio;
						val = rb_float_new(dval);
					} else {
						val = INT2NUM(*(long*)var->sqldata);
					}
					break;

				case SQL_FLOAT:
					val = rb_float_new((double)*(float*)var->sqldata);
					break;

				case SQL_DOUBLE:
					val = rb_float_new(*(double*)var->sqldata);
					break;
#if HAVE_LONG_LONG
				case SQL_INT64:
					val = LL2NUM(*(LONG_LONG*)var->sqldata);
					break;
#endif
				case SQL_TIMESTAMP:
					isc_decode_timestamp((ISC_TIMESTAMP *)var->sqldata, &tms);
					t = mktime(&tms);
					if (t < 0) t = 0;
					val = rb_time_new(t, 0);
					break;

				case SQL_TYPE_TIME:
					isc_decode_sql_time((ISC_TIME *)var->sqldata, &tms);
					/*
					t = mktime(&tms);
					if (t < 0) t = t + (24 * 60 * 60);
					val = rb_time_new(t, 0);
					*/
					tms.tm_year = 2000;
					tms.tm_mon = 1;
					tms.tm_mday = 1;
					val = fb_mktime(&tms);
					break;

				case SQL_TYPE_DATE:
					isc_decode_sql_date((ISC_DATE *)var->sqldata, &tms);
					val = fb_mkdate(&tms);
					break;

				case SQL_BLOB:
					blob_handle = NULL;
					blob_id = *(ISC_QUAD *)var->sqldata;
					isc_open_blob2(isc_status, &fb_connection->db, &fb_connection->transact, &blob_handle, &blob_id, 0, NULL);
					fb_error_check(isc_status);
					isc_blob_info(
						isc_status, &blob_handle,
						sizeof(blob_items), blob_items,
						sizeof(blob_info), blob_info);
					fb_error_check(isc_status);
					for (p = blob_info; *p != isc_info_end; p += length) {
						item = *p++;
						length = (short) isc_vax_integer(p,2);
						p += 2;
						switch (item) {
							case isc_info_blob_max_segment:
								max_segment = isc_vax_integer(p,length);
								break;
							case isc_info_blob_num_segments:
								num_segments = isc_vax_integer(p,length);
								break;
							case isc_info_blob_total_length:
								total_length = isc_vax_integer(p,length);
								break;
						}
					}
					val = rb_tainted_str_new(NULL,total_length);
					for (p = RSTRING(val)->ptr; num_segments > 0; num_segments--, p += actual_seg_len) {
						isc_get_segment(isc_status, &blob_handle, &actual_seg_len, max_segment, p);
						fb_error_check(isc_status);
					}
					isc_close_blob(isc_status, &blob_handle);
					fb_error_check(isc_status);
					break;

				case SQL_ARRAY:
					rb_warn("ARRAY not supported (yet)");
					val = Qnil;
					break;

				default:
					rb_raise(rb_eFbError, "Specified table includes unsupported datatype (%d)", dtp);
					break;
			}
		}
		rb_ary_push(ary, val);
	}

	return ary;
}

static long cursor_rows_affected(struct FbCursor *fb_cursor, long statement_type)
{
	long inserted = 0, selected = 0, updated = 0, deleted = 0;
	char request[] = { isc_info_sql_records };
	char response[64], *r;
	long isc_status[20];

	isc_dsql_sql_info(isc_status, &fb_cursor->stmt, sizeof(request), request, sizeof(response), response);
	fb_error_check(isc_status);
	if (response[0] != isc_info_sql_records) { return -1; }

	r = response + 3; /* skip past first cluster */
	while (*r != isc_info_end) {
		char count_type = *r++;
		short len = isc_vax_integer(r, sizeof(short));
		r += sizeof(short);
		switch (count_type) {
			case isc_info_req_insert_count:
				inserted = isc_vax_integer(r, len);
				break;
			case isc_info_req_select_count:
				selected = isc_vax_integer(r, len);
				break;
			case isc_info_req_update_count:
				updated = isc_vax_integer(r, len);
				break;
			case isc_info_req_delete_count:
				deleted = isc_vax_integer(r, len);
				break;
		}
		r += len;
	}
	switch (statement_type) {
		case isc_info_sql_stmt_select: return selected;
		case isc_info_sql_stmt_insert: return inserted;
		case isc_info_sql_stmt_update: return updated;
		case isc_info_sql_stmt_delete: return deleted;
		default: return inserted + selected + updated + deleted;
	}
}

/* call-seq:
 *   execute2(sql, *args) -> nil or rows affected
 *
 * This function is not published.
 */
static VALUE cursor_execute2(VALUE args)
{
	struct FbCursor *fb_cursor;
	struct FbConnection *fb_connection;
	char *sql;
	long statement;
	long length;
	long in_params;
	long cols;
	long rows_affected;
	VALUE result = Qnil;
	long isc_status[20];

	VALUE self = rb_ary_pop(args);
	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);

	sql = STR2CSTR(rb_ary_shift(args));

	/* Prepare query */
	isc_dsql_prepare(isc_status, &fb_connection->transact, &fb_cursor->stmt, 0, sql, fb_connection_dialect(fb_connection), fb_cursor->o_sqlda);
	fb_error_check(isc_status);

	/* Get the statement type */
	isc_dsql_sql_info(isc_status, &fb_cursor->stmt,
			sizeof(isc_info_stmt), isc_info_stmt,
			sizeof(isc_info_buff), isc_info_buff);
	fb_error_check(isc_status);

	if (isc_info_buff[0] == isc_info_sql_stmt_type) {
		length = isc_vax_integer(&isc_info_buff[1], 2);
		statement = isc_vax_integer(&isc_info_buff[3], (short)length);
	} else {
		statement = 0;
	}
	/* Describe the parameters */
	isc_dsql_describe_bind(isc_status, &fb_cursor->stmt, 1, fb_cursor->i_sqlda);
	fb_error_check(isc_status);

	isc_dsql_describe(isc_status, &fb_cursor->stmt, 1, fb_cursor->o_sqlda);
	fb_error_check(isc_status);

	/* Get the number of parameters and reallocate the SQLDA */
	in_params = fb_cursor->i_sqlda->sqld;
	if (fb_cursor->i_sqlda->sqln < in_params) {
		xfree(fb_cursor->i_sqlda);
		fb_cursor->i_sqlda = sqlda_alloc(in_params);
		/* Describe again */
		isc_dsql_describe_bind(isc_status, &fb_cursor->stmt, 1, fb_cursor->i_sqlda);
		fb_error_check(isc_status);
	}

    /* Get the size of parameters buffer and reallocate it */
	if (in_params) {
		length = calculate_buffsize(fb_cursor->i_sqlda);
		if (length > fb_cursor->i_buffer_size) {
			fb_cursor->i_buffer = xrealloc(fb_cursor->i_buffer, length);
			fb_cursor->i_buffer_size = length;
		}
	}

    /* Execute the SQL statement if it is not query */
	if (!fb_cursor->o_sqlda->sqld) {
		if (statement == isc_info_sql_stmt_start_trans) {
			rb_raise(rb_eFbError, "use Fb::Connection#transaction()");
		} else if (statement == isc_info_sql_stmt_commit) {
			rb_raise(rb_eFbError, "use Fb::Connection#commit()");
		} else if (statement == isc_info_sql_stmt_rollback) {
			rb_raise(rb_eFbError, "use Fb::Connection#rollback()");
		} else if (in_params) {
			fb_cursor_execute_withparams(fb_cursor, RARRAY(args)->len, RARRAY(args)->ptr);
		} else {
			isc_dsql_execute2(isc_status, &fb_connection->transact, &fb_cursor->stmt, SQLDA_VERSION1, NULL, NULL);
			fb_error_check(isc_status);
		}
		rows_affected = cursor_rows_affected(fb_cursor, statement);
		result = INT2NUM(rows_affected);
	} else {
		/* Open cursor if the SQL statement is query */
		/* Get the number of columns and reallocate the SQLDA */
		cols = fb_cursor->o_sqlda->sqld;
		if (fb_cursor->o_sqlda->sqln < cols) {
			xfree(fb_cursor->o_sqlda);
			fb_cursor->o_sqlda = sqlda_alloc(cols);
			/* Describe again */
			isc_dsql_describe(isc_status, &fb_cursor->stmt, 1, fb_cursor->o_sqlda);
			fb_error_check(isc_status);
		}

		if (in_params) {
			fb_cursor_set_inputparams(fb_cursor, RARRAY(args)->len, RARRAY(args)->ptr);
		}

		/* Open cursor */
		isc_dsql_execute2(isc_status, &fb_connection->transact, &fb_cursor->stmt, SQLDA_VERSION1, in_params ? fb_cursor->i_sqlda : NULL, NULL);
		fb_error_check(isc_status);
		fb_cursor->open = Qtrue;

		/* Get the size of results buffer and reallocate it */
		length = calculate_buffsize(fb_cursor->o_sqlda);
		if (length > fb_cursor->o_buffer_size) {
			fb_cursor->o_buffer = xrealloc(fb_cursor->o_buffer, length);
			fb_cursor->o_buffer_size = length;
		}

		/* Set the description attributes */
		fb_cursor->fields_ary = fb_cursor_fields_ary(fb_cursor->o_sqlda, fb_connection->downcase_names);
		fb_cursor->fields_hash = fb_cursor_fields_hash(fb_cursor->fields_ary);
	}
	return result;
}

/* call-seq:
 *   execute(sql, *args) -> nil or rows affected
 *
 * This function is no longer published.
 */
static VALUE cursor_execute(int argc, VALUE* argv, VALUE self)
{
	struct FbCursor *fb_cursor;
	struct FbConnection *fb_connection;
	long isc_status[20];
	VALUE args;

	if (argc < 1) {
		rb_raise(rb_eArgError, "At least 1 argument required.");
	}

	args = rb_ary_new4(argc, argv);
	rb_ary_push(args, self);

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	fb_connection_check(fb_connection);

	if (fb_cursor->open) {
		isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_close);
		fb_error_check(isc_status);
		fb_cursor->open = Qfalse;
	}

	if (!fb_connection->transact) {
		VALUE result;
		int state;

		fb_connection_transaction_start(fb_connection, Qnil);
		fb_cursor->auto_transact = fb_connection->transact;

		result = rb_protect(cursor_execute2, args, &state);
		if (state) {
			fb_connection_rollback(fb_connection);
			return rb_funcall(rb_mKernel, rb_intern("raise"), 0);
		} else if (result != Qnil) {
			fb_connection_commit(fb_connection);
			return result;
		} else {
			return result;
		}
	} else {
		return cursor_execute2(args);
	}
}

static VALUE fb_hash_from_ary(VALUE fields, VALUE row)
{
	VALUE hash = rb_hash_new();
	int i;
	for (i = 0; i < RARRAY(fields)->len; i++) {
		VALUE field = rb_ary_entry(fields, i);
		VALUE name = rb_struct_aref(field, LONG2NUM(0));
		VALUE v = rb_ary_entry(row, i);
		rb_hash_aset(hash, name, v);
	}
	return hash;
}

static int hash_format(int argc, VALUE *argv)
{
	if (argc == 0 || argv[0] == ID2SYM(rb_intern("array"))) {
		return 0;
	} else if (argv[0] == ID2SYM(rb_intern("hash"))) {
		return 1;
	} else {
		rb_raise(rb_eFbError, "Unknown format");
	}
}

/* call-seq:
 *   fetch() -> Array
 *   fetch(:array) -> Array
 *   fetch(:hash) -> Hash
 *
 * Reads and returns a single row from the open cursor in either an Array or a Hash,
 * where the column names or aliases from the query form the keys.
 * If the +downcase_names+ attribute of the associated connection evaluates to true,
 * the keys are lower case, except where the column name was mixed case to begin with.
 */
static VALUE cursor_fetch(int argc, VALUE* argv, VALUE self)
{
	VALUE ary;
	struct FbCursor *fb_cursor;

	int hash_row = hash_format(argc, argv);

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	fb_cursor_fetch_prep(fb_cursor);

	ary = fb_cursor_fetch(fb_cursor);
	return hash_row ? fb_hash_from_ary(fb_cursor->fields_ary, ary) : ary;
}

/* call-seq:
 *   fetchall() -> Array of Arrays
 *   fetchall(:array) -> Array of Arrays
 *   fetchall(:hash) -> Array of Hashes
 *
 * Returns the remainder of the rows from the open cursor, with each row represented
 * by either an Array or a Hash, where the column names or aliases from the query form the keys.
 * If the +downcase_names+ attribute of the associated connection evaluates to true,
 * the keys are lower case, except where the column name was mixed case to begin with.
 */
static VALUE cursor_fetchall(int argc, VALUE* argv, VALUE self)
{
	VALUE ary, row;
	struct FbCursor *fb_cursor;

	int hash_rows = hash_format(argc, argv);

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	fb_cursor_fetch_prep(fb_cursor);

	ary = rb_ary_new();
	for (;;) {
		row = fb_cursor_fetch(fb_cursor);
		if (NIL_P(row)) break;
		if (hash_rows) {
			rb_ary_push(ary, fb_hash_from_ary(fb_cursor->fields_ary, row));
		} else {
			rb_ary_push(ary, row);
		}
	}

	return ary;
}

/* call-seq:
 *   each() {|Array| } -> nil
 *   each(:array) {|Array| } -> nil
 *   each(:hash) {|Hash| } -> nil
 *
 * Iterates the rows from the open cursor, passing each one to a block in either
 * an Array or a Hash, where the column names or aliases from the query form the keys.
 * If the +downcase_names+ attribute of the associated connection evaluates to true,
 * the keys are lower case, except where the column name was mixed case to begin with.
 */
static VALUE cursor_each(int argc, VALUE* argv, VALUE self)
{
	VALUE ary, row;
	struct FbCursor *fb_cursor;

	int hash_rows = hash_format(argc, argv);

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	fb_cursor_fetch_prep(fb_cursor);

	for (;;) {
		row = fb_cursor_fetch(fb_cursor);
		if (NIL_P(row)) break;
		if (hash_rows) {
			rb_yield(fb_hash_from_ary(fb_cursor->fields_ary, row));
		} else {
			rb_yield(row);
		}
	}

	return Qnil;
}

/* call-seq:
 *   close(sql, *args) -> nil
 *
 * Closes the cursor.  If a transaction was automatically started for this cursor, the transaction is commited.
 */
static VALUE cursor_close(VALUE self)
{
	struct FbCursor *fb_cursor;
	struct FbConnection *fb_connection;
	long isc_status[20];

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	fb_cursor_check(fb_cursor);

	/* Close the cursor */
	if (fb_cursor->stmt) {
		isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_close);
		fb_error_check_warn(isc_status);
		isc_dsql_free_statement(isc_status, &fb_cursor->stmt, DSQL_drop);
		fb_error_check(isc_status);
		fb_cursor->open = Qfalse;
		if (fb_connection->transact == fb_cursor->auto_transact) {
			isc_commit_transaction(isc_status, &fb_connection->transact);
			fb_error_check(isc_status);
			fb_cursor->auto_transact = fb_connection->transact;
		}
	}
	fb_cursor->fields_ary = Qnil;
	fb_cursor->fields_hash = Qnil;
	return Qnil;
}

/* call-seq:
 *   drop() -> nil
 *
 * Drops the cursor.
 *
 * TODO: How is this different from close()?
 */
static VALUE cursor_drop(VALUE self)
{
	struct FbCursor *fb_cursor;
	struct FbConnection *fb_connection;
	int i;

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	fb_cursor_drop(fb_cursor);
	fb_cursor->fields_ary = Qnil;
	fb_cursor->fields_hash = Qnil;

	/* reset the reference from connection */
	Data_Get_Struct(fb_cursor->connection, struct FbConnection, fb_connection);
	for (i = 0; i < RARRAY(fb_connection->cursor)->len; i++) {
		if (RARRAY(fb_connection->cursor)->ptr[i] == self) {
			RARRAY(fb_connection->cursor)->ptr[i] = Qnil;
		}
	}

	return Qnil;
}

/* call-seq:
 *   fields() -> Array
 *   fields(:array) -> Array
 *   fields(:hash) -> Hash
 *
 * Return an array of Field Structs or a hash indexed by field name.
 * If the +downcase_names+ attribute of the associated connection evaluates to true,
 * the keys are lower case, except where the column name was mixed case to begin with.
 */
static VALUE cursor_fields(int argc, VALUE* argv, VALUE self)
{
	struct FbCursor *fb_cursor;

	Data_Get_Struct(self, struct FbCursor, fb_cursor);
	if (argc == 0 || argv[0] == ID2SYM(rb_intern("array"))) {
		return fb_cursor->fields_ary;
	} else if (argv[0] == ID2SYM(rb_intern("hash"))) {
		return fb_cursor->fields_hash;
	} else {
		rb_raise(rb_eFbError, "Unknown format");
	}
}

/* call-seq:
 *   error_code -> int
 *
 * Returns the sqlcode associated with the error.
 */
static VALUE error_error_code(VALUE error)
{
	rb_p(error);
	return rb_iv_get(error, "error_code");
}

static char* dbp_create(int *length)
{
	char *dbp = ALLOC_N(char, 1);
	*dbp = isc_dpb_version1;
	*length = 1;
	return dbp;
}

static char* dbp_add_string(char *dbp, char isc_dbp_code, char *s, int *length)
{
	char *buf;
	int old_length = *length;
	int s_len = strlen(s);
	*length += 2 + s_len;
	REALLOC_N(dbp, char, *length);
	buf = dbp + old_length;
	*buf++ = isc_dbp_code;
	*buf++ = (char)s_len;
	memcpy(buf, s, s_len);
	return dbp;
}

static char* connection_create_dbp(VALUE self, int *length)
{
	char *dbp;
	VALUE username, password, charset, role;

	username = rb_iv_get(self, "@username");
	Check_Type(username, T_STRING);
	password = rb_iv_get(self, "@password");
	Check_Type(password, T_STRING);
	role = rb_iv_get(self, "@role");
	charset = rb_iv_get(self, "@charset");

	dbp = dbp_create(length);
	dbp = dbp_add_string(dbp, isc_dpb_user_name, STR2CSTR(username), length);
	dbp = dbp_add_string(dbp, isc_dpb_password, STR2CSTR(password), length);
	if (!NIL_P(charset)) {
		dbp = dbp_add_string(dbp, isc_dpb_lc_ctype, STR2CSTR(charset), length);
	}
	if (!NIL_P(role)) {
		dbp = dbp_add_string(dbp, isc_dpb_sql_role_name, STR2CSTR(role), length);
	}
	return dbp;
}

static char* CONNECTION_PARMS[] = {
	"@database",
	"@username",
	"@password",
	"@charset",
	"@role",
	"@downcase_names",
	(char *)0
};

static VALUE connection_create(isc_db_handle handle, VALUE db)
{
	unsigned short dialect;
	unsigned short db_dialect;
	VALUE downcase_names;
	char *parm;
	int i;
	struct FbConnection *fb_connection;
	VALUE connection = Data_Make_Struct(rb_cFbConnection, struct FbConnection, fb_connection_mark, fb_connection_free, fb_connection);
	fb_connection->db = handle;
	fb_connection->transact = 0;
	fb_connection->cursor = rb_ary_new();
	connection_count++;
	fb_connection->next = fb_connection_list;
	fb_connection_list = fb_connection;

	dialect = SQL_DIALECT_CURRENT;
	db_dialect = fb_connection_db_SQL_Dialect(fb_connection);

	if (db_dialect < dialect) {
		dialect = db_dialect;
		/* TODO: downgrade warning */
	}

	fb_connection->dialect = dialect;
	fb_connection->db_dialect = db_dialect;
	downcase_names = rb_iv_get(db, "@downcase_names");
	fb_connection->downcase_names = RTEST(downcase_names);

	for (i = 0; parm = CONNECTION_PARMS[i]; i++) {
		rb_iv_set(connection, parm, rb_iv_get(db, parm));
	}

	return connection;
}

static VALUE connection_names(VALUE self, char *sql)
{
	VALUE row;
	VALUE query = rb_str_new2(sql);
	VALUE cursor = connection_execute(1, &query, self);
	VALUE names = rb_ary_new();
	ID id_rstrip_bang = rb_intern("rstrip!");
	struct FbConnection *fb_connection;
	VALUE re_lowercase;
	ID id_matches, id_downcase_bang;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	if (fb_connection->downcase_names) {
		re_lowercase = rb_reg_regcomp(rb_str_new2("[[:lower:]]"));
		id_matches = rb_intern("=~");
		id_downcase_bang = rb_intern("downcase!");
	}

	while ((row = cursor_fetch(0, NULL, cursor)) != Qnil) {
		VALUE name = rb_ary_entry(row, 0);
		if (fb_connection->downcase_names && rb_funcall(re_lowercase, id_matches, 1, name) == Qnil) {
			rb_funcall(name, id_downcase_bang, 0);
		}
		rb_funcall(name, id_rstrip_bang, 0);
		rb_ary_push(names, name);
	}

	cursor_close(cursor);
	return names;
}

/* call-seq:
 *   table_names() -> array
 *
 * Returns sorted array of table names in connected database.
 */
static VALUE connection_table_names(VALUE self)
{
	char *sql = "SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
				"WHERE (RDB$SYSTEM_FLAG <> 1 OR RDB$SYSTEM_FLAG IS NULL) AND RDB$VIEW_BLR IS NULL "
				"ORDER BY RDB$RELATION_NAME";
	return connection_names(self, sql);
}

/* call-seq:
 *   generator_names() -> array
 *
 * Returns sorted array of generator names in connected database.
 */
static VALUE connection_generator_names(VALUE self)
{
	char *sql = "SELECT RDB$GENERATOR_NAME FROM RDB$GENERATORS "
				"WHERE (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG <> 1) "
				"ORDER BY RDB$GENERATOR_NAME";
	return connection_names(self, sql);
}

/* call-seq:
 *   view_names() -> array
 *
 * Returns sorted array of view names in connected database.
 */
static VALUE connection_view_names(VALUE self)
{
	char *sql = "SELECT RDB$RELATION_NAME, RDB$OWNER_NAME, RDB$VIEW_SOURCE FROM RDB$RELATIONS "
				"WHERE (RDB$SYSTEM_FLAG <> 1 OR RDB$SYSTEM_FLAG IS NULL) AND NOT RDB$VIEW_BLR IS NULL AND RDB$FLAGS = 1 "
				"ORDER BY RDB$RELATION_ID";
	return connection_names(self, sql);
}

/* call-seq:
 *   role_names() -> array
 *
 * Returns sorted array of role names in connected database.
 */
static VALUE connection_role_names(VALUE self)
{
	char *sql = "SELECT * FROM RDB$ROLES ORDER BY RDB$ROLE_NAME";
	return connection_names(self, sql);
}

/* call-seq:
 *   procedure_names() -> array
 *
 * Returns sorted array of stored procedure names in connected database.
 */
static VALUE connection_procedure_names(VALUE self)
{
	char *sql = "SELECT RDB$PROCEDURE_NAME FROM RDB$PROCEDURES "
				"ORDER BY RDB$PROCEDURE_NAME";
	return connection_names(self, sql);
}

char *p(char *prompt, VALUE s)
{
	char *sz;
	if (TYPE(s) != T_STRING) {
		s = rb_funcall(s, rb_intern("to_s"), 0);
	}
	sz = StringValuePtr(s);
	printf("%s: %s\n", prompt, sz);
	return sz;
}

static VALUE connection_index_columns(VALUE self, VALUE index_name)
{
	char *sql_columns = "SELECT * "
						"FROM RDB$INDEX_SEGMENTS "
						"WHERE RDB$INDEX_SEGMENTS.RDB$INDEX_NAME = ? "
						"ORDER BY RDB$INDEX_SEGMENTS.RDB$FIELD_POSITION";
	ID id_rstrip_bang = rb_intern("rstrip!");
	VALUE query_columns = rb_str_new2(sql_columns);
	VALUE query_parms[] = { query_columns, index_name };
	VALUE result = connection_query(2, query_parms, self);
	VALUE columns = rb_ary_new();
	VALUE re_lowercase;
	ID id_matches, id_downcase_bang;
	int i;
	struct FbConnection *fb_connection;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	if (fb_connection->downcase_names) {
		re_lowercase = rb_reg_regcomp(rb_str_new2("[[:lower:]]"));
		id_matches = rb_intern("=~");
		id_downcase_bang = rb_intern("downcase!");
	}

	for (i = 0; i < RARRAY(result)->len; i++) {
		VALUE row = rb_ary_entry(result, i);
		VALUE name = rb_ary_entry(row, 1);
		rb_funcall(name, id_rstrip_bang, 0);
		if (fb_connection->downcase_names && rb_funcall(re_lowercase, id_matches, 1, name) == Qnil) {
			rb_funcall(name, id_downcase_bang, 0);
		}
		rb_ary_push(columns, name);
	}
	return columns;
}

/* call-seq:
 *   indexes() -> Hash
 *
 * Returns a hash of indexes, keyed by index name.
 */
static VALUE connection_indexes(VALUE self)
{
	char *sql_indexes = "SELECT RDB$INDICES.RDB$RELATION_NAME, RDB$INDICES.RDB$INDEX_NAME, RDB$INDICES.RDB$UNIQUE_FLAG, RDB$INDICES.RDB$INDEX_TYPE "
						"FROM RDB$INDICES "
						"  JOIN RDB$RELATIONS ON RDB$INDICES.RDB$RELATION_NAME = RDB$RELATIONS.RDB$RELATION_NAME "
						"WHERE (RDB$RELATIONS.RDB$SYSTEM_FLAG <> 1 OR RDB$RELATIONS.RDB$SYSTEM_FLAG IS NULL) ";
	ID id_rstrip_bang = rb_intern("rstrip!");
	VALUE query_indexes = rb_str_new2(sql_indexes);
	VALUE ary_indexes = connection_query(1, &query_indexes, self);
	VALUE indexes = rb_hash_new();
	VALUE re_lowercase;
	ID id_matches, id_downcase_bang;
	int i;
	struct FbConnection *fb_connection;
	Data_Get_Struct(self, struct FbConnection, fb_connection);

	if (fb_connection->downcase_names) {
		re_lowercase = rb_reg_regcomp(rb_str_new2("[[:lower:]]"));
		id_matches = rb_intern("=~");
		id_downcase_bang = rb_intern("downcase!");
	}

	for (i = 0; i < RARRAY(ary_indexes)->len; i++) {
		VALUE index_struct;
		VALUE row = rb_ary_entry(ary_indexes, i);
		VALUE table_name = rb_ary_entry(row, 0);
		VALUE index_name = rb_ary_entry(row, 1);
		VALUE unique = rb_ary_entry(row, 2);
		VALUE descending = rb_ary_entry(row, 3);
		VALUE columns = connection_index_columns(self, index_name);

		rb_funcall(table_name, id_rstrip_bang, 0);
		rb_funcall(index_name, id_rstrip_bang, 0);

		if (fb_connection->downcase_names) {
			if (rb_funcall(re_lowercase, id_matches, 1, table_name) == Qnil) {
				rb_funcall(table_name, id_downcase_bang, 0);
			}
			if (rb_funcall(re_lowercase, id_matches, 1, index_name) == Qnil) {
				rb_funcall(index_name, id_downcase_bang, 0);
			}
		}

		rb_str_freeze(table_name);
		rb_str_freeze(index_name);

		unique = RTEST(unique) ? Qtrue : Qfalse;
		descending = RTEST(descending) ? Qtrue : Qfalse;

		index_struct = rb_struct_new(rb_sFbIndex, table_name, index_name, unique, descending, columns);
		rb_hash_aset(indexes, index_name, index_struct);
	}
	return indexes;
}

/* call-seq:
 *   from_code(code, subtype) -> String
 *
 * Returns the SQL type, such as VARCHAR or INTEGER for a given type code and subtype.
 */
static VALUE sql_type_from_code(VALUE self, VALUE code, VALUE subtype)
{
	return fb_sql_type_from_code(NUM2INT(code), NUM2INT(subtype));
}

static void define_attrs(VALUE klass, char **attrs)
{
	char *parm;
	while (parm = *attrs)
	{
		rb_define_attr(klass, parm+1, 1, 1);
		attrs++;
	}
}

static VALUE default_string(VALUE hash, char *key, char *def)
{
	VALUE sym = ID2SYM(rb_intern(key));
	VALUE val = rb_hash_aref(hash, sym);
	return NIL_P(val) ? rb_str_new2(def) : StringValue(val);
}

static VALUE default_int(VALUE hash, char *key, int def)
{
	VALUE sym = ID2SYM(rb_intern(key));
	VALUE val = rb_hash_aref(hash, sym);
	return NIL_P(val) ? INT2NUM(def) : val;
}

static VALUE database_allocate_instance(VALUE klass)
{
    NEWOBJ(obj, struct RObject);
    OBJSETUP(obj, klass, T_OBJECT);
    return (VALUE)obj;
}

static VALUE hash_from_connection_string(VALUE cs)
{
	VALUE hash = rb_hash_new();
	VALUE re_SemiColon = rb_reg_regcomp(rb_str_new2("\\s*;\\s*"));
	VALUE re_Equal = rb_reg_regcomp(rb_str_new2("\\s*=\\s*"));
	ID id_split = rb_intern("split");
	VALUE pairs = rb_funcall(cs, id_split, 1, re_SemiColon);
	int i;
	for (i = 0; i < RARRAY(pairs)->len; i++) {
		VALUE pair = rb_ary_entry(pairs, i);
		VALUE keyValue = rb_funcall(pair, id_split, 1, re_Equal);
		if (RARRAY(keyValue)->len == 2) {
			VALUE key = rb_ary_entry(keyValue, 0);
			VALUE val = rb_ary_entry(keyValue, 1);
			rb_hash_aset(hash, rb_str_intern(key), val);
		}
	}
	return hash;
}

static void check_page_size(int page_size)
{
	if (page_size != 1024 && page_size != 2048 && page_size != 4096 && page_size != 8192) {
		rb_raise(rb_eFbError, "Invalid page size: %d", page_size);
	}
}

/* call-seq:
 *   Database.new(options) -> Database
 *
 * Initialize Database with Hash of values:
 * :database:: Full Firebird connection string, e.g. 'localhost:/var/fbdata/drivertest.fdb' (required)
 * :username:: database username (default: 'sysdba')
 * :password:: database password (default: 'masterkey')
 * :charset:: character set to be used with the connection (default: 'NONE')
 * :role:: database role to connect using (default: nil)
 * :downcase_names:: Column names are reported in lowercase, unless they were originally mixed case (default: nil).
 * :page_size:: page size to use when creating a database (default: 1024)
 */
static VALUE database_initialize(int argc, VALUE *argv, VALUE self)
{
	VALUE parms, database;

	if (argc >= 1) {
		parms = argv[0];
		if (TYPE(parms) == T_STRING) {
			parms = hash_from_connection_string(parms);
		} else {
			Check_Type(parms, T_HASH);
		}
		database = rb_hash_aref(parms, ID2SYM(rb_intern("database")));
		if (NIL_P(database)) rb_raise(rb_eFbError, "Database must be specified.");
		rb_iv_set(self, "@database", database);
		rb_iv_set(self, "@username", default_string(parms, "username", "sysdba"));
		rb_iv_set(self, "@password", default_string(parms, "password", "masterkey"));
		rb_iv_set(self, "@charset", default_string(parms, "charset", "NONE"));
		rb_iv_set(self, "@role", rb_hash_aref(parms, ID2SYM(rb_intern("role"))));
		rb_iv_set(self, "@downcase_names", rb_hash_aref(parms, ID2SYM(rb_intern("downcase_names"))));
		rb_iv_set(self, "@page_size", default_int(parms, "page_size", 1024));
	}
	return self;
}

/* call-seq:
 *   create() -> Database
 *   create() {|connection| } -> Database
 *
 * Create a database using the current database options.
 * If a block is provided, an open connection to the new database is passed to it
 * before being automatically closed.
 */
static VALUE database_create(VALUE self)
{
	long isc_status[20];
	isc_db_handle handle = 0;
	isc_tr_handle local_transact = 0;
	VALUE parms, fmt, stmt;
	char *sql;

	VALUE database = rb_iv_get(self, "@database");
	VALUE username = rb_iv_get(self, "@username");
	VALUE password = rb_iv_get(self, "@password");
	VALUE page_size = rb_iv_get(self, "@page_size");
	VALUE charset = rb_iv_get(self, "@charset");

	check_page_size(NUM2INT(page_size));

	parms = rb_ary_new3(5, database, username, password, page_size, charset);

	fmt = rb_str_new2("CREATE DATABASE '%s' USER '%s' PASSWORD '%s' PAGE_SIZE = %d DEFAULT CHARACTER SET %s;");
	stmt = rb_funcall(fmt, rb_intern("%"), 1, parms);
	sql = StringValuePtr(stmt);

	if (isc_dsql_execute_immediate(isc_status, &handle, &local_transact, 0, sql, 3, NULL) != 0) {
		fb_error_check(isc_status);
	}
	if (handle) {
		if (rb_block_given_p()) {
			VALUE connection = connection_create(handle, self);
			rb_ensure(rb_yield,connection,connection_close,connection);
		} else {
			isc_detach_database(isc_status, &handle);
			fb_error_check(isc_status);
		}
	}

	return self;
}

/* call-seq:
 *   Database.create(options) -> Database
 *   Database.create(options) {|connection| } -> Database
 *
 * Create a database using the specified options (see: Database.new for details of options Hash).
 * If a block is provided, an open connection to the new database is passed to it
 * before being automatically closed.
 */
static VALUE database_s_create(int argc, VALUE *argv, VALUE klass)
{
	VALUE obj = database_allocate_instance(klass);
	database_initialize(argc, argv, obj);
	return database_create(obj);
}

/* call-seq:
 *   connect() -> Connection
 *   connect() {|connection| } -> nil
 *
 * Connect to the database specified by the current database options.
 *
 * If a block is provided, the open connection is passed to it before being
 * automatically closed.
 */
static VALUE database_connect(VALUE self)
{
	long isc_status[20];
	char *dbp;
	int length;
	isc_db_handle handle = NULL;
	VALUE database = rb_iv_get(self, "@database");
	Check_Type(database, T_STRING);
	dbp = connection_create_dbp(self, &length);
	isc_attach_database(isc_status, 0, STR2CSTR(database), &handle, length, dbp);
	xfree(dbp);
	fb_error_check(isc_status);
	{
		VALUE connection = connection_create(handle, self);
		if (rb_block_given_p()) {
			return rb_ensure(rb_yield, connection, connection_close, connection);
			return Qnil;
		} else {
			return connection;
		}
	}
}

/* call-seq:
 *   Database.connect(options) -> Connection
 *   Database.connect(options) {|connection| } -> nil
 *
 * Connect to a database using the options given (see: Database.new for details of options Hash).
 *
 * If a block is provided, the open connection is passed to it before being
 * automatically closed.
 */
static VALUE database_s_connect(int argc, VALUE *argv, VALUE klass)
{
	VALUE obj = database_allocate_instance(klass);
	database_initialize(argc, argv, obj);
	return database_connect(obj);
}

/* call-seq:
 *   drop() -> nil
 *
 * Drop the database specified by the current database options.
 */
static VALUE database_drop(VALUE self)
{
	long isc_status[20];
	struct FbConnection *fb_connection;

	VALUE connection = database_connect(self);
	Data_Get_Struct(connection, struct FbConnection, fb_connection);
	isc_drop_database(isc_status, &fb_connection->db);
	fb_error_check(isc_status);
	fb_connection_remove(fb_connection);
	return Qnil;
}

/* call-seq:
 *   Database.drop(options) -> nil
 *
 * Drop the database specified by the options given (see: Database.new for details of options Hash).
 */
static VALUE database_s_drop(int argc, VALUE *argv, VALUE klass)
{
	VALUE obj = database_allocate_instance(klass);
	database_initialize(argc, argv, obj);
	return database_drop(obj);
}

void Init_fb()
{
	rb_mFb = rb_define_module("Fb");

	rb_cFbDatabase = rb_define_class_under(rb_mFb, "Database", rb_cData);
    rb_define_alloc_func(rb_cFbDatabase, database_allocate_instance);
    rb_define_method(rb_cFbDatabase, "initialize", database_initialize, -1);
	rb_define_attr(rb_cFbDatabase, "database", 1, 1);
	rb_define_attr(rb_cFbDatabase, "username", 1, 1);
	rb_define_attr(rb_cFbDatabase, "password", 1, 1);
	rb_define_attr(rb_cFbDatabase, "charset", 1, 1);
	rb_define_attr(rb_cFbDatabase, "role", 1, 1);
	rb_define_attr(rb_cFbDatabase, "downcase_names", 1, 1);
	rb_define_attr(rb_cFbDatabase, "page_size", 1, 1);
    rb_define_method(rb_cFbDatabase, "create", database_create, 0);
	rb_define_singleton_method(rb_cFbDatabase, "create", database_s_create, -1);
	rb_define_method(rb_cFbDatabase, "connect", database_connect, 0);
	rb_define_singleton_method(rb_cFbDatabase, "connect", database_s_connect, -1);
	rb_define_method(rb_cFbDatabase, "drop", database_drop, 0);
	rb_define_singleton_method(rb_cFbDatabase, "drop", database_s_drop, -1);

	rb_cFbConnection = rb_define_class_under(rb_mFb, "Connection", rb_cData);
	rb_define_attr(rb_cFbConnection, "database", 1, 1);
	rb_define_attr(rb_cFbConnection, "username", 1, 1);
	rb_define_attr(rb_cFbConnection, "password", 1, 1);
	rb_define_attr(rb_cFbConnection, "charset", 1, 1);
	rb_define_attr(rb_cFbConnection, "role", 1, 1);
	rb_define_attr(rb_cFbConnection, "downcase_names", 1, 1);
	rb_define_method(rb_cFbConnection, "to_s", connection_to_s, 0);
	rb_define_method(rb_cFbConnection, "execute", connection_execute, -1);
	rb_define_method(rb_cFbConnection, "query", connection_query, -1);
	rb_define_method(rb_cFbConnection, "transaction", connection_transaction, -1);
	rb_define_method(rb_cFbConnection, "transaction_started", connection_transaction_started, 0);
	rb_define_method(rb_cFbConnection, "commit", connection_commit, 0);
	rb_define_method(rb_cFbConnection, "rollback", connection_rollback, 0);
	rb_define_method(rb_cFbConnection, "close", connection_close, 0);
	rb_define_method(rb_cFbConnection, "drop", connection_drop, 0);
	rb_define_method(rb_cFbConnection, "open?", connection_is_open, 0);
	rb_define_method(rb_cFbConnection, "dialect", connection_dialect, 0);
	rb_define_method(rb_cFbConnection, "db_dialect", connection_db_dialect, 0);
	rb_define_method(rb_cFbConnection, "table_names", connection_table_names, 0);
	rb_define_method(rb_cFbConnection, "generator_names", connection_generator_names, 0);
	rb_define_method(rb_cFbConnection, "view_names", connection_view_names, 0);
	rb_define_method(rb_cFbConnection, "role_names", connection_role_names, 0);
	rb_define_method(rb_cFbConnection, "procedure_names", connection_procedure_names, 0);
	rb_define_method(rb_cFbConnection, "indexes", connection_indexes, 0);
	//rb_define_method(rb_cFbConnection, "cursor", connection_cursor, 0);

	rb_cFbCursor = rb_define_class_under(rb_mFb, "Cursor", rb_cData);
	//rb_define_method(rb_cFbCursor, "execute", cursor_execute, -1);
	rb_define_method(rb_cFbCursor, "fields", cursor_fields, -1);
	rb_define_method(rb_cFbCursor, "fetch", cursor_fetch, -1);
	rb_define_method(rb_cFbCursor, "fetchall", cursor_fetchall, -1);
	rb_define_method(rb_cFbCursor, "each", cursor_each, -1);
	rb_define_method(rb_cFbCursor, "close", cursor_close, 0);
	rb_define_method(rb_cFbCursor, "drop", cursor_drop, 0);

	rb_cFbSqlType = rb_define_class_under(rb_mFb, "SqlType", rb_cData);
	rb_define_singleton_method(rb_cFbSqlType, "from_code", sql_type_from_code, 2);

/*
//	rb_cFbGlobal = rb_define_class_under(rb_mFb, "Global", rb_cData);
//	rb_define_singleton_method(rb_cFbGlobal, "transaction", global_transaction, -1);
//	rb_define_singleton_method(rb_cFbGlobal, "transaction_started", global_transaction_started, 0);
//	rb_define_singleton_method(rb_cFbGlobal, "commit", global_commit, 0);
//	rb_define_singleton_method(rb_cFbGlobal, "rollback", global_rollback, 0);
*/

	rb_eFbError = rb_define_class_under(rb_mFb, "Error", rb_eStandardError);
	rb_define_method(rb_eFbError, "error_code", error_error_code, 0);

	rb_sFbField = rb_struct_define("FbField", "name", "sql_type", "sql_subtype", "display_size", "internal_size", "precision", "scale", "nullable", "type_code", NULL);
	rb_sFbIndex = rb_struct_define("FbIndex", "table_name", "index_name", "unique", "descending", "columns", NULL);
	
	rb_require("date");
	rb_cDate = rb_const_get(rb_cObject, rb_intern("Date"));
}
