<?php
/**
 * Project:     PHPPDO
 * File:        base_statement.php
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 * For questions, help, comments, discussion, etc.,
 * visit <http://devuni.com>
 *
 * @link http://devuni.com/
 * @Copyright 2007, 2008, 2009 Nikolay Ananiev.
 * @author Nikolay Ananiev <admin at devuni dot com>
 */

class phppdo_base_statement_iterator implements Iterator
{
    private $stmt;
    private $row;
    private $cnt = -1;

    public function __construct(PDOStatement $stmt)
    {
        $this->stmt = $stmt;
    }

    public function key()
    {
        return $this->cnt;
    }

    public function rewind()
    {
        $this->next();
    }

    public function current()
    {
        return $this->row;
    }

    public function next()
    {
        $this->row = $this->stmt->fetch();
        $this->cnt++;
    }

    public function valid()
    {
        if ($this->row === false) {
            return false;
        }

        return true;
    }
}

// PHP 5.2.6 and earlier's fetchObject method signature differes than PHP 5.2.7 and up
$phppdo_error_reporting = error_reporting();
if (version_compare(PHP_VERSION, '5.2.7', '<') && ($phppdo_error_reporting & E_STRICT)) {
    error_reporting($phppdo_error_reporting & ~E_STRICT);
} else {
    $phppdo_error_reporting = null;
}

abstract class phppdo_base_statement extends PDOStatement implements IteratorAggregate
{
    protected $_driver;
    protected $_link;
    protected $_result = null;
    protected $_result_name;

    protected $_params_info;
    protected $_bound_params        = array();

    private $driver_options         = array();
    private $last_error             = array('');

    private $prepared;
    private $bound_columns          = array();
    private $columns_meta           = null;

    private $fetch_func             = 'fetch';
    private $fetch_mode             = array(
        PDO::FETCH_COLUMN       => array(0),
    );

    // Required by IteratorAggregate
    public function getIterator()
    {
        return new phppdo_base_statement_iterator($this);
    }

    public function bindColumn($column, &$param, $type = 0, $maxlen = 0, $driver_options = null)
    {
        if ($this->_result === null) {
            return false;
        } elseif (is_numeric($column)) {
            if ($column < 1) {
                $this->_set_error(0, 'Invalid parameter number: Columns/Parameters are 1-based', 'HY093', PDO::ERRMODE_WARNING, 'bindColumn');

                return false;
            }

            $column -= 1;
        }

        $this->bound_columns[$column] = array(&$param, $type);

        return true;
    }

    public function bindParam($parameter, &$variable, $data_type = -1, $length = 0, $driver_options = null)
    {
        if ($parameter[0] != ':' && !is_numeric($parameter)) {
            $parameter = ':'.$parameter;
        }

        if (isset($this->_params_info[$parameter])) {
            $this->_bound_params[$this->_params_info[$parameter]] = array(&$variable, $data_type, $length);

            return true;
        }

        return false;
    }

    public function bindValue($parameter, $value, $data_type = -1)
    {
        return $this->bindParam($parameter, $value, $data_type);
    }

    public function errorCode()
    {
        if (func_num_args() > 0) {
            return false;
        }

        return $this->last_error[0];
    }

    public function errorInfo()
    {
        if (func_num_args() > 0) {
            return false;
        }

        return $this->last_error;
    }

    public function execute($input_parameters = array())
    {
        if (!$this->prepared) {
            $this->_set_error(0, 'Invalid parameter number: statement not prepared', 'HY093', PDO::ERRMODE_WARNING, 'execute');

            return false;
        }

        if (is_array($input_parameters)) {
            $status = true;
            foreach ($input_parameters as $p => &$v) {
                if (is_numeric($p)) {
                    if ($p >= 0) {
                        $status = $this->bindParam(($p + 1), $v);
                    }
                } else {
                    $status = $this->bindParam($p, $v);
                }

                if (!$status) {
                    $this->_set_error(0, 'Invalid parameter number: number of bound variables does not match number of tokens', 'HY093', PDO::ERRMODE_WARNING, 'execute');

                    return false;
                }
            }
        }

        if ($this->_execute()) {
            $this->_driver->clear_error($this->last_error);

            return true;
        }

        return false;
    }

    public function fetch($fetch_style = 0, $cursor_orientation = 0, $cursor_offset = 0)
    {
        if ($this->_result) {
            $fetch_mode = & $this->fetch_mode;

            switch ($fetch_style) {
                case 0:
                    $fetch_style = $this->driver_options[PDO::ATTR_DEFAULT_FETCH_MODE];

                    switch ($fetch_style) {
                        case PDO::FETCH_CLASS:
                        case PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE:
                            if (!isset($fetch_mode[PDO::FETCH_CLASS]) || !$fetch_mode[PDO::FETCH_CLASS][0]) {
                                $this->_set_error(0, 'General error: No fetch class specified', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);
                                $this->_set_error(0, 'General error', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);

                                return false;
                            }
                        break;
                    }
                break;

                case PDO::FETCH_GROUP:
                    $fetch_style |= $this->driver_options[PDO::ATTR_DEFAULT_FETCH_MODE];
                break;
            }

            switch ($fetch_style) {
                case PDO::FETCH_COLUMN:
                    return $this->fetchColumn($fetch_mode[PDO::FETCH_COLUMN][0]);
                break;

                case PDO::FETCH_FUNC:
                    $this->_set_error(0, 'General error: PDO::FETCH_FUNC is only allowed in PDOStatement::fetchAll()', 'HY000', PDO::ERRMODE_WARNING, $this->fetch_func);

                    return false;
                break;
            }

            $row = $this->_fetch_row();
            if (!$row) {
                return false;
            }

            $stringify = $this->driver_options[PDO::ATTR_STRINGIFY_FETCHES];
            $nulls = $this->driver_options[PDO::ATTR_ORACLE_NULLS];

            if ($stringify || $nulls != PDO::NULL_NATURAL) {
                $driver = $this->_driver;
                $cnt = count($row);

                // seems like foreach($row as &$r) modifies $row and makes the value a referece in php 5.2.5
                for ($x = 0; $x < $cnt; $x++) {
                    $driver->filter_result($row[$x], $stringify, $nulls);
                }
            }

            if ($this->bound_columns && $this->fetch_func == 'fetch') {
                $this->bind_columns($row);
            }

            switch ($fetch_style) {
                case PDO::FETCH_ASSOC:
                    return $this->make_assoc($row);
                break;

                case PDO::FETCH_ASSOC|PDO::FETCH_GROUP:
                    $row = $this->make_assoc($row);

                    return array(array_shift($row) => $row);
                break;

                case PDO::FETCH_NAMED:
                    return $this->make_named($row);
                break;

                case PDO::FETCH_NUM:
                    return $row;
                break;

                case PDO::FETCH_NUM|PDO::FETCH_GROUP:
                    return array(array_shift($row) => $row);
                break;

                case PDO::FETCH_BOTH:
                    return $this->make_both($row);
                break;

                case PDO::FETCH_LAZY:
                case PDO::FETCH_OBJ:
                    return $this->map_obj_props(new stdClass(), $row);
                break;

                case PDO::FETCH_CLASS:
                case PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE:
                    if (isset($fetch_mode[PDO::FETCH_CLASS]) && $fetch_mode[PDO::FETCH_CLASS][0]) {
                        $class_name = $fetch_mode[PDO::FETCH_CLASS][0];
                    } else {
                        $class_name = 'stdClass';
                    }

                    if (isset($fetch_mode[PDO::FETCH_CLASS]) && $fetch_mode[PDO::FETCH_CLASS][1]) {
                        $class  = new ReflectionClass($class_name);
                        $obj    = $class->newInstanceArgs($fetch_mode[PDO::FETCH_CLASS][1]);
                    } else {
                        $obj = new $class_name();
                    }

                    return $this->map_obj_props($obj, $row);
                break;

                case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE:
                case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE|PDO::FETCH_SERIALIZE:
                    $class = array_shift($row);
                    if (!$class) {
                        if (isset($fetch_mode[PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE]) && $fetch_mode[PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE][0]) {
                            $class = $fetch_mode[PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE][0];
                        } else {
                            $class = 'stdClass';
                        }
                    }

                    $obj = new $class();
                    if ($fetch_style & PDO::FETCH_SERIALIZE) {
                        if (is_callable(array($obj, 'unserialize'))) {
                            $obj->unserialize(array_shift($row));
                        } else {
                            $this->_set_error(0, 'General error: cannot unserialize class', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);

                            return false;
                        }
                    } else {
                        $this->map_obj_props($obj, $row, 1);
                    }

                    return $obj;
                break;

                case PDO::FETCH_INTO:
                    return $this->map_obj_props($fetch_mode[PDO::FETCH_INTO][0], $row);
                break;

                case PDO::FETCH_KEY_PAIR:
                    if (count($row) != 2) {
                        $this->_set_error(0, 'General error: PDO::FETCH_KEY_PAIR fetch mode requires the result set to contain extactly 2 columns.', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);
                        $this->_set_error(0, 'General error', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);

                        return false;
                    }

                    return array($row[0] => &$row[1]);
                break;

                case PDO::FETCH_BOUND:
                    return true;
                break;
            }
        }

        return false;
    }

    public function fetchAll($fetch_style = 0, $column_index = null, $ctor_args = array())
    {
        if (!$this->_result) {
            return false;
        }
        $result = array();

        if ($fetch_style) {
            $style = $fetch_style;
        } else {
            $style = $this->driver_options[PDO::ATTR_DEFAULT_FETCH_MODE];
        }

        $this->fetch_func = 'fetchAll';

        switch ($style) {
            case PDO::FETCH_ASSOC:
            case PDO::FETCH_NAMED:
            case PDO::FETCH_NUM:
            case PDO::FETCH_BOTH:
            case PDO::FETCH_LAZY:
            case PDO::FETCH_OBJ:
            case PDO::FETCH_INTO:
            case PDO::FETCH_BOUND:
            case PDO::FETCH_GROUP:
                while ($row = $this->fetch($fetch_style)) {
                    $result[] = $row;
                }
            break;

            case PDO::FETCH_FUNC:
            case PDO::FETCH_FUNC|PDO::FETCH_GROUP:
                if (!$column_index && isset($this->fetch_mode[PDO::FETCH_FUNC])) {
                    $column_index = $this->fetch_mode[PDO::FETCH_FUNC][0];
                }

                if ($column_index) {
                    if (is_callable($column_index)) {
                        if ($style & PDO::FETCH_GROUP) {
                            while ($row = $this->fetch(PDO::FETCH_NUM)) {
                                $key = array_shift($row);
                                if (isset($result[$key])) {
                                    $result[$key][] = call_user_func_array($column_index, $row);
                                } else {
                                    $result[$key] = array(call_user_func_array($column_index, $row));
                                }
                            }
                        } else {
                            while ($row = $this->fetch(PDO::FETCH_NUM)) {
                                $result[] = call_user_func_array($column_index, $row);
                            }
                        }
                    } else {
                        $this->_set_error(0, 'General error: user-supplied function must be a valid callback', 'HY000', PDO::ERRMODE_WARNING, $this->fetch_func);
                    }
                } else {
                    $this->_set_error(0, 'General error: No fetch function specified', 'HY000', PDO::ERRMODE_WARNING, $this->fetch_func);

                    if ($style != $fetch_style) {
                        $this->_set_error(0, 'General error', 'HY000', PDO::ERRMODE_SILENT, $this->fetch_func);
                    }
                }
            break;

            case PDO::FETCH_CLASS:
            case PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE:
                if ($column_index) {
                    $this->setFetchMode(PDO::FETCH_CLASS, $column_index, $ctor_args);
                }

                while ($row = $this->fetch($fetch_style)) {
                    $result[] = $row;
                }
            break;

            case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE:
                $this->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE, $column_index, $ctor_args);
                while ($row = $this->fetch(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE)) {
                    $result[] = $row;
                }
            break;

            case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE|PDO::FETCH_GROUP:
                $this->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE, $column_index, $ctor_args);
                $first_property = null;

                while ($row = $this->fetch(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE)) {
                    if ($first_property === null) {
                        $first_property = key(get_object_vars($row));
                    }

                    $key = $row->$first_property;
                    unset($row->$first_property);

                    if (isset($result[$key])) {
                        $result[$key][] = $row;
                    } else {
                        $result[$key] = array($row);
                    }
                }
            break;

            case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE|PDO::FETCH_UNIQUE:
                $this->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE, $column_index, $ctor_args);
                $first_property = null;

                while ($row = $this->fetch(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE)) {
                    if ($first_property === null) {
                        $first_property = key(get_object_vars($row));
                    }

                    $key = $row->$first_property;
                    unset($row->$first_property);

                    $result[$key] = $row;
                }
            break;

            case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE|PDO::FETCH_SERIALIZE:
                $this->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE, $column_index, $ctor_args);
                while ($row = $this->fetch(PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE|PDO::FETCH_SERIALIZE)) {
                    $result[] = $row;
                }
            break;

            case PDO::FETCH_KEY_PAIR:
                while ($row = $this->fetch(PDO::FETCH_NUM)) {
                    $result[$row[0]] = $row[1];
                }
            break;

            case PDO::FETCH_COLUMN:
                if ($column_index === null) {
                    $column_index = $this->fetch_mode[PDO::FETCH_COLUMN][0];
                }
                while (($row = $this->fetchColumn($column_index)) !== false) {
                    $result[] = $row;
                }
            break;

            case PDO::FETCH_COLUMN|PDO::FETCH_UNIQUE:
                if ($column_index === null) {
                    $column_index = 1;
                }

                while ($row = $this->fetch(PDO::FETCH_NUM)) {
                    if (isset($result[$row[0]])) {
                        continue;
                    }
                    $result[$row[0]] = $row[$column_index];
                }
            break;

            case PDO::FETCH_COLUMN|PDO::FETCH_GROUP:
                if ($column_index === null) {
                    $column_index = 1;
                }

                while ($row = $this->fetch(PDO::FETCH_NUM)) {
                    $key = $row[0];
                    if (isset($result[$key])) {
                        $result[$key][] = $row[$column_index];
                    } else {
                        $result[$key] = array($row[$column_index]);
                    }
                }
            break;

            case PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC:
            case PDO::FETCH_UNIQUE|PDO::FETCH_NUM:
                $s = $style & ~PDO::FETCH_UNIQUE;
                while ($row = $this->fetch($s)) {
                    $result[array_shift($row)] = $row;
                }
            break;

            case PDO::FETCH_GROUP|PDO::FETCH_ASSOC:
            case PDO::FETCH_GROUP|PDO::FETCH_NUM:
            case PDO::FETCH_COLUMN|PDO::FETCH_NUM:
                $s = $style & ~PDO::FETCH_GROUP;

                if ($s == PDO::FETCH_COLUMN) {
                    if ($column_index === null) {
                        $column_index = $fetch_mode[PDO::FETCH_COLUMN][0];
                    }

                    while ($row = $this->fetch(PDO::FETCH_NUM)) {
                        $key = array_shift($row[$column_index]);
                        if (isset($result[$key])) {
                            $result[$key] = array_merge($result[$key], $row);
                        } else {
                            $result[$key] = array($row);
                        }
                    }
                } else {
                    while ($row = $this->fetch($s)) {
                        $key = array_shift($row);
                        if (isset($result[$key])) {
                            $result[$key][] = $row;
                        } else {
                            $result[$key] = array($row);
                        }
                    }
                }
            break;
        }

        $this->fetch_func = 'fetch';

        return $result;
    }

    public function fetchColumn($column_number = 0)
    {
        if ($this->_result) {
            $row = $this->_fetch_row();
            if ($row && array_key_exists($column_number, $row)) {
                $this->_driver->filter_result($row[$column_number], $this->driver_options[PDO::ATTR_STRINGIFY_FETCHES], $this->driver_options[PDO::ATTR_ORACLE_NULLS]);

                return $row[$column_number];
            }
        }

        return false;
    }

    public function fetchObject($class_name = '', $ctor_args = array())
    {
        if ($class_name) {
            $this->setFetchMode(PDO::FETCH_CLASS, $class_name, $ctor_args);
        }

        return $this->fetch(PDO::FETCH_CLASS);
    }

    public function getAttribute($attribute)
    {
        if (func_num_args() != 1 || !is_int($attribute)) {
            return false;
        }

        return $this->_driver->getAttribute($attribute, $this->driver_options, 'PDOStatement::getAttribute', $this->last_error);
    }

    public function nextRowset()
    {
        return false;
    }

    public function setAttribute($attribute, $value)
    {
        if (func_num_args() != 2) {
            return false;
        }

        switch ($attribute) {
            case PDO::ATTR_PREFETCH:
                $this->_set_error(0, 'Driver does not support this function: This driver doesn\'t support setting attributes', 'IM001', PDO::ERRMODE_WARNING, 'setAttribute', $this->last_error);
            break;

            default:
                return $this->_driver->setAttribute($attribute, $value, $this->driver_options, 'PDOStatement::setAttribute', $this->last_error);
            break;
        }

        return false;
    }

    public function setFetchMode($mode, $param = '', $ctorargs = array())
    {
        switch ($mode) {
            case PDO::FETCH_LAZY:
            case PDO::FETCH_ASSOC:
            case PDO::FETCH_NAMED:
            case PDO::FETCH_NUM:
            case PDO::FETCH_BOTH:
            case PDO::FETCH_OBJ:
            case PDO::FETCH_BOUND:
            case PDO::FETCH_COLUMN:
            case PDO::FETCH_FUNC:
            case PDO::FETCH_CLASS:
            case PDO::FETCH_INTO:
            case PDO::FETCH_KEY_PAIR:
            case PDO::FETCH_SERIALIZE:
            case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE:

                switch ($mode) {
                    case PDO::FETCH_INTO:
                        if (!is_object($param)) {
                            return false;
                        }
                    break;

                    case PDO::FETCH_CLASS:
                        if (!class_exists($param)) {
                            return false;
                        }
                    break;

                    case PDO::FETCH_CLASS|PDO::FETCH_CLASSTYPE:
                        if ($param && !class_exists($param)) {
                            return false;
                        }
                    break;

                    case PDO::FETCH_COLUMN:
                        if (!is_numeric($param) || $param < 0) {
                            return false;
                        }
                    break;
                }

                $this->fetch_mode[$mode] = array(&$param, $ctorargs);
                $this->driver_options[PDO::ATTR_DEFAULT_FETCH_MODE] = $mode;

                return true;
            break;
        }

        return false;
    }

    public static function _new_instance(&$data, &$statement)
    {
        if (isset($data[1]) && count($data[1])) {
            $class = new ReflectionClass($data[0]);
            $obj = $class->newInstanceArgs($data[1]);
        } else {
            $obj = new $data[0]();
        }

        $obj->queryString = & $statement;

        return $obj;
    }

    final public function _setup($link, $driver, &$driver_options, $prepared, $params_info)
    {
        if ($this->_link) {
            return false;
        }

        $this->_link            = $link;
        $this->_driver          = $driver;
        $this->driver_options   += $driver_options;
        $this->prepared        = & $prepared;
        $this->_params_info      = & $params_info;
    }

    final public function _set_result($result, $result_name = '')
    {
        if ($this->_result !== null) {
            return false;
        }
        $this->_result      = $result;
        $this->_result_name = $result_name;
    }

    protected function _set_error($code, $message, $state = 'HY000', $mode = PDO::ERRMODE_SILENT, $func = '')
    {
        if ($func && strpos($func, '::') === false) {
            $func = 'PDOStatement::'.$func;
        }

        $this->_driver->set_error($code, $message, $state, $mode, $func, $this->last_error);
    }

    abstract protected function _set_stmt_error($state = null, $mode = PDO::ERRMODE_SILENT, $func = '');
    abstract protected function _execute();
    abstract protected function _fetch_row();
    abstract protected function _field_name($field);
    abstract protected function _table_name($field);

    protected function _build_query()
    {
        $params             = & $this->_bound_params;
        $params_cnt         = count($params);
        $params_info_cnt    = count($this->_params_info);

        if ($params_info_cnt && !$params_cnt) {
            $this->_set_error(0, 'Invalid parameter number: no parameters were bound', 'HY093', PDO::ERRMODE_WARNING, 'execute');

            return false;
        }

        if ($params_info_cnt != $params_cnt) {
            $this->_set_error(0, 'Invalid parameter number: number of bound variables does not match number of tokens', 'HY093', PDO::ERRMODE_WARNING, 'execute');

            return false;
        }

        $prepared   = & $this->prepared;
        $driver     = & $this->_driver;

        foreach ($params as $k => &$v) {
            $param = & $v[0];
            $type = $v[1];

            if ($type == PDO::PARAM_LOB && is_resource($param)) {
                $buffer = '';
                while (!feof($param)) {
                    $buffer .= fread($param, 8192);
                }

                $prepared[$k] = $driver->quote($buffer, PDO::PARAM_STR);
            } else {
                // get param type
                if ($type == -1) {
                    if (is_int($param) || is_float($param)) {
                        $type = PDO::PARAM_INT;
                    } elseif (is_bool($param)) {
                        $type = PDO::PARAM_BOOL;
                    } elseif (is_null($param)) {
                        $type = PDO::PARAM_NULL;
                    } else {
                        $type = PDO::PARAM_STR;
                    }
                }

                $prepared[$k] = $driver->quote($param, $type);
            }
        }

        return implode(' ', $prepared);
    }

    protected function _fetch_lob(&$p, &$col)
    {
        $p = tmpfile();
        if ($p) {
            fwrite($p, $col);
            rewind($p);
        }
    }

    private function make_assoc(&$row)
    {
        if (!$this->columns_meta) {
            $this->fetch_columns_meta();
        }

        return array_combine($this->columns_meta[1], $row);
    }

    private function make_both(&$row)
    {
        if (!$this->columns_meta) {
            $this->fetch_columns_meta();
        }

        $fields = & $this->columns_meta[1];
        $result = array();

        foreach ($row as $k => &$v) {
            $result[$k] = $v;
            $result[$fields[$k]] = $v;
        }

        return $result;
    }

    private function make_named(&$row)
    {
        if (!$this->columns_meta) {
            $this->fetch_columns_meta();
        }

        $fields = & $this->columns_meta[1];

        $result = array();
        foreach ($row as $k => &$v) {
            $fname = & $fields[$k];
            if (!isset($result[$fname])) {
                $result[$fname] = &$v;
            } elseif (!is_array($result[$fname])) {
                $result[$fname] = array($result[$fname], &$v);
            } else {
                $result[$fname][] = &$v;
            }
        }

        return $result;
    }

    private function map_obj_props($obj, &$row, $offset = 0)
    {
        if (!$this->columns_meta) {
            $this->fetch_columns_meta();
        }

        $fields = & $this->columns_meta[0];

        foreach ($row as $k => &$v) {
            // do not assign =& $v
            $obj->$fields[$k + $offset] = $v;
        }

        return $obj;
    }

    private function bind_columns(&$row)
    {
        if (!$this->columns_meta) {
            $this->fetch_columns_meta();
        }

        $fields = array_flip($this->columns_meta[0]);

        foreach ($this->bound_columns as $k => &$v) {
            if (isset($row[$k])) {
                $col = & $row[$k];
            } elseif (isset($fields[$k])) {
                $col = & $row[$fields[$k]];
            } else {
                continue;
            }

            $p = & $v[0];
            switch ($v[1]) {
                case PDO::PARAM_LOB:
                    $this->_fetch_lob($p, $col);
                break;

                default:
                    $p = $col;
                break;
            }
        }
    }

    private function fetch_columns_meta()
    {
        $opt            = & $this->driver_options;
        $case           = $opt[PDO::ATTR_CASE];
        $table_names    = $opt[PDO::ATTR_FETCH_TABLE_NAMES];
        $catalog_names  = $opt[PDO::ATTR_FETCH_CATALOG_NAMES];

        $x      = 0;
        $count  = $this->columnCount();

        $result = array(array(), array());
        $names  = & $result[0];
        $tables = & $result[1];

        while ($x < $count) {
            $name = & $names[$x];
            $table = & $tables[$x];

            $name = $this->_field_name($x);

            switch ($case) {
                case PDO::CASE_LOWER:
                    $name = strtolower($name);
                break;

                case PDO::CASE_UPPER:
                    $name = strtoupper($name);
                break;
            }

            if ($table_names && ($table_name = $this->_table_name($x))) {
                $table = $table_name.'.'.$name;
            } else {
                $table = $name;
            }

            $x++;
        }

        $this->columns_meta = & $result;
    }
}

// set the old error reporting
if ($phppdo_error_reporting !== null) {
    error_reporting($phppdo_error_reporting);
}
