/*
  +----------------------------------------------------------------------+
  | PHP Version 7                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) The PHP Group                                          |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author: Wez Furlong <wez@php.net>                                    |
  +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "pdo/php_pdo.h"
#include "pdo/php_pdo_driver.h"
#include "php_pdo_oci.h"
#include "php_pdo_oci_int.h"
#include "Zend/zend_extensions.h"

#define PDO_OCI_LOBMAXSIZE (4294967295UL) /* OCI_LOBMAXSIZE */

#define STMT_CALL(name, params)											\
	do {																\
		S->last_err = name params;										\
		S->last_err = _oci_error(S->err, stmt->dbh, stmt, #name, S->last_err, FALSE, __FILE__, __LINE__); \
		if (S->last_err) {												\
			return 0;													\
		}																\
	} while(0)

#define STMT_CALL_MSG(name, msg, params)								\
	do { 																\
		S->last_err = name params;										\
		S->last_err = _oci_error(S->err, stmt->dbh, stmt, #name ": " #msg, S->last_err, FALSE, __FILE__, __LINE__); \
		if (S->last_err) {												\
			return 0;													\
		}																\
	} while(0)

static php_stream *oci_create_lob_stream(zval *dbh, pdo_stmt_t *stmt, OCILobLocator *lob);

#define OCI_TEMPLOB_CLOSE(envhp, svchp, errhp, lob)				\
	do															\
	{															\
		boolean isTempLOB;										\
		OCILobIsTemporary(envhp, errhp, lob, &isTempLOB);		\
		if (isTempLOB)											\
			OCILobFreeTemporary(svchp, errhp, lob);				\
	} while(0)

static int oci_stmt_dtor(pdo_stmt_t *stmt) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
	HashTable *BC = stmt->bound_columns;
	HashTable *BP = stmt->bound_params;

	int i;

	if (S->stmt) {
		/* cancel server side resources for the statement if we didn't
		 * fetch it all */
		OCIStmtFetch(S->stmt, S->err, 0, OCI_FETCH_NEXT, OCI_DEFAULT);

		/* free the handle */
		OCIHandleFree(S->stmt, OCI_HTYPE_STMT);
		S->stmt = NULL;
	}
	if (S->err) {
		OCIHandleFree(S->err, OCI_HTYPE_ERROR);
		S->err = NULL;
	}

	/* need to ensure these go away now */
	if (BC) {
		zend_hash_destroy(BC);
		FREE_HASHTABLE(stmt->bound_columns);
		stmt->bound_columns = NULL;
	}

	if (BP) {
		zend_hash_destroy(BP);
		FREE_HASHTABLE(stmt->bound_params);
		stmt->bound_params = NULL;
	}

	if (S->einfo.errmsg) {
		pefree(S->einfo.errmsg, stmt->dbh->is_persistent);
		S->einfo.errmsg = NULL;
	}

	if (S->cols) {
		for (i = 0; i < stmt->column_count; i++) {
			if (S->cols[i].data) {
				switch (S->cols[i].dtype) {
					case SQLT_BLOB:
					case SQLT_CLOB:
						OCI_TEMPLOB_CLOSE(S->H->env, S->H->svc, S->H->err,
							(OCILobLocator *) S->cols[i].data);
						OCIDescriptorFree(S->cols[i].data, OCI_DTYPE_LOB);
						break;
					default:
						efree(S->cols[i].data);
				}
			}
		}
		efree(S->cols);
		S->cols = NULL;
	}
	efree(S);

	stmt->driver_data = NULL;

	return 1;
} /* }}} */

static int oci_stmt_execute(pdo_stmt_t *stmt) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
	ub4 rowcount;
	b4 mode;

	if (!S->stmt_type) {
		STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_STMT_TYPE",
				(S->stmt, OCI_HTYPE_STMT, &S->stmt_type, 0, OCI_ATTR_STMT_TYPE, S->err));
	}

	if (stmt->executed) {
		/* ensure that we cancel the cursor from a previous fetch */
		OCIStmtFetch(S->stmt, S->err, 0, OCI_FETCH_NEXT, OCI_DEFAULT);
	}

#ifdef OCI_STMT_SCROLLABLE_READONLY /* needed for oci8 ? */
	if (S->exec_type == OCI_STMT_SCROLLABLE_READONLY) {
		mode = OCI_STMT_SCROLLABLE_READONLY;
	} else
#endif
	if (stmt->dbh->auto_commit && !stmt->dbh->in_txn) {
		mode = OCI_COMMIT_ON_SUCCESS;
	} else {
		mode = OCI_DEFAULT;
	}

	STMT_CALL(OCIStmtExecute, (S->H->svc, S->stmt, S->err,
				(S->stmt_type == OCI_STMT_SELECT && !S->have_blobs) ? 0 : 1, 0, NULL, NULL,
				mode));

	if (!stmt->executed) {
		ub4 colcount;
		/* do first-time-only definition of bind/mapping stuff */

		/* how many columns do we have ? */
		STMT_CALL_MSG(OCIAttrGet, "ATTR_PARAM_COUNT",
				(S->stmt, OCI_HTYPE_STMT, &colcount, 0, OCI_ATTR_PARAM_COUNT, S->err));

		stmt->column_count = (int)colcount;

		if (S->cols) {
			int i;
			for (i = 0; i < stmt->column_count; i++) {
				if (S->cols[i].data) {
					switch (S->cols[i].dtype) {
						case SQLT_BLOB:
						case SQLT_CLOB:
							/* do nothing */
							break;
						default:
							efree(S->cols[i].data);
					}
				}
			}
			efree(S->cols);
		}

		S->cols = ecalloc(colcount, sizeof(pdo_oci_column));
	}

	STMT_CALL_MSG(OCIAttrGet, "ATTR_ROW_COUNT",
			(S->stmt, OCI_HTYPE_STMT, &rowcount, 0, OCI_ATTR_ROW_COUNT, S->err));
	stmt->row_count = (long)rowcount;

	return 1;
} /* }}} */

static sb4 oci_bind_input_cb(dvoid *ctx, OCIBind *bindp, ub4 iter, ub4 index, dvoid **bufpp, ub4 *alenp, ub1 *piecep, dvoid **indpp) /* {{{ */
{
	struct pdo_bound_param_data *param = (struct pdo_bound_param_data*)ctx;
	pdo_oci_bound_param *P = (pdo_oci_bound_param*)param->driver_data;
	zval *parameter;

	if (!param) {
		php_error_docref(NULL, E_WARNING, "param is NULL in oci_bind_input_cb; this should not happen");
		return OCI_ERROR;
	}

	*indpp = &P->indicator;

    if (Z_ISREF(param->parameter))
		parameter = Z_REFVAL(param->parameter);
	else
		parameter = &param->parameter;

	if (P->thing) {
		*bufpp = P->thing;
		*alenp = sizeof(void*);
	} else if (ZVAL_IS_NULL(parameter)) {
		/* insert a NULL value into the column */
		P->indicator = -1; /* NULL */
		*bufpp = 0;
		*alenp = -1;
	} else if (!P->thing) {
		/* regular string bind */
		if (!try_convert_to_string(parameter)) {
			return OCI_ERROR;
		}
		*bufpp = Z_STRVAL_P(parameter);
		*alenp = (ub4) Z_STRLEN_P(parameter);
	}

	*piecep = OCI_ONE_PIECE;
	return OCI_CONTINUE;
} /* }}} */

static sb4 oci_bind_output_cb(dvoid *ctx, OCIBind *bindp, ub4 iter, ub4 index, dvoid **bufpp, ub4 **alenpp, ub1 *piecep, dvoid **indpp, ub2 **rcodepp) /* {{{ */
{
	struct pdo_bound_param_data *param = (struct pdo_bound_param_data*)ctx;
	pdo_oci_bound_param *P = (pdo_oci_bound_param*)param->driver_data;
	zval *parameter;

	if (!param) {
		php_error_docref(NULL, E_WARNING, "param is NULL in oci_bind_output_cb; this should not happen");
		return OCI_ERROR;
	}

	if (Z_ISREF(param->parameter))
        parameter = Z_REFVAL(param->parameter);
    else
        parameter = &param->parameter;

	if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB) {
		P->actual_len = sizeof(OCILobLocator*);
		*bufpp = P->thing;
		*alenpp = &P->actual_len;
		*piecep = OCI_ONE_PIECE;
		*rcodepp = &P->retcode;
		*indpp = &P->indicator;
		return OCI_CONTINUE;
	}

	if (Z_TYPE_P(parameter) == IS_OBJECT || Z_TYPE_P(parameter) == IS_RESOURCE) {
		return OCI_CONTINUE;
	}

	zval_ptr_dtor(parameter);

	Z_STR_P(parameter) = zend_string_alloc(param->max_value_len, 1);
	P->used_for_output = 1;

	P->actual_len = (ub4) Z_STRLEN_P(parameter);
	*alenpp = &P->actual_len;
	*bufpp = (Z_STR_P(parameter))->val;
	*piecep = OCI_ONE_PIECE;
	*rcodepp = &P->retcode;
	*indpp = &P->indicator;

	return OCI_CONTINUE;
} /* }}} */

static int oci_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *param, enum pdo_param_event event_type) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;

	/* we're only interested in parameters for prepared SQL right now */
	if (param->is_param) {
		pdo_oci_bound_param *P;
		sb4 value_sz = -1;
		zval *parameter;

		if (Z_ISREF(param->parameter))
			parameter = Z_REFVAL(param->parameter);
		else
			parameter = &param->parameter;

		P = (pdo_oci_bound_param*)param->driver_data;

		switch (event_type) {
			case PDO_PARAM_EVT_FETCH_PRE:
			case PDO_PARAM_EVT_FETCH_POST:
			case PDO_PARAM_EVT_NORMALIZE:
				/* Do nothing */
				break;

			case PDO_PARAM_EVT_FREE:
				P = param->driver_data;
				if (P && P->thing) {
					OCI_TEMPLOB_CLOSE(S->H->env, S->H->svc, S->H->err, P->thing);
					OCIDescriptorFree(P->thing, OCI_DTYPE_LOB);
					P->thing = NULL;
					efree(P);
				}
				else if (P) {
					efree(P);
				}
				break;

			case PDO_PARAM_EVT_ALLOC:
				P = (pdo_oci_bound_param*)ecalloc(1, sizeof(pdo_oci_bound_param));
				param->driver_data = P;

				/* figure out what we're doing */
				switch (PDO_PARAM_TYPE(param->param_type)) {
					case PDO_PARAM_STMT:
						return 0;

					case PDO_PARAM_LOB:
						/* P->thing is now an OCILobLocator * */
						P->oci_type = SQLT_BLOB;
						value_sz = (sb4) sizeof(OCILobLocator*);
						break;

					case PDO_PARAM_STR:
					default:
						P->oci_type = SQLT_CHR;
						value_sz = (sb4) param->max_value_len;
						if (param->max_value_len == 0) {
							value_sz = (sb4) 1332; /* maximum size before value is interpreted as a LONG value */
						}

				}

				if (param->name) {
					STMT_CALL(OCIBindByName, (S->stmt,
							&P->bind, S->err, (text*)param->name->val,
							(sb4) param->name->len, 0, value_sz, P->oci_type,
							&P->indicator, 0, &P->retcode, 0, 0,
							OCI_DATA_AT_EXEC));
				} else {
					STMT_CALL(OCIBindByPos, (S->stmt,
							&P->bind, S->err, ((ub4)param->paramno)+1,
							0, value_sz, P->oci_type,
							&P->indicator, 0, &P->retcode, 0, 0,
							OCI_DATA_AT_EXEC));
				}

				STMT_CALL(OCIBindDynamic, (P->bind,
							S->err,
							param, oci_bind_input_cb,
							param, oci_bind_output_cb));

				return 1;

			case PDO_PARAM_EVT_EXEC_PRE:
				P->indicator = 0;
				P->used_for_output = 0;
				if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB) {
					ub4 empty = 0;
					STMT_CALL(OCIDescriptorAlloc, (S->H->env, &P->thing, OCI_DTYPE_LOB, 0, NULL));
					STMT_CALL(OCIAttrSet, (P->thing, OCI_DTYPE_LOB, &empty, 0, OCI_ATTR_LOBEMPTY, S->err));
					S->have_blobs = 1;
				}
				return 1;

			case PDO_PARAM_EVT_EXEC_POST:
				/* fixup stuff set in motion in oci_bind_output_cb */
				if (P->used_for_output) {
					if (P->indicator == -1) {
						/* set up a NULL value */
						if (Z_TYPE_P(parameter) == IS_STRING) {
							/* OCI likes to stick non-terminated strings in things */
							*Z_STRVAL_P(parameter) = '\0';
						}
						zval_ptr_dtor_str(parameter);
						ZVAL_UNDEF(parameter);
					} else if (Z_TYPE_P(parameter) == IS_STRING) {
						Z_STR_P(parameter) = zend_string_init(Z_STRVAL_P(parameter), P->actual_len, 1);
					}
				} else if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_LOB && P->thing) {
					php_stream *stm;

					if (Z_TYPE_P(parameter) == IS_NULL) {
						/* if the param is NULL, then we assume that they
						 * wanted to bind a lob locator into it from the query
						 * */

						stm = oci_create_lob_stream(&stmt->database_object_handle, stmt, (OCILobLocator*)P->thing);
						if (stm) {
							OCILobOpen(S->H->svc, S->err, (OCILobLocator*)P->thing, OCI_LOB_READWRITE);
							php_stream_to_zval(stm, parameter);
						}
					} else {
						/* we're a LOB being used for insert; transfer the data now */
						size_t n;
						ub4 amt, offset = 1;
						char *consume;

						php_stream_from_zval_no_verify(stm, parameter);
						if (stm) {
							OCILobOpen(S->H->svc, S->err, (OCILobLocator*)P->thing, OCI_LOB_READWRITE);
							do {
								char buf[8192];
								n = php_stream_read(stm, buf, sizeof(buf));
								if ((int)n <= 0) {
									break;
								}
								consume = buf;
								do {
									amt = (ub4) n;
									OCILobWrite(S->H->svc, S->err, (OCILobLocator*)P->thing,
											&amt, offset, consume, (ub4) n,
											OCI_ONE_PIECE,
											NULL, NULL, 0, SQLCS_IMPLICIT);
									offset += amt;
									n -= amt;
									consume += amt;
								} while (n);
							} while (1);
							OCILobClose(S->H->svc, S->err, (OCILobLocator*)P->thing);
							OCILobFlushBuffer(S->H->svc, S->err, (OCILobLocator*)P->thing, 0);
						} else if (Z_TYPE_P(parameter) == IS_STRING) {
							/* stick the string into the LOB */
							consume = Z_STRVAL_P(parameter);
							n = Z_STRLEN_P(parameter);
							if (n) {
								OCILobOpen(S->H->svc, S->err, (OCILobLocator*)P->thing, OCI_LOB_READWRITE);
								while (n) {
									amt = (ub4) n;
									OCILobWrite(S->H->svc, S->err, (OCILobLocator*)P->thing,
											&amt, offset, consume, (ub4) n,
											OCI_ONE_PIECE,
											NULL, NULL, 0, SQLCS_IMPLICIT);
									consume += amt;
									n -= amt;
								}
								OCILobClose(S->H->svc, S->err, (OCILobLocator*)P->thing);
							}
						}
						OCI_TEMPLOB_CLOSE(S->H->env, S->H->svc, S->H->err, P->thing);
						OCIDescriptorFree(P->thing, OCI_DTYPE_LOB);
						P->thing = NULL;
					}
				}

				return 1;
		}
	}

	return 1;
} /* }}} */

static int oci_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori,	zend_long offset) /* {{{ */
{
#if HAVE_OCISTMTFETCH2
	ub4 ociori;
#endif
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;

#if HAVE_OCISTMTFETCH2
	switch (ori) {
		case PDO_FETCH_ORI_NEXT:	ociori = OCI_FETCH_NEXT; break;
		case PDO_FETCH_ORI_PRIOR:	ociori = OCI_FETCH_PRIOR; break;
		case PDO_FETCH_ORI_FIRST:	ociori = OCI_FETCH_FIRST; break;
		case PDO_FETCH_ORI_LAST:	ociori = OCI_FETCH_LAST; break;
		case PDO_FETCH_ORI_ABS:		ociori = OCI_FETCH_ABSOLUTE; break;
		case PDO_FETCH_ORI_REL:		ociori = OCI_FETCH_RELATIVE; break;
	}
	S->last_err = OCIStmtFetch2(S->stmt, S->err, 1, ociori, (sb4) offset, OCI_DEFAULT);
#else
	S->last_err = OCIStmtFetch(S->stmt, S->err, 1, OCI_FETCH_NEXT, OCI_DEFAULT);
#endif

	if (S->last_err == OCI_NO_DATA) {
		/* no (more) data */
		return 0;
	}

	if (S->last_err == OCI_NEED_DATA) {
		oci_stmt_error("OCI_NEED_DATA");
		return 0;
	}

	if (S->last_err == OCI_SUCCESS_WITH_INFO || S->last_err == OCI_SUCCESS) {
		return 1;
	}

	oci_stmt_error("OCIStmtFetch");

	return 0;
} /* }}} */

static sb4 oci_define_callback(dvoid *octxp, OCIDefine *define, ub4 iter, dvoid **bufpp,
		ub4 **alenpp, ub1 *piecep, dvoid **indpp, ub2 **rcodepp)
{
	pdo_oci_column *col = (pdo_oci_column*)octxp;

	switch (col->dtype) {
		case SQLT_BLOB:
		case SQLT_CLOB:
			*piecep = OCI_ONE_PIECE;
			*bufpp = col->data;
			*alenpp = &col->datalen;
			*indpp = (dvoid *)&col->indicator;
			break;

		default:
			php_error_docref(NULL, E_WARNING,
				"unhandled datatype in oci_define_callback; this should not happen");
			return OCI_ERROR;
	}

	return OCI_CONTINUE;
}

static int oci_stmt_describe(pdo_stmt_t *stmt, int colno) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
	OCIParam *param = NULL;
	text *colname;
	ub2 dtype, data_size, precis;
	ub4 namelen;
	struct pdo_column_data *col = &stmt->columns[colno];
	zend_bool dyn = FALSE;

	/* describe the column */
	STMT_CALL(OCIParamGet, (S->stmt, OCI_HTYPE_STMT, S->err, (dvoid*)&param, colno+1));

	/* what type ? */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_DATA_TYPE",
			(param, OCI_DTYPE_PARAM, &dtype, 0, OCI_ATTR_DATA_TYPE, S->err));

	/* how big ? */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_DATA_SIZE",
			(param, OCI_DTYPE_PARAM, &data_size, 0, OCI_ATTR_DATA_SIZE, S->err));

	/* precision ? */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_PRECISION",
			(param, OCI_DTYPE_PARAM, &precis, 0, OCI_ATTR_PRECISION, S->err));

	/* name ? */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_NAME",
			(param, OCI_DTYPE_PARAM, &colname, &namelen, OCI_ATTR_NAME, S->err));

	col->precision = precis;
	col->maxlen = data_size;
	col->name = zend_string_init((char *)colname, namelen, 0);

	S->cols[colno].dtype = dtype;

	/* how much room do we need to store the field */
	switch (dtype) {
		case SQLT_LBI:
		case SQLT_LNG:
			if (dtype == SQLT_LBI) {
				dtype = SQLT_BIN;
			} else {
				dtype = SQLT_CHR;
			}
			S->cols[colno].datalen = 512; /* XXX should be INT_MAX and fetched by pieces */
			S->cols[colno].data = emalloc(S->cols[colno].datalen + 1);
			col->param_type = PDO_PARAM_STR;
			break;

		case SQLT_BLOB:
		case SQLT_CLOB:
			col->param_type = PDO_PARAM_LOB;
			STMT_CALL(OCIDescriptorAlloc, (S->H->env, (dvoid**)&S->cols[colno].data, OCI_DTYPE_LOB, 0, NULL));
			S->cols[colno].datalen = sizeof(OCILobLocator*);
			dyn = TRUE;
			break;

		case SQLT_BIN:
		default:
			if (dtype == SQLT_DAT || dtype == SQLT_NUM || dtype == SQLT_RDD
#ifdef SQLT_TIMESTAMP
					|| dtype == SQLT_TIMESTAMP
#endif
#ifdef SQLT_TIMESTAMP_TZ
					|| dtype == SQLT_TIMESTAMP_TZ
#endif
					) {
				/* should be big enough for most date formats and numbers */
				S->cols[colno].datalen = 512;
#if defined(SQLT_IBFLOAT) && defined(SQLT_IBDOUBLE)
			} else if (dtype == SQLT_IBFLOAT || dtype == SQLT_IBDOUBLE) {
				S->cols[colno].datalen = 1024;
#endif
			} else if (dtype == SQLT_BIN) {
				S->cols[colno].datalen = (ub4) col->maxlen * 2; /* raw characters to hex digits */
			} else {
				S->cols[colno].datalen = (ub4) (col->maxlen * S->H->max_char_width);
			}

			S->cols[colno].data = emalloc(S->cols[colno].datalen + 1);
			dtype = SQLT_CHR;

			/* returning data as a string */
			col->param_type = PDO_PARAM_STR;
	}

	STMT_CALL(OCIDefineByPos, (S->stmt, &S->cols[colno].def, S->err, colno+1,
				S->cols[colno].data, S->cols[colno].datalen, dtype, &S->cols[colno].indicator,
				&S->cols[colno].fetched_len, &S->cols[colno].retcode, dyn ? OCI_DYNAMIC_FETCH : OCI_DEFAULT));

	if (dyn) {
		STMT_CALL(OCIDefineDynamic, (S->cols[colno].def, S->err, &S->cols[colno],
				oci_define_callback));
	}

	return 1;
} /* }}} */

struct _oci_lob_env {
	OCISvcCtx *svc;
	OCIError  *err;
};
typedef struct _oci_lob_env oci_lob_env;

struct oci_lob_self {
	zval dbh;
	pdo_stmt_t *stmt;
	pdo_oci_stmt *S;
	OCILobLocator *lob;
	oci_lob_env   *E;
	ub4 offset;
};

static ssize_t oci_blob_write(php_stream *stream, const char *buf, size_t count)
{
	struct oci_lob_self *self = (struct oci_lob_self*)stream->abstract;
	ub4 amt;
	sword r;

	amt = (ub4) count;
	r = OCILobWrite(self->E->svc, self->E->err, self->lob,
		&amt, self->offset, (char*)buf, (ub4) count,
		OCI_ONE_PIECE,
		NULL, NULL, 0, SQLCS_IMPLICIT);

	if (r != OCI_SUCCESS) {
		return (ssize_t)-1;
	}

	self->offset += amt;
	return amt;
}

static size_t oci_blob_read(php_stream *stream, char *buf, size_t count)
{
	struct oci_lob_self *self = (struct oci_lob_self*)stream->abstract;
	ub4 amt;
	sword r;

	amt = (ub4) count;
	r = OCILobRead(self->E->svc, self->E->err, self->lob,
		&amt, self->offset, buf, (ub4) count,
		NULL, NULL, 0, SQLCS_IMPLICIT);

	if (r != OCI_SUCCESS && r != OCI_NEED_DATA) {
		return (size_t)-1;
	}

	self->offset += amt;
	if (amt < count) {
		stream->eof = 1;
	}
	return amt;
}

static int oci_blob_close(php_stream *stream, int close_handle)
{
	struct oci_lob_self *self = (struct oci_lob_self *)stream->abstract;
	pdo_stmt_t *stmt = self->stmt;

	if (close_handle) {
		zend_object *obj = &stmt->std;

		OCILobClose(self->E->svc, self->E->err, self->lob);
		zval_ptr_dtor(&self->dbh);
		GC_DELREF(obj);
		efree(self->E);
		efree(self);
	}

	/* php_pdo_free_statement(stmt); */
	return 0;
}

static int oci_blob_flush(php_stream *stream)
{
	struct oci_lob_self *self = (struct oci_lob_self*)stream->abstract;
	OCILobFlushBuffer(self->E->svc, self->E->err, self->lob, 0);
	return 0;
}

static int oci_blob_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset)
{
	struct oci_lob_self *self = (struct oci_lob_self*)stream->abstract;

	if (offset >= PDO_OCI_LOBMAXSIZE) {
		return -1;
	} else {
		self->offset = (ub4) offset + 1;  /* Oracle LOBS are 1-based, but PHP is 0-based */
		return 0;
	}
}

static const php_stream_ops oci_blob_stream_ops = {
	oci_blob_write,
	oci_blob_read,
	oci_blob_close,
	oci_blob_flush,
	"pdo_oci blob stream",
	oci_blob_seek,
	NULL,
	NULL,
	NULL
};

static php_stream *oci_create_lob_stream(zval *dbh, pdo_stmt_t *stmt, OCILobLocator *lob)
{
	php_stream *stm;
	struct oci_lob_self *self = ecalloc(1, sizeof(*self));

	ZVAL_COPY_VALUE(&self->dbh, dbh);
	self->lob = lob;
	self->offset = 1; /* 1-based */
	self->stmt = stmt;
	self->S = (pdo_oci_stmt*)stmt->driver_data;
	self->E = ecalloc(1, sizeof(oci_lob_env));
	self->E->svc = self->S->H->svc;
	self->E->err = self->S->err;

	stm = php_stream_alloc(&oci_blob_stream_ops, self, 0, "r+b");

	if (stm) {
		zend_object *obj;
		obj = &stmt->std;
		Z_ADDREF(self->dbh);
		GC_ADDREF(obj);
		return stm;
	}

	efree(self);
	return NULL;
}

static int oci_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_t *len, int *caller_frees) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
	pdo_oci_column *C = &S->cols[colno];

	/* check the indicator to ensure that the data is intact */
	if (C->indicator == -1) {
		/* A NULL value */
		*ptr = NULL;
		*len = 0;
		return 1;
	} else if (C->indicator == 0) {
		/* it was stored perfectly */

		if (C->dtype == SQLT_BLOB || C->dtype == SQLT_CLOB) {
			if (C->data) {
				*ptr = (char*)oci_create_lob_stream(&stmt->database_object_handle, stmt, (OCILobLocator*)C->data);
				OCILobOpen(S->H->svc, S->err, (OCILobLocator*)C->data, OCI_LOB_READONLY);
			}
			*len = (size_t) 0;
			return *ptr ? 1 : 0;
		}

		*ptr = C->data;
		*len = (size_t) C->fetched_len;
		return 1;
	} else {
		/* it was truncated */
		php_error_docref(NULL, E_WARNING, "column %d data was too large for buffer and was truncated to fit it", colno);

		*ptr = C->data;
		*len = (size_t) C->fetched_len;
		return 1;
	}
} /* }}} */


static int oci_stmt_col_meta(pdo_stmt_t *stmt, zend_long colno, zval *return_value) /* {{{ */
{
	pdo_oci_stmt *S = (pdo_oci_stmt*)stmt->driver_data;
	OCIParam *param = NULL;
	ub2 dtype, precis;
	sb1 scale;
	zval flags;
	ub1 isnull, charset_form;
	if (!S->stmt) {
		return FAILURE;
	}
	if (colno >= stmt->column_count) {
		/* error invalid column */
		return FAILURE;
	}

	array_init(return_value);
	array_init(&flags);

	/* describe the column */
	STMT_CALL(OCIParamGet, (S->stmt, OCI_HTYPE_STMT, S->err, (dvoid*)&param, colno+1));

	/* column data type */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_DATA_TYPE",
			(param, OCI_DTYPE_PARAM, &dtype, 0, OCI_ATTR_DATA_TYPE, S->err));

	/* column precision */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_PRECISION",
			(param, OCI_DTYPE_PARAM, &precis, 0, OCI_ATTR_PRECISION, S->err));

	/* column scale */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_SCALE",
			(param, OCI_DTYPE_PARAM, &scale, 0, OCI_ATTR_SCALE, S->err));

	/* string column charset form */
	if (dtype == SQLT_CHR || dtype == SQLT_VCS || dtype == SQLT_AFC || dtype == SQLT_CLOB) {
		STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_CHARSET_FORM",
			(param, OCI_DTYPE_PARAM, &charset_form, 0, OCI_ATTR_CHARSET_FORM, S->err));
	}


	if (dtype) {
	/* if there is a declared type */
		switch (dtype) {
#ifdef SQLT_TIMESTAMP
		case SQLT_TIMESTAMP:
			add_assoc_string(return_value, "oci:decl_type", "TIMESTAMP");
			add_assoc_string(return_value, "native_type", "TIMESTAMP");
			break;
#endif
#ifdef SQLT_TIMESTAMP_TZ
		case SQLT_TIMESTAMP_TZ:
			add_assoc_string(return_value, "oci:decl_type", "TIMESTAMP WITH TIMEZONE");
			add_assoc_string(return_value, "native_type", "TIMESTAMP WITH TIMEZONE");
			break;
#endif
#ifdef SQLT_TIMESTAMP_LTZ
		case SQLT_TIMESTAMP_LTZ:
			add_assoc_string(return_value, "oci:decl_type", "TIMESTAMP WITH LOCAL TIMEZONE");
			add_assoc_string(return_value, "native_type", "TIMESTAMP WITH LOCAL TIMEZONE");
			break;
#endif
#ifdef SQLT_INTERVAL_YM
		case SQLT_INTERVAL_YM:
			add_assoc_string(return_value, "oci:decl_type", "INTERVAL YEAR TO MONTH");
			add_assoc_string(return_value, "native_type", "INTERVAL YEAR TO MONTH");
			break;
#endif
#ifdef SQLT_INTERVAL_DS
		case SQLT_INTERVAL_DS:
			add_assoc_string(return_value, "oci:decl_type", "INTERVAL DAY TO SECOND");
			add_assoc_string(return_value, "native_type", "INTERVAL DAY TO SECOND");
			break;
#endif
		case SQLT_DAT:
			add_assoc_string(return_value, "oci:decl_type", "DATE");
			add_assoc_string(return_value, "native_type", "DATE");
			break;
		case SQLT_FLT :
		case SQLT_NUM:
			/* if the precision is nonzero and scale is -127 then it is a FLOAT */
			if (scale == -127 && precis != 0) {
				add_assoc_string(return_value, "oci:decl_type", "FLOAT");
				add_assoc_string(return_value, "native_type", "FLOAT");
			} else {
				add_assoc_string(return_value, "oci:decl_type", "NUMBER");
				add_assoc_string(return_value, "native_type", "NUMBER");
			}
			break;
		case SQLT_LNG:
			add_assoc_string(return_value, "oci:decl_type", "LONG");
			add_assoc_string(return_value, "native_type", "LONG");
			break;
		case SQLT_BIN:
			add_assoc_string(return_value, "oci:decl_type", "RAW");
			add_assoc_string(return_value, "native_type", "RAW");
			break;
		case SQLT_LBI:
			add_assoc_string(return_value, "oci:decl_type", "LONG RAW");
			add_assoc_string(return_value, "native_type", "LONG RAW");
			break;
		case SQLT_CHR:
		case SQLT_VCS:
			if (charset_form == SQLCS_NCHAR) {
				add_assoc_string(return_value, "oci:decl_type", "NVARCHAR2");
				add_assoc_string(return_value, "native_type", "NVARCHAR2");
			} else {
				add_assoc_string(return_value, "oci:decl_type", "VARCHAR2");
				add_assoc_string(return_value, "native_type", "VARCHAR2");
			}
			break;
		case SQLT_AFC:
			if (charset_form == SQLCS_NCHAR) {
				add_assoc_string(return_value, "oci:decl_type", "NCHAR");
				add_assoc_string(return_value, "native_type", "NCHAR");
			} else {
				add_assoc_string(return_value, "oci:decl_type", "CHAR");
				add_assoc_string(return_value, "native_type", "CHAR");
			}
			break;
		case SQLT_BLOB:
			add_assoc_string(return_value, "oci:decl_type", "BLOB");
			add_next_index_string(&flags, "blob");
			add_assoc_string(return_value, "native_type", "BLOB");
			break;
		case SQLT_CLOB:
			if (charset_form == SQLCS_NCHAR) {
				add_assoc_string(return_value, "oci:decl_type", "NCLOB");
				add_assoc_string(return_value, "native_type", "NCLOB");
			} else {
				add_assoc_string(return_value, "oci:decl_type", "CLOB");
				add_assoc_string(return_value, "native_type", "CLOB");
			}
			add_next_index_string(&flags, "blob");
			break;
		case SQLT_BFILE:
			add_assoc_string(return_value, "oci:decl_type", "BFILE");
			add_next_index_string(&flags, "blob");
			add_assoc_string(return_value, "native_type", "BFILE");
			break;
		case SQLT_RDD:
			add_assoc_string(return_value, "oci:decl_type", "ROWID");
			add_assoc_string(return_value, "native_type", "ROWID");
			break;
		case SQLT_BFLOAT:
		case SQLT_IBFLOAT:
			add_assoc_string(return_value, "oci:decl_type", "BINARY_FLOAT");
			add_assoc_string(return_value, "native_type", "BINARY_FLOAT");
			break;
		case SQLT_BDOUBLE:
		case SQLT_IBDOUBLE:
			add_assoc_string(return_value, "oci:decl_type", "BINARY_DOUBLE");
			add_assoc_string(return_value, "native_type", "BINARY_DOUBLE");
			break;
		default:
			add_assoc_long(return_value, "oci:decl_type", dtype);
			add_assoc_string(return_value, "native_type", "UNKNOWN");
		}
	} else {
		/* if the column is NULL */
		add_assoc_long(return_value, "oci:decl_type", 0);
		add_assoc_string(return_value, "native_type", "NULL");
	}

	/* column can be null */
	STMT_CALL_MSG(OCIAttrGet, "OCI_ATTR_IS_NULL",
			(param, OCI_DTYPE_PARAM, &isnull, 0, OCI_ATTR_IS_NULL, S->err));

	if (isnull) {
		add_next_index_string(&flags, "nullable");
	} else {
		add_next_index_string(&flags, "not_null");
	}

	/* PDO type */
	switch (dtype) {
		case SQLT_BFILE:
		case SQLT_BLOB:
		case SQLT_CLOB:
			add_assoc_long(return_value, "pdo_type", PDO_PARAM_LOB);
			break;
		default:
			add_assoc_long(return_value, "pdo_type", PDO_PARAM_STR);
	}

	add_assoc_long(return_value, "scale", scale);
	add_assoc_zval(return_value, "flags", &flags);

	OCIDescriptorFree(param, OCI_DTYPE_PARAM);
	return SUCCESS;
} /* }}} */

const struct pdo_stmt_methods oci_stmt_methods = {
	oci_stmt_dtor,
	oci_stmt_execute,
	oci_stmt_fetch,
	oci_stmt_describe,
	oci_stmt_get_col,
	oci_stmt_param_hook,
	NULL, /* set_attr */
	NULL, /* get_attr */
	oci_stmt_col_meta
};
