<?php
/**
 * IBM DB2 Native Client driver.
 *
 * Originally DB2 drivers were dependent on an ODBC driver, and some installations
 * may still use that. To use an ODBC driver connection, use the odbc_db2
 * ADOdb driver. For Linux, you need the 'ibm_db2' PECL extension for PHP,
 * For Windows, you need to locate an appropriate version of the php_ibm_db2.dll,
 * as well as the IBM data server client software.
 * This is basically a full rewrite of the original driver, for information
 * about all the changes, see the update information on the ADOdb website
 * for version 5.21.0.
 *
 * @link http://pecl.php.net/package/ibm_db2 PECL Extension For DB2
 *
 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
 *
 * @package ADOdb
 * @link https://adodb.org Project's web site and documentation
 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker
 *
 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause
 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option,
 * any later version. This means you can use it in proprietary products.
 * See the LICENSE.md file distributed with this source code for details.
 * @license BSD-3-Clause
 * @license LGPL-2.1-or-later
 *
 * @copyright 2000-2013 John Lim
 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
 * @author Mark Newnham
 */

// security - hide paths
if (!defined('ADODB_DIR')) die();

define("_ADODB_DB2_LAYER", 2 );


class ADODB_db2 extends ADOConnection {
	var $databaseType = "db2";
	var $fmtDate = "'Y-m-d'";
	var $concat_operator = '||';

	var $sysTime = 'CURRENT TIME';
	var $sysDate = 'CURRENT DATE';
	var $sysTimeStamp = 'CURRENT TIMESTAMP';

	var $fmtTimeStamp = "'Y-m-d H:i:s'";
	var $replaceQuote = "''"; // string to use to replace quotes
	var $dataProvider = "db2";
	var $hasAffectedRows = true;

	var $binmode = DB2_BINARY;

	/*
	* setting this to true will make array elements in FETCH_ASSOC
	* mode case-sensitive breaking backward-compat
	*/
	var $useFetchArray = false;
	var $_bindInputArray = true;
	var $_genIDSQL = "VALUES NEXTVAL FOR %s";
	var $_genSeqSQL = "
	CREATE SEQUENCE %s START WITH %s
	NO MAXVALUE NO CYCLE INCREMENT BY 1 NO CACHE
	";
	var $_dropSeqSQL = "DROP SEQUENCE %s";
	var $_autocommit = true;
	var $_lastAffectedRows = 0;
	var $hasInsertID = true;
	var $hasGenID    = true;

	/*
	 * Character used to wrap column and table names for escaping special
	 * characters in column and table names as well as forcing upper and
	 * lower case
	 */
	public $nameQuote = '"';

	/*
	 * Holds information about the stored procedure request
	 * currently being built
	 */
	private $storedProcedureParameters = false;


	function __construct() {}

	protected function _insertID($table = '', $column = '')
	{
		return ADOConnection::GetOne('VALUES IDENTITY_VAL_LOCAL()');
	}

	public function _connect($argDSN, $argUsername, $argPassword, $argDatabasename)
	{
		return $this->doDB2Connect($argDSN, $argUsername, $argPassword, $argDatabasename);
	}

	public function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename)
	{
		return $this->doDB2Connect($argDSN, $argUsername, $argPassword, $argDatabasename,true);
	}

	private function doDB2Connect($argDSN, $argUsername, $argPassword, $argDatabasename, $persistent=false)
	{

		if (!function_exists('db2_connect')) {
			ADOConnection::outp("DB2 extension not installed.");
			return null;
		}

		$connectionParameters = $this->unpackParameters($argDSN,
														$argUsername,
														$argPassword,
														$argDatabasename);

		if ($connectionParameters == null)
		{
			/*
			 * Error thrown
			 */
			return null;
		}

		$argDSN 		         = $connectionParameters['dsn'];
		$argUsername 	         = $connectionParameters['uid'];
		$argPassword 	         = $connectionParameters['pwd'];
		$argDatabasename         = $connectionParameters['database'];
		$useCataloguedConnection = $connectionParameters['catalogue'];

		if ($this->debug){
			if ($useCataloguedConnection){
				$connectMessage = "Catalogued connection using parameters: ";
				$connectMessage .= "DB=$argDatabasename / ";
				$connectMessage .= "UID=$argUsername / ";
				$connectMessage .= "PWD=$argPassword";
			}
			else
			{
				$connectMessage = "Uncatalogued connection using DSN: $argDSN";
			}
			ADOConnection::outp($connectMessage);
		}
		/*
		 * This needs to be set before the connect().
		 */
		ini_set('ibm_db2.binmode', $this->binmode);

		if ($persistent)
			$db2Function = 'db2_pconnect';
		else
			$db2Function = 'db2_connect';

		/*
		* We need to flatten out the connectionParameters
		*/

		$db2Options = array();
		if ($this->connectionParameters)
		{
			foreach($this->connectionParameters as $p)
				foreach($p as $k=>$v)
					$db2Options[$k] = $v;
		}

		if ($useCataloguedConnection)
			$this->_connectionID = $db2Function($argDatabasename,
												$argUsername,
												$argPassword,
												$db2Options);
		else
			$this->_connectionID = $db2Function($argDSN,
												null,
												null,
												$db2Options);


		$this->_errorMsg = @db2_conn_errormsg();

		if ($this->_connectionID && $this->connectStmt)
			$this->execute($this->connectStmt);

		return $this->_connectionID != false;

	}

	/**
	 * Validates and preprocesses the passed parameters for consistency
	 *
	 * @param	string	$argDSN				Either DSN or database
	 * @param	string	$argUsername		User name or null
	 * @param	string	$argPassword		Password or null
	 * @param	string	$argDatabasename	Either DSN or database
	 *
	 * @return mixed  array if correct, null if not
	 */
	private function unpackParameters($argDSN, $argUsername, $argPassword, $argDatabasename)
	{


		$connectionParameters = array('dsn'=>'',
									  'uid'=>'',
									  'pwd'=>'',
									  'database'=>'',
									  'catalogue'=>true
									  );

		/*
		 * Uou can either connect to a catalogued connection
		 * with a database name e.g. 'SAMPLE'
		 * or an uncatalogued connection with a DSN like connection
		 * DATABASE=database;HOSTNAME=hostname;PORT=port;PROTOCOL=TCPIP;UID=username;PWD=password;
		 */

		if (!$argDSN && !$argDatabasename)
		{
			$errorMessage = 'Supply either catalogued or uncatalogued connection parameters';
			$this->_errorMsg = $errorMessage;
			if ($this->debug)
				ADOConnection::outp($errorMessage);
			return null;
		}

		$useCataloguedConnection = true;
		$schemaName 			 = '';

		if ($argDSN && $argDatabasename)
		{
			/*
			 * If a catalogued connection if provided,
			 * as well as user and password
			 * that will take priority
			 */
			if ($argUsername && $argPassword && !$this->isDsn($argDatabasename))
			{
				if ($this->debug){
					$errorMessage = 'Warning: Because you provided user,';
					$errorMessage.= 'password and database, DSN connection ';
					$errorMessage.= 'parameters were discarded';
					ADOConnection::outp($errorMessage);

				}
				$argDSN = '';
			}
			else if ($this->isDsn($argDSN) && $this->isDsn($argDatabasename))
			{
				$errorMessage = 'Supply uncatalogued connection parameters ';
				$errorMessage.= 'in either the database or DSN arguments, ';
				$errorMessage.= 'but not both';

				if ($this->debug)
					ADOConnection::outp($errorMessage);
				return null;
			}
		}

		if (!$this->isDsn($argDSN) && $this->isDsn($argDatabasename))
		{
			/*
			 * Switch them around for next test
			 */
			$temp           = $argDSN;
			$argDsn         = $argDatabasename;
			$argDatabasenME = $temp;
		}

		if ($this->isDsn($argDSN))
		{

			if (!preg_match('/uid=/i',$argDSN)
			||  !preg_match('/pwd=/i',$argDSN))
			{
				$errorMessage = 'For uncatalogued connections, provide ';
				$errorMessage.= 'both UID and PWD in the connection string';

				if ($this->debug)
					ADOConnection::outp($errorMessage);
				return null;
			}

			if (preg_match('/database=/i',$argDSN))
			{
				if ($argDatabasename)
				{
					$argDatabasename = '';
					if ($this->debug)
					{
						$errorMessage = 'Warning: Because you provided ';
						$errorMessage.= 'database information in the DSN ';
						$errorMessage.= 'parameters, the supplied database ';
						$errorMessage.= 'name was discarded';
						ADOConnection::outp($errorMessage);
					}
				}
				$useCataloguedConnection = false;

			}
			elseif ($argDatabasename)
			{
				$this->database = $argDatabasename;
				$argDSN .= ';database=' . $argDatabasename;
				$argDatabasename = '';
				$useCataloguedConnection = false;

			}
			else
			{
				$errorMessage = 'Uncatalogued connection parameters ';
				$errorMessage.= 'must contain a database= argument';

				if ($this->debug)
					ADOConnection::outp($errorMessage);
				return null;
			}
		}

		if ($argDSN && !$argDatabasename && $useCataloguedConnection)
		{
			$argDatabasename = $argDSN;
			$argDSN          = '';
		}


		if ($useCataloguedConnection
		&& (!$argDatabasename
		|| !$argUsername
		|| !$argPassword))
		{

			$errorMessage = 'For catalogued connections, provide ';
			$errorMessage.= 'database, username and password';
			$this->_errorMsg = $errorMessage;
			if ($this->debug)
				ADOConnection::outp($errorMessage);
			return null;

		}

		if ($argDatabasename)
			$this->database = $argDatabasename;
		elseif (!$this->database)
			$this->database = $this->getDatabasenameFromDsn($argDSN);


		$connectionParameters = array('dsn'=>$argDSN,
									  'uid'=>$argUsername,
									  'pwd'=>$argPassword,
									  'database'=>$argDatabasename,
									  'catalogue'=>$useCataloguedConnection
									  );

		return $connectionParameters;

	}

	/**
	  * Does the provided string look like a DSN
	  *
	  * @param	string	$dsnString
	  *
	  * @return bool
	  */
	private function isDsn($dsnString){
		$dsnArray = preg_split('/[;=]+/',$dsnString);
		if (count($dsnArray) > 2)
			return true;
		return false;
	}


	/**
	  * Gets the database name from the DSN
	  *
	  * @param	string	$dsnString
	  *
	  * @return string
	  */
	private function getDatabasenameFromDsn($dsnString){

		$dsnArray = preg_split('/[;=]+/',$dsnString);
		$dbIndex  = array_search('database',$dsnArray);

		return $dsnArray[$dbIndex + 1];
	}


	/**
	* format and return date string in database timestamp format
	*
	* @param	mixed	$ts		either a string or a unixtime
	* @param	bool	$isField	discarded
	*
	* @return string
	*/
	function dbTimeStamp($ts,$isField=false)
	{
		if (empty($ts) && $ts !== 0) return 'null';
		if (is_string($ts)) $ts = ADORecordSet::unixTimeStamp($ts);
		return 'TO_DATE('.adodb_date($this->fmtTimeStamp,$ts).",'YYYY-MM-DD HH24:MI:SS')";
	}

	/**
	* Format date column in sql string given an input format that understands Y M D
	*
	* @param	string	$fmt
	* @param	bool	$col
	*
	* @return string
	*/
	function sqlDate($fmt, $col=false)
	{
		if (!$col) $col = $this->sysDate;

		/* use TO_CHAR() if $fmt is TO_CHAR() allowed fmt */
		if ($fmt== 'Y-m-d H:i:s')
			return 'TO_CHAR('.$col.", 'YYYY-MM-DD HH24:MI:SS')";

		$s = '';

		$len = strlen($fmt);
		for ($i=0; $i < $len; $i++) {
			if ($s) $s .= $this->concat_operator;
			$ch = $fmt[$i];
			switch($ch) {
			case 'Y':
			case 'y':
				if ($len==1) return "year($col)";
				$s .= "char(year($col))";
				break;
			case 'M':
				if ($len==1) return "monthname($col)";
				$s .= "substr(monthname($col),1,3)";
				break;
			case 'm':
				if ($len==1) return "month($col)";
				$s .= "right(digits(month($col)),2)";
				break;
			case 'D':
			case 'd':
				if ($len==1) return "day($col)";
				$s .= "right(digits(day($col)),2)";
				break;
			case 'H':
			case 'h':
				if ($len==1) return "hour($col)";
				if ($col != $this->sysDate) $s .= "right(digits(hour($col)),2)";
				else $s .= "''";
				break;
			case 'i':
			case 'I':
				if ($len==1) return "minute($col)";
				if ($col != $this->sysDate)
					$s .= "right(digits(minute($col)),2)";
					else $s .= "''";
				break;
			case 'S':
			case 's':
				if ($len==1) return "second($col)";
				if ($col != $this->sysDate)
					$s .= "right(digits(second($col)),2)";
				else $s .= "''";
				break;
			default:
				if ($ch == '\\') {
					$i++;
					$ch = substr($fmt,$i,1);
				}
				$s .= $this->qstr($ch);
			}
		}
		return $s;
	}


	function serverInfo()
	{
		$sql = "SELECT service_level, fixpack_num
				  FROM TABLE(sysproc.env_get_inst_info())
					AS INSTANCEINFO";
		$row = $this->GetRow($sql);


		if ($row) {
			$info['version'] = $row[0].':'.$row[1];
			$info['fixpack'] = $row[1];
			$info['description'] = '';
		} else {
			return ADOConnection::serverInfo();
		}

		return $info;
	}

	function createSequence($seqname='adodbseq',$start=1)
	{
		if (empty($this->_genSeqSQL))
			return false;

		$ok = $this->execute(sprintf($this->_genSeqSQL,$seqname,$start));
		if (!$ok)
			return false;
		return true;
	}

	function dropSequence($seqname='adodbseq')
	{
		if (empty($this->_dropSeqSQL)) return false;
		return $this->execute(sprintf($this->_dropSeqSQL,$seqname));
	}

	function selectLimit($sql,$nrows=-1,$offset=-1,$inputArr=false,$secs2cache=0)
	{
		$nrows = (integer) $nrows;

		if ($offset <= 0)
		{
			if ($nrows >= 0)
				$sql .=  " FETCH FIRST $nrows ROWS ONLY ";

			$rs = $this->execute($sql,$inputArr);

		}
		else
		{
			if ($offset > 0 && $nrows < 0);

			else
			{
				$nrows += $offset;
				$sql .=  " FETCH FIRST $nrows ROWS ONLY ";
			}

			/*
			 * DB2 has no native support for mid table offset
			 */
			$rs = ADOConnection::selectLimit($sql,$nrows,$offset,$inputArr);

		}

		return $rs;
	}


	function errorMsg()
	{
		if ($this->_errorMsg !== false)
			return $this->_errorMsg;

		if (empty($this->_connectionID))
			return @db2_conn_errormsg();

		return @db2_conn_errormsg($this->_connectionID);
	}

	function errorNo()
	{

		if ($this->_errorCode !== false)
			return $this->_errorCode;


		if (empty($this->_connectionID))
			$e = @db2_conn_error();

		else
			$e = @db2_conn_error($this->_connectionID);

		return $e;
	}



	function beginTrans()
	{
		if (!$this->hasTransactions)
			return false;
		if ($this->transOff)
			return true;

		$this->transCnt += 1;

		$this->_autocommit = false;

		return db2_autocommit($this->_connectionID,false);
	}

	function CommitTrans($ok=true)
	{
		if ($this->transOff)
			return true;

		if (!$ok)
			return $this->RollbackTrans();

		if ($this->transCnt)
			$this->transCnt -= 1;

		$this->_autocommit = true;
		$ret = @db2_commit($this->_connectionID);
		@db2_autocommit($this->_connectionID,true);
		return $ret;
	}

	function RollbackTrans()
	{
		if ($this->transOff) return true;
		if ($this->transCnt) $this->transCnt -= 1;
		$this->_autocommit = true;
		$ret = @db2_rollback($this->_connectionID);
		@db2_autocommit($this->_connectionID,true);
		return $ret;
	}

	/**
	 * Return a list of Primary Keys for a specified table
	 *
	 * We don't use db2_statistics as the function does not seem to play
	 * well with mixed case table names
	 *
	 * @param string   $table
	 * @param bool     $primary    (optional) only return primary keys
	 * @param bool     $owner      (optional) not used in this driver
	 *
	 * @return string[]    Array of indexes
	 */
	public function metaPrimaryKeys($table,$owner=false)
	{

		$primaryKeys = array();

		global $ADODB_FETCH_MODE;

		$schema = '';
		$this->_findschema($table,$schema);

		$table = $this->getTableCasedValue($table);

		$savem 			  = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
		$this->setFetchMode(ADODB_FETCH_NUM);


		$sql = "SELECT *
				  FROM syscat.indexes
				 WHERE tabname='$table'";

		$rows = $this->getAll($sql);

		$this->setFetchMode($savem);
		$ADODB_FETCH_MODE = $savem;

		if (empty($rows))
			return false;

		foreach ($rows as $r)
		{
			if ($r[7] != 'P')
				continue;

			$cols = explode('+',$r[6]);
			foreach ($cols as $colIndex=>$col)
			{
				if ($colIndex == 0)
					continue;
				$columnName = $this->getMetaCasedValue($col);
				$primaryKeys[] = $columnName;
			}
			break;
		}
		return $primaryKeys;
	}

	/**
	 * Returns a list of Foreign Keys associated with a specific table.
	 *
	 * @param string $table
	 * @param string $owner       discarded
	 * @param bool   $upper       discarded
	 * @param bool   $associative discarded
	 *
	 * @return string[]|false An array where keys are tables, and values are foreign keys;
	 *                        false if no foreign keys could be found.
	 */
	public function metaForeignKeys($table, $owner = '', $upper = false, $associative = false)
	{

		global $ADODB_FETCH_MODE;

		$schema = '';
		$this->_findschema($table,$schema);

		$savem = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;

		$this->setFetchMode(ADODB_FETCH_NUM);

		$sql = "SELECT SUBSTR(tabname,1,20) table_name,
					   SUBSTR(constname,1,20) fk_name,
					   SUBSTR(REFTABNAME,1,12) parent_table,
					   SUBSTR(refkeyname,1,20) pk_orig_table,
					   fk_colnames
				 FROM syscat.references
				WHERE tabname = '$table'";

		$results = $this->getAll($sql);

		$ADODB_FETCH_MODE = $savem;
		$this->setFetchMode($savem);

		if (empty($results))
			return false;

		$foreignKeys = array();

		foreach ($results as $r)
		{
			$parentTable = trim($this->getMetaCasedValue($r[2]));
			$keyName     = trim($this->getMetaCasedValue($r[1]));
			$foreignKeys[$parentTable] = $keyName;
		}

		return $foreignKeys;
	}

	/**
	 * Returns a list of tables
	 *
	 * @param string	$ttype (optional)
	 * @param	string	$schema	(optional)
	 * @param	string	$mask	(optional)
	 *
	 * @return array
	 */
	public function metaTables($ttype=false,$schema=false,$mask=false)
	{

		global $ADODB_FETCH_MODE;

		$savem 			  = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;

		/*
		* Values for TABLE_TYPE
		* ---------------------------
		* ALIAS, HIERARCHY TABLE, INOPERATIVE VIEW, NICKNAME,
		* MATERIALIZED QUERY TABLE, SYSTEM TABLE, TABLE,
		* TYPED TABLE, TYPED VIEW, and VIEW
		*
		* If $ttype passed as '', match 'TABLE' and 'VIEW'
		* If $ttype passed as 'T' it is assumed to be 'TABLE'
		* if $ttype passed as 'V' it is assumed to be 'VIEW'
		*/
		$ttype = strtoupper($ttype);
		if ($ttype) {
			/*
			 * @todo We could do valid type checking or array type
			 */
			 if ($ttype == 'V')
				$ttype = 'VIEW';
			if ($ttype == 'T')
				$ttype = 'TABLE';
		}

		if (!$schema)
			$schema = '%';

		if (!$mask)
			$mask = '%';

		$qid = @db2_tables($this->_connectionID,NULL,$schema,$mask,$ttype);

		$rs = new ADORecordSet_db2($qid);

		$ADODB_FETCH_MODE = $savem;

		if (!$rs)
			return false;

		$arr = $rs->getArray();

		$rs->Close();

		$tableList = array();

		/*
		* Array items
		* ---------------------------------
		* 0 TABLE_CAT	The catalog that contains the table.
		*				The value is NULL if this table does not have catalogs.
		* 1 TABLE_SCHEM	Name of the schema that contains the table.
		* 2 TABLE_NAME	Name of the table.
		* 3 TABLE_TYPE	Table type identifier for the table.
		* 4 REMARKS		Description of the table.
		*/

		for ($i=0; $i < sizeof($arr); $i++)
		{

			$tableRow = $arr[$i];
			$tableName = $tableRow[2];
			$tableType = $tableRow[3];

			if (!$tableName)
				continue;

			if ($ttype == '' && (strcmp($tableType,'TABLE') <> 0 && strcmp($tableType,'VIEW') <> 0))
				continue;

			/*
			 * Set metacasing if required
			 */
			$tableName = $this->getMetaCasedValue($tableName);

			/*
			 * If we requested a schema, we prepend the schema
			   name to the table name
			 */
			if (strcmp($schema,'%') <> 0)
				$tableName = $schema . '.' . $tableName;

			$tableList[] = $tableName;

		}
		return $tableList;
	}

	/**
	  * Return a list of indexes for a specified table
	  *
	  * We don't use db2_statistics as the function does not seem to play
	  * well with mixed case table names
	  *
	  * @param string   $table
	  * @param bool     $primary    (optional) only return primary keys
	  * @param bool     $owner      (optional) not used in this driver
	  *
	  * @return string[]    Array of indexes
	  */
	public function metaIndexes($table, $primary = false, $owner = false) {

		global $ADODB_FETCH_MODE;

		 /* Array(
		 *   [name_of_index] => Array(
		 *     [unique] => true or false
		 *     [columns] => Array(
		 *       [0] => firstcol
		 *       [1] => nextcol
		 *       [2] => etc........
		 *     )
		 *   )
		 * )
		 */
		$indices 		= array();
		$primaryKeyName = '';

		$table = $this->getTableCasedValue($table);


		$savem 			  = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
		$this->setFetchMode(ADODB_FETCH_NUM);

		$sql = "SELECT *
				  FROM syscat.indexes
				 WHERE tabname='$table'";

		$rows = $this->getAll($sql);

		$this->setFetchMode($savem);
		$ADODB_FETCH_MODE = $savem;

		if (empty($rows))
			return false;

		foreach ($rows as $r)
		{

			$primaryIndex = $r[7] == 'P'?1:0;
			if (!$primary)
				/*
				 * Primary key not requested, ignore that one
				 */
				if ($r[7] == 'P')
					continue;

			$indexName = $this->getMetaCasedValue($r[1]);
			if (!isset($indices[$indexName]))
			{
				$unique = ($r[7] == 'U')?1:0;
				$indices[$indexName] = array('unique'=>$unique,
											 'primary'=>$primaryIndex,
											 'columns'=>array()
										);
			}
			$cols = explode('+',$r[6]);
			foreach ($cols as $colIndex=>$col)
			{
				if ($colIndex == 0)
					continue;
				$columnName = $this->getMetaCasedValue($col);
				$indices[$indexName]['columns'][] = $columnName;
			}

		}

		return $indices;

	}

	/**
	 * List procedures or functions in an array.
	 *
	 * We interrogate syscat.routines instead of calling the PHP
	 * function procedures because ADOdb requires the type of procedure
	 * this is not available in the php function
	 *
	 * @param	string $procedureNamePattern (optional)
	 * @param	string $catalog				 (optional)
	 * @param	string $schemaPattern		 (optional)

	 * @return array of procedures on current database.
	 *
	 */
	public function metaProcedures($procedureNamePattern = null, $catalog  = null, $schemaPattern  = null) {


		global $ADODB_FETCH_MODE;

		$metaProcedures = array();
		$procedureSQL   = '';
		$catalogSQL     = '';
		$schemaSQL      = '';

		$savem 			  = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;

		if ($procedureNamePattern)
			$procedureSQL = "AND ROUTINENAME LIKE " . strtoupper($this->qstr($procedureNamePattern));

		if ($catalog)
			$catalogSQL = "AND OWNER=" . strtoupper($this->qstr($catalog));

		if ($schemaPattern)
			$schemaSQL = "AND ROUTINESCHEMA LIKE {$this->qstr($schemaPattern)}";


		$fields = "
		ROUTINENAME,
		CASE ROUTINETYPE
			 WHEN 'P' THEN 'PROCEDURE'
			 WHEN 'F' THEN 'FUNCTION'
			 ELSE 'METHOD'
			 END AS ROUTINETYPE_NAME,
		ROUTINESCHEMA,
		REMARKS";

		$SQL = "SELECT $fields
				  FROM syscat.routines
				 WHERE OWNER IS NOT NULL
				  $procedureSQL
				  $catalogSQL
				  $schemaSQL
				ORDER BY ROUTINENAME
				";

		$result = $this->execute($SQL);

		$ADODB_FETCH_MODE = $savem;

		if (!$result)
			return false;

		while ($r = $result->fetchRow()){
			$procedureName = $this->getMetaCasedValue($r[0]);
			$schemaName    = $this->getMetaCasedValue($r[2]);
			$metaProcedures[$procedureName] = array('type'=> $r[1],
												   'catalog' => '',
												   'schema'  => $schemaName,
												   'remarks' => $r[3]
													);
		}

		return $metaProcedures;

	}

	/**
	  * Lists databases. Because instances are independent, we only know about
	  * the current database name
	  *
	  * @return string[]
	  */
	public function metaDatabases(){

		$dbName = $this->getMetaCasedValue($this->database);

		return (array)$dbName;

	}




/*
See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/db2/htm/db2datetime_data_type_changes.asp
/ SQL data type codes /
#define	SQL_UNKNOWN_TYPE	0
#define SQL_CHAR			1
#define SQL_NUMERIC		 2
#define SQL_DECIMAL		 3
#define SQL_INTEGER		 4
#define SQL_SMALLINT		5
#define SQL_FLOAT		   6
#define SQL_REAL			7
#define SQL_DOUBLE		  8
#if (DB2VER >= 0x0300)
#define SQL_DATETIME		9
#endif
#define SQL_VARCHAR		12


/ One-parameter shortcuts for date/time data types /
#if (DB2VER >= 0x0300)
#define SQL_TYPE_DATE	  91
#define SQL_TYPE_TIME	  92
#define SQL_TYPE_TIMESTAMP 93

#define SQL_UNICODE                             (-95)
#define SQL_UNICODE_VARCHAR                     (-96)
#define SQL_UNICODE_LONGVARCHAR                 (-97)
*/
	function DB2Types($t)
	{
		switch ((integer)$t) {
		case 1:
		case 12:
		case 0:
		case -95:
		case -96:
			return 'C';
		case -97:
		case -1: //text
			return 'X';
		case -4: //image
			return 'B';

		case 9:
		case 91:
			return 'D';

		case 10:
		case 11:
		case 92:
		case 93:
			return 'T';

		case 4:
		case 5:
		case -6:
			return 'I';

		case -11: // uniqidentifier
			return 'R';
		case -7: //bit
			return 'L';

		default:
			return 'N';
		}
	}

	public function metaColumns($table, $normalize=true)
	{
		global $ADODB_FETCH_MODE;

		$savem = $ADODB_FETCH_MODE;

		$schema = '%';
		$this->_findschema($table,$schema);
		$table = $this->getTableCasedValue($table);
		$colname = "%";
		$qid = db2_columns($this->_connectionID, null, $schema, $table, $colname);
		if (empty($qid))
		{
			if ($this->debug)
			{
				$errorMessage = @db2_conn_errormsg($this->_connectionID);
				ADOConnection::outp($errorMessage);
			}
			return false;
		}

		$rs = new ADORecordSet_db2($qid);

		if (!$rs)
			return false;

		$rs->_fetch();

		$retarr = array();

		/*
		$rs->fields indices
		0 TABLE_QUALIFIER
		1 TABLE_SCHEM
		2 TABLE_NAME
		3 COLUMN_NAME
		4 DATA_TYPE
		5 TYPE_NAME
		6 PRECISION
		7 LENGTH
		8 SCALE
		9 RADIX
		10 NULLABLE
		11 REMARKS
		12 Column Default
		13 SQL Data Type
		14 SQL DateTime SubType
		15 Max length in Octets
		16 Ordinal Position
		17 Is NULLABLE
		*/
		while (!$rs->EOF)
		{
			if ($rs->fields[2] == $table)
			{

				$fld       = new ADOFieldObject();
				$fld->name = $rs->fields[3];
				$fld->type = $this->DB2Types($rs->fields[4]);

				// ref: http://msdn.microsoft.com/library/default.asp?url=/archive/en-us/dnaraccgen/html/msdn_odk.asp
				// access uses precision to store length for char/varchar

				if ($fld->type == 'C' or $fld->type == 'X') {
					if ($rs->fields[4] <= -95) // UNICODE
						$fld->max_length = $rs->fields[7]/2;
					else
						$fld->max_length = $rs->fields[7];
				} else
					$fld->max_length = $rs->fields[7];

				$fld->not_null         = !empty($rs->fields[10]);
				$fld->scale            = $rs->fields[8];
				$fld->primary_key      = false;

				//$columnName = $this->getMetaCasedValue($fld->name);
				$columnName = strtoupper($fld->name);
				$retarr[$columnName] = $fld;

			}
			else if (sizeof($retarr)>0)
				break;

			$rs->MoveNext();

		}

		$rs->Close();
		if (empty($retarr))
			$retarr = false;

		/*
		 * Now we find out if the column is part of a primary key
		 */

		$qid = @db2_primary_keys($this->_connectionID, "", $schema, $table);
		if (empty($qid))
			return false;

		$rs = new ADORecordSet_db2($qid);

		if (!$rs)
		{
			$ADODB_FETCH_MODE = $savem;
			return $retarr;
		}
		$rs->_fetch();

		/*
		$rs->fields indices
		0 TABLE_CAT
		1 TABLE_SCHEM
		2 TABLE_NAME
		3 COLUMN_NAME
		4 KEY_SEQ
		5 PK_NAME
		*/
		while (!$rs->EOF) {
			if (strtoupper(trim($rs->fields[2])) == $table
			&& (!$schema || strtoupper($rs->fields[1]) == $schema))
			{
				$retarr[strtoupper($rs->fields[3])]->primary_key = true;
			}
			else if (sizeof($retarr)>0)
				break;

			$rs->MoveNext();
		}
		$rs->Close();

		$ADODB_FETCH_MODE = $savem;

		if (empty($retarr))
			return false;

		/*
		* If the fetch mode is numeric, return as numeric array
		*/
		if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM)
			$retarr = array_values($retarr);

		return $retarr;
	}

	/**
	  * In this version if prepareSp, we just check to make sure
	  * that the name of the stored procedure is correct
	  * If true, we returns an array
	  * else false
	  *
	  * @param	string	$procedureName
	  * @param	mixed   $parameters (not used in db2 connections)
	  * @return mixed[]
	  */
	function prepareSp($procedureName,$parameters=false) {

		global $ADODB_FETCH_MODE;

		$this->storedProcedureParameters = array('name'=>'',
												 'resource'=>false,
												 'in'=>array(),
												 'out'=>array(),
												 'index'=>array(),
												 'parameters'=>array(),
												 'keyvalue' => array());

		//$procedureName = strtoupper($procedureName);
		//$procedureName = $this->getTableCasedValue($procedureName);

		$savem = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;

		$qid = db2_procedures($this->_connectionID, NULL , '%' , $procedureName );

		$ADODB_FETCH_MODE = $savem;

		if (!$qid)
		{
			if ($this->debug)
				ADOConnection::outp(sprintf('No Procedure of name %s available',$procedureName));
			return false;
		}



		$this->storedProcedureParameters['name'] = $procedureName;
		/*
		 * Now we know we have a valid procedure name, lets see if it requires
		 * parameters
		 */
		$savem = $ADODB_FETCH_MODE;
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;

		$qid = db2_procedure_columns($this->_connectionID, NULL , '%' , $procedureName , NULL );

		$ADODB_FETCH_MODE = $savem;

		if (!$qid)
		{
			if ($this->debug)
				ADOConnection::outp(sprintf('No columns of name %s available',$procedureName));
			return false;
		}
		$rs = new ADORecordSet_db2($qid);
		if (!$rs)
			return false;

		$preparedStatement = 'CALL %s(%s)';
		$parameterMarkers = array();
		while (!$rs->EOF)
		{
			$parameterName = $rs->fields[3];
			if ($parameterName == '')
			{
				$rs->moveNext();
				continue;
			}
			$parameterType = $rs->fields[4];
			$ordinalPosition = $rs->fields[17];
			switch($parameterType)
			{
			case DB2_PARAM_IN:
			case DB2_PARAM_INOUT:
				$this->storedProcedureParameters['in'][$parameterName] = '';
				break;
			case DB2_PARAM_INOUT:
			case DB2_PARAM_OUT:
				$this->storedProcedureParameters['out'][$parameterName] = '';
				break;
			}
			$this->storedProcedureParameters['index'][$parameterName] = $ordinalPosition;
			$this->storedProcedureParameters['parameters'][$ordinalPosition] = $rs->fields;
			$rs->moveNext();

		}
		$parameterCount = count($this->storedProcedureParameters['index']);
		$parameterMarkers = array_fill(0,$parameterCount,'?');

		/*
		 * We now know how many parameters to bind to the stored procedure
		 */
		$parameterList = implode(',',$parameterMarkers);

		$sql = sprintf($preparedStatement,$procedureName,$parameterList);

		$spResource = @db2_prepare($this->_connectionID,$sql);

		if (!$spResource)
		{
			$errorMessage = @db2_conn_errormsg($this->_connectionID);
			$this->_errorMsg = $errorMessage;

			if ($this->debug)
				ADOConnection::outp($errorMessage);

			return false;
		}

		$this->storedProcedureParameters['resource'] = $spResource;

		if ($this->debug)
		{

			ADOConnection::outp('The following parameters will be used in the SP call');
			ADOConnection::outp(print_r($this->storedProcedureParameters));
		}
		/*
		 * We now have a stored parameter resource
		 * to bind to. The spResource and sql that is returned are
		 * not usable, its for dummy compatibility. Everything
		 * will be handled by the storedProcedureParameters
		 * array
		 */
		return array($sql,$spResource);

	}

	private function storedProcedureParameter(&$stmt,
											  &$var,
											  $name,
											  $isOutput=false,
											  $maxLen=4000,
											  $type=false)
	{


		$name = strtoupper($name);

		/*
		 * Must exist in the list of parameter names for the type
		 */
		if ($isOutput
		&& !isset( $this->storedProcedureParameters['out'][$name]))
		{
			$errorMessage = sprintf('%s is not a valid OUT parameter name',$name);

			$this->_errorMsg = $errorMessage;
			if ($this->debug)
				ADOConnection::outp($errorMessage);
			return false;
		}

		if (!$isOutput
		&& !isset( $this->storedProcedureParameters['in'][$name]))
		{
			$errorMessage = sprintf('%s is not a valid IN parameter name',$name);

			$this->_errorMsg = $errorMessage;
			if ($this->debug)
				ADOConnection::outp($errorMessage);
			return false;
		}

		/*
		 * We will use these values to bind to when we execute
		 * the query
		 */
		$this->storedProcedureParameters['keyvalue'][$name] = &$var;

		return true;

	}

	/**
	* Executes a prepared stored procedure.
	*
	* The function uses the previously accumulated information and
	* resources in the $storedProcedureParameters array
	*
	* @return mixed	The statement id if successful, or false
	*/
	private function executeStoredProcedure()
	{

		/*
		 * Get the previously built resource
		 */
		$stmtid = $this->storedProcedureParameters['resource'];

		/*
		 * Bind our variables to the DB2 procedure
		 */
		foreach ($this->storedProcedureParameters['keyvalue'] as $spName=>$spValue){

			/*
			 * Get the ordinal position, required for binding
			 */
			$ordinalPosition = $this->storedProcedureParameters['index'][$spName];

			/*
			 * Get the db2 column dictionary for the parameter
			 */
			$columnDictionary = $this->storedProcedureParameters['parameters'][$ordinalPosition];
			$parameterType    = $columnDictionary[4];
			$dataType         = $columnDictionary[5];
			$precision        = $columnDictionary[10];
			$scale        	  = $columnDictionary[9];

			$ok = @db2_bind_param ($this->storedProcedureParameters['resource'],
								  $ordinalPosition ,
								  $spName,
								  $parameterType,
								  $dataType,
								  $precision,
								  $scale
								  );

			if (!$ok)
			{
				$this->_errorMsg  = @db2_stmt_errormsg();
				$this->_errorCode = @db2_stmt_error();

				if ($this->debug)
					ADOConnection::outp($this->_errorMsg);
				return false;
			}

			if ($this->debug)
				ADOConnection::outp("Correctly Bound parameter $spName to procedure");

			/*
			 * Build a variable in the current environment that matches
			 * the parameter name
			 */
			${$spName} = $spValue;

		}

		/*
		 * All bound, execute
		 */

		if (!@db2_execute($stmtid))
		{
			$this->_errorMsg = @db2_stmt_errormsg();
			$this->_errorCode = @db2_stmt_error();

			if ($this->debug)
				ADOConnection::outp($this->_errorMsg);
			return false;
		}

		/*
		 * We now take the changed parameters back into the
		 * stored procedures array where we can query them later
		 * Remember that $spValue was passed in by reference, so we
		 * can access the value in the variable that was originally
		 * passed to inParameter or outParameter
		 */
		foreach ($this->storedProcedureParameters['keyvalue'] as $spName=>$spValue)
		{
			/*
			 * We make it available to the environment
			 */
			$spValue = ${$spName};
			$this->storedProcedureParameters['keyvalue'][$spName] = $spValue;
		}

		return $stmtid;
	}

	/**
	 *
	 * Accepts an input or output parameter to bind to either a stored
	 * or prepared statements. For DB2, this should not be called as an
	 * API. always wrap with inParameter and outParameter
	 *
	 * @param mixed[] $stmt 		Statement returned by Prepare() or PrepareSP().
	 * @param mixed   $var 		PHP variable to bind to. Can set to null (for isNull support).
	 * @param string  $name 		Name of stored procedure variable name to bind to.
	 * @param int	 $isOutput 	optional) Indicates direction of parameter
	 * 							0/false=IN  1=OUT  2= IN/OUT
	 *							This is ignored for Stored Procedures
	 * @param int	$maxLen		(optional)Holds an maximum length of the variable.
	 *							This is ignored for Stored Procedures
	 * @param int	$type 		(optional) The data type of $var.
	 *							This is ignored for Stored Procedures
	 *
	 * @return bool				Success of the operation
	 */
	public function parameter(&$stmt, &$var, $name, $isOutput=false, $maxLen=4000, $type=false)
	{

		/*
		 * If the $stmt is the name of a stored procedure we are
		 * setting up, we will process it one way, otherwise
		 * we assume we are setting up a prepared statement
		*/
		if (is_array($stmt))
		{
			if ($this->debug)
				ADOConnection::outp("Adding parameter to stored procedure");
			if ($stmt[1] == $this->storedProcedureParameters['resource'])
				return $this->storedProcedureParameter($stmt[1],
														$var,
														$name,
														$isOutput,
														$maxLen,
														$type);

		}

		/*
		 * We are going to add a parameter to a prepared statement
		 */
		if ($this->debug)
			ADOConnection::outp("Adding parameter to prepared statement");
	}


	/**
	 * Prepares a prepared SQL statement, not used for stored procedures
	 *
	 * @param string	$sql
	 *
	 * @return mixed
	 */
	function prepare($sql)
	{

		if (! $this->_bindInputArray) return $sql; // no binding

		$stmt = @db2_prepare($this->_connectionID,$sql);
		if (!$stmt) {
			// we don't know whether db2 driver is parsing prepared stmts, so just return sql
			return $sql;
		}
		return array($sql,$stmt,false);
	}

	/**
	 * Executes a query
	 *
	 * @param	mixed $sql
	 * @param	mixed $inputarr	An optional array of parameters
	 *
	 * @return mixed				either the queryID or false
	 */
	function _query(&$sql,$inputarr=false)
	{
		$db2Options = array();
		/*
		 * Use DB2 Internal case handling for best speed
		 */
		switch(ADODB_ASSOC_CASE)
		{
		case ADODB_ASSOC_CASE_UPPER:
			$db2Options = array('db2_attr_case'=>DB2_CASE_UPPER);
			$setOption = @db2_set_option($this->_connectionID,$db2Options,1);
			break;

		 case ADODB_ASSOC_CASE_LOWER:
			$db2Options = array('db2_attr_case'=>DB2_CASE_LOWER);
			$setOption = @db2_set_option($this->_connectionID,$db2Options,1);
			break;

		default:
			$db2Options = array('db2_attr_case'=>DB2_CASE_NATURAL);
			$setOption = @db2_set_option($this->_connectionID,$db2Options,1);
		}

		if ($inputarr)
		{
			if (is_array($sql))
			{
				$stmtid = $sql[1];
			}
			else
			{
				$stmtid = @db2_prepare($this->_connectionID,$sql);

				if ($stmtid == false)
				{
					$this->_errorMsg  = @db2_stmt_errormsg();
					$this->_errorCode = @db2_stmt_error();

					if ($this->debug)
						ADOConnection::outp($this->_errorMsg);

					return false;
				}
			}

			if (! @db2_execute($stmtid,$inputarr))
			{
				$this->_errorMsg = @db2_stmt_errormsg();
				$this->_errorCode = @db2_stmt_error();
				if ($this->debug)
					ADOConnection::outp($this->_errorMsg);
				return false;
			}

		}
		else if (is_array($sql))
		{

			/*
			 * Either a prepared statement or a stored procedure
			 */

			if (is_array($this->storedProcedureParameters)
				&& is_resource($this->storedProcedureParameters['resource']
			))
				/*
				 * This is all handled in the separate method for
				 * readability
				 */
				return $this->executeStoredProcedure();

			/*
			 * First, we prepare the statement
			 */
			$stmtid = @db2_prepare($this->_connectionID,$sql[0]);
			if (!$stmtid){
				$this->_errorMsg = @db2_stmt_errormsg();
				$this->_errorCode = @db2_stmt_error();
				if ($this->debug)
					ADOConnection::outp("Prepare failed: " . $this->_errorMsg);

				return false;
			}
			/*
			 * We next bind some input parameters
			 */
			$ordinal = 1;
			foreach ($sql[1] as $psVar=>$psVal){
				${$psVar} = $psVal;
				$ok = @db2_bind_param($stmtid, $ordinal, $psVar, DB2_PARAM_IN);
				if (!$ok)
				{
					$this->_errorMsg = @db2_stmt_errormsg();
					$this->_errorCode = @db2_stmt_error();
					if ($this->debug)
						ADOConnection::outp("Bind failed: " . $this->_errorMsg);
					return false;
				}
			}

			if (!@db2_execute($stmtid))
			{
				$this->_errorMsg = @db2_stmt_errormsg();
				$this->_errorCode = @db2_stmt_error();
				if ($this->debug)
					ADOConnection::outp($this->_errorMsg);
				return false;
			}

			return $stmtid;
		}
		else
		{

			$stmtid = @db2_exec($this->_connectionID,$sql);
		}
		$this->_lastAffectedRows = 0;
		if ($stmtid)
		{
			if (@db2_num_fields($stmtid) == 0)
			{
				$this->_lastAffectedRows = db2_num_rows($stmtid);
				$stmtid = true;
			}
			else
			{
				$this->_lastAffectedRows = 0;
			}

			$this->_errorMsg = '';
			$this->_errorCode = 0;

		}
		else
		{

			$this->_errorMsg = @db2_stmt_errormsg();
			$this->_errorCode = @db2_stmt_error();

		}
		return $stmtid;
	}

	/*
		Insert a null into the blob field of the table first.
		Then use UpdateBlob to store the blob.

		Usage:

		$conn->execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)');
		$conn->UpdateBlob('blobtable','blobcol',$blob,'id=1');
	*/
	function updateBlob($table,$column,$val,$where,$blobtype='BLOB')
	{
		return $this->execute("UPDATE $table SET $column=? WHERE $where",array($val)) != false;
	}

	// returns true or false
	function _close()
	{
		$ret = @db2_close($this->_connectionID);
		$this->_connectionID = false;
		return $ret;
	}

	function _affectedrows()
	{
		return $this->_lastAffectedRows;
	}

	/**
	 * Gets a meta cased parameter
	 *
	 * Receives an input variable to be processed per the metaCasing
	 * rule, and returns the same value, processed
	 *
	 * @param string $value
	 *
	 * @return string
	 */
	final public function getMetaCasedValue($value)
	{
		global $ADODB_ASSOC_CASE;

		switch($ADODB_ASSOC_CASE)
		{
		case ADODB_ASSOC_CASE_LOWER:
			$value = strtolower($value);
			break;
		case ADODB_ASSOC_CASE_UPPER:
			$value = strtoupper($value);
			break;
		}
		return $value;
	}


	const TABLECASE_LOWER    =  0;
	const TABLECASE_UPPER    =  1;
	const TABLECASE_DEFAULT  =  2;

	/**
	 * Controls the casing of the table provided to the meta functions
	 */
	private $tableCase = 2;

	/**
	 * Sets the table case parameter
	 *
	 * @param int $caseOption
	 * @return null
	 */
	final public function setTableCasing($caseOption)
	{
		$this->tableCase = $caseOption;
	}

	/**
	 * Gets the table casing parameter
	 *
	 * @return int $caseOption
	 */
	final public function getTableCasing()
	{
		return $this->tableCase;
	}

	/**
	 * Gets a table cased parameter
	 *
	 * Receives an input variable to be processed per the tableCasing
	 * rule, and returns the same value, processed
	 *
	 * @param string $value
	 *
	 * @return string
	 */
	final public function getTableCasedValue($value)
	{
		switch($this->tableCase)
		{
		case self::TABLECASE_LOWER:
			$value = strtolower($value);
			break;
		case self::TABLECASE_UPPER:
			$value = strtoupper($value);
			break;
		}
		return $value;
	}

}

/*--------------------------------------------------------------------------------------
	 Class Name: Recordset
--------------------------------------------------------------------------------------*/

class ADORecordSet_db2 extends ADORecordSet {

	var $bind = false;
	var $databaseType = "db2";
	var $dataProvider = "db2";
	var $useFetchArray;

	function __construct($id,$mode=false)
	{
		if ($mode === false) {
			global $ADODB_FETCH_MODE;
			$mode = $ADODB_FETCH_MODE;
		}
		$this->fetchMode = $mode;

		$this->_queryID = $id;
	}


	// returns the field object
	function fetchField($offset = 0)
	{
		$o			   = new ADOFieldObject();
		$o->name 	   = @db2_field_name($this->_queryID,$offset);
		$o->type 	   = @db2_field_type($this->_queryID,$offset);
		$o->max_length = @db2_field_width($this->_queryID,$offset);

		/*
		if (ADODB_ASSOC_CASE == 0)
			$o->name = strtolower($o->name);
		else if (ADODB_ASSOC_CASE == 1)
			$o->name = strtoupper($o->name);
		*/
		return $o;
	}

	/* Use associative array to get fields array */
	function fields($colname)
	{

		if ($this->fetchMode & ADODB_FETCH_ASSOC) {
			return $this->fields[$colname];
		}

		if (!$this->bind) {
			$this->bind = array();
			for ($i=0; $i < $this->_numOfFields; $i++) {
				$o = $this->FetchField($i);
				$this->bind[strtoupper($o->name)] = $i;
			}
		}

		 return $this->fields[$this->bind[strtoupper($colname)]];
	}


	function _initrs()
	{
		global $ADODB_COUNTRECS;
		$this->_numOfRows = ($ADODB_COUNTRECS) ? @db2_num_rows($this->_queryID) : -1;

		$this->_numOfFields = @db2_num_fields($this->_queryID);

		// some silly drivers such as db2 as/400 and intersystems cache return _numOfRows = 0

		if ($this->_numOfRows == 0)
			$this->_numOfRows = -1;
	}

	function _seek($row)
	{
		return false;
	}

	function getArrayLimit($nrows,$offset=0)
	{
		if ($offset <= 0) {
			$rs = $this->GetArray($nrows);
			return $rs;
		}

		$this->Move($offset);


		$results = array();
		$cnt = 0;
		while (!$this->EOF && $nrows != $cnt) {
			$results[$cnt++] = $this->fields;
			$this->MoveNext();
		}

		return $results;
	}

	function moveNext()
	{
		if ($this->EOF || $this->_numOfRows == 0)
			return false;

		$this->_currentRow++;

		$this->processCoreFetch();
		return $this->processMoveRecord();

	}

	private function processCoreFetch()
	{
		switch ($this->fetchMode){
		case ADODB_FETCH_ASSOC:

			/*
			 * Associative array
			 */
			$this->fields = @db2_fetch_assoc($this->_queryID);
			break;

		case ADODB_FETCH_BOTH:
			/*
			 * Fetch both numeric and Associative array
			 */
			$this->fields = @db2_fetch_both($this->_queryID);
			break;
		default:
			/*
			 * Numeric array
			 */
			$this->fields = @db2_fetch_array($this->_queryID);
			break;
		}
	}

	private function processMoveRecord()
	{
		if (!$this->fields){
			$this->EOF = true;
			return false;
		}

		return true;
	}

	function _fetch()
	{
		$this->processCoreFetch();
		if ($this->fields)
			return true;

		$this->fields = false;
		return false;
	}

	function _close()
	{
		$ok = @db2_free_result($this->_queryID);
		if (!$ok)
		{
			$this->connection->_errorMsg  = @db2_stmt_errormsg($this->_queryID);
			$this->connection->_errorCode = @db2_stmt_error();

			if ($this->debug)
				ADOConnection::outp($this->connection->_errorMsg);
			return false;
		}
	}

}
