<?php

/** Filtered table representation
 * @method NotORM_Result and (mixed $condition, mixed $parameters = array()) Add AND condition
 * @method NotORM_Result or (mixed $condition, mixed $parameters = array()) Add OR condition
 * @property string  sequence
 */
class NotORM_Result extends NotORM_Abstract implements Iterator, ArrayAccess, Countable, JsonSerializable{
    protected $single;
    protected $select             = array(), $conditions = array(), $where = array(), $parameters = array(), $order = array(), $limit = null, $offset = null, $group = "", $having = "", $lock = null;
    protected $union              = array(), $unionOrder = array(), $unionLimit = null, $unionOffset = null;
    protected $data, $referencing = array(), $aggregation = array(), $accessed, $access, $keys = array();

    public static $queryTimes = 0;

    /** Create table result
     *
     * @param string $table
     * @param NotORM $notORM
     * @param bool $single row
     *
     * @access protected must be public because it is called from NotORM
     */
    function __construct($table, NotORM $notORM, $single = false){
        $this->table   = $table;
        $this->notORM  = $notORM;
        $this->single  = $single;
        $this->primary = $notORM->structure->getPrimary($table);
    }

    /** Save data to cache and empty result
     */
    function __destruct(){
        if($this->notORM->cache && !$this->select && isset($this->rows)){
            $access = $this->access;
            if(is_array($access)){
                $access = array_filter($access);
            }
            $this->notORM->cache->save("$this->table;" . implode(",", $this->conditions), $access);
        }
        $this->rows = null;
        unset($this->data);
    }

    protected function limitString($limit, $offset = null){
        $return = "";
        if(isset($limit) && $this->notORM->driver != "oci" && $this->notORM->driver != "dblib" && $this->notORM->driver != "mssql" && $this->notORM->driver != "sqlsrv"){
            $return .= " LIMIT $limit";
            if($offset){
                //$return .= " OFFSET $offset";
                $return .= ",$offset";      //@dogstar 2014-10-24
            }
        }
        return $return;
    }

    protected function removeExtraDots($expression){
        return preg_replace('~(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])~i', '\\1.\\2', $expression); // rewrite tab1.tab2.col
    }

    protected function whereString(){
        $return = "";
        if($this->group){
            $return .= " GROUP BY $this->group";
        }
        if($this->having){
            $return .= " HAVING $this->having";
        }
        if($this->order){
            $return .= " ORDER BY " . implode(", ", $this->order);
        }
        $return = $this->removeExtraDots($return);

        $where = $this->where;
        if(isset($this->limit) && $this->notORM->driver == "oci"){
            $where[] = ($where ? " AND " : "") . "(" . ($this->offset ? "rownum > $this->offset AND "
                    : "") . "rownum <= " . ($this->limit + $this->offset) . ")"; //! rownum > doesn't work - requires subselect (see adminer/drivers/oracle.inc.php)
        }
        if($where){
            $return = " WHERE " . implode($where) . $return;
        }

        $return .= $this->limitString($this->limit, $this->offset);
        if(isset($this->lock)){
            $return .= ($this->lock ? " FOR UPDATE" : " LOCK IN SHARE MODE");
        }
        return $return;
    }

    /**
     * @param      $limit
     *
     * @return string
     */
    protected function topString( $limit){
        if(isset($limit) && ($this->notORM->driver == "dblib" || $this->notORM->driver == "mssql" || $this->notORM->driver == "sqlsrv")){
            return " TOP ($this->limit)"; //! offset is not supported
        }
        return "";
    }

    protected function createJoins($val){
        $return = array();
        preg_match_all('~\\b([a-z_][a-z0-9_.:]*[.:])[a-z_*]~i', $val, $matches);
        foreach($matches[1] as $names){
            $parent = $this->table;
            if($names != "$parent."){ // case-sensitive
                preg_match_all('~\\b([a-z_][a-z0-9_]*)([.:])~i', $names, $matches, PREG_SET_ORDER);
                foreach($matches as $match){
                    list(, $name, $delimiter) = $match;
                    $table         = $this->notORM->structure->getReferencedTable($name, $parent);
                    $column        = ($delimiter == ':' ? $this->notORM->structure->getPrimary($parent)
                        : $this->notORM->structure->getReferencedColumn($name, $parent));
                    $primary       = ($delimiter == ':' ? $this->notORM->structure->getReferencedColumn($parent, $table)
                        : $this->notORM->structure->getPrimary($table));
                    $return[$name] = " LEFT JOIN $table" . ($table != $name ? " AS $name"
                            : "") . " ON $parent.$column = $name.$primary"; // should use alias if the table is used on more places
                    $parent        = $name;
                }
            }
        }
        return $return;
    }

    /** Get SQL query
     * @return string
     */
    function __toString(){
        $return = "SELECT" . $this->topString($this->limit) . " ";
        $join   = $this->createJoins(implode(",", $this->conditions) . "," . implode(",", $this->select) . ",$this->group,$this->having," . implode(",", $this->order));
        if(!isset($this->rows) && $this->notORM->cache && !is_string($this->accessed)){
            $this->accessed = $this->notORM->cache->load("$this->table;" . implode(",", $this->conditions));
            $this->access   = $this->accessed;
        }
        if($this->select){
            $return .= $this->removeExtraDots(implode(", ", $this->select));
        }elseif($this->accessed){
            $return .= ($join ? "$this->table." : "") . implode(", " . ($join ? "$this->table."
                        : ""), array_keys($this->accessed));
        }else{
            $return .= ($join ? "$this->table." : "") . "*";
        }
        $return .= " FROM $this->table" . implode($join) . $this->whereString();
        if($this->union){
            $return = ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $return
                    : "($return)") . implode($this->union);
            if($this->unionOrder){
                $return .= " ORDER BY " . implode(", ", $this->unionOrder);
            }
            $return .= $this->limitString($this->unionLimit, $this->unionOffset);
            $top = $this->topString($this->unionLimit);
            if($top){
                $return = "SELECT$top * FROM ($return) t";
            }
        }
        return $return;
    }

    /**
     * 放开限制 protected -> public @scott 反馈
     *
     * @param $query        string
     * @param $parameters   array
     *
     * @return bool|PDOStatement
     */
    public function query($query, $parameters){
        $debugTrace = array();

        self::$queryTimes++;

        /**
         * 修正当参数过多时的SQLSTATE[HY093] @dogstar 2014-11-18
         */
        $parameters = array_map(array($this, 'formatValue'), $parameters);
        foreach($parameters as $key => $val){
            if(substr($key, 0, 1) == ':' && stripos($query, $key) === false){
                unset($parameters[$key]);
            }
        }

        if($this->notORM->debug){
            $debugTrace['startTime'] = microtime(true);

            if(!is_callable($this->notORM->debug)){
                $debug = "$query;";
                if($parameters){
                    $debug .= " -- " . implode(", ", array_map(array($this, 'quote'), $parameters));
                }
                $pattern = '(^' . preg_quote(dirname(__FILE__)) . '(\\.php$|[/\\\\]))'; // can be static
                foreach(debug_backtrace() as $backtrace){
                    if(isset($backtrace["file"]) && !preg_match($pattern, $backtrace["file"])){ // stop on first file outside NotORM source codes
                        break;
                    }
                }
                /** @var array $backtrace */
                //error_log("{$backtrace['file']}:{$backtrace['line']}:$debug\n", 0);
                //if ($this->notORM->debug) echo "$debug<br />\n";    //@dogstar 2014-10-31

                $debugTrace['sql'] = $debug;
            }elseif(call_user_func($this->notORM->debug, $query, $parameters) === false){
                return false;
            }
        }

        $return = $this->notORM->connection->prepare($query);

        //对于替换参数进行处理   @喵了个咪 2015-12-25
        $sum = 1;
        foreach($parameters as $key => $val){
            if(strstr($key, ':')){
                if(is_int($val)){
                    $return->bindValue($key, $val, PDO::PARAM_INT);
                }elseif(is_bool($val)){
                    $return->bindValue($key, $val, PDO::PARAM_BOOL);
                }elseif(is_null($val)){
                    $return->bindValue($key, $val, PDO::PARAM_NULL);
                }else{
                    $return->bindValue($key, $val);
                }
            }else{
                if(is_int($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_INT);
                }elseif(is_bool($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_BOOL);
                }elseif(is_null($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_NULL);
                }else{
                    $return->bindValue($sum, $val);
                }
                $sum++;
            }
        }

        $errorMessage = null;
        if(!$return || !$return->execute()){
            // fixed non-object bug @dogstar 20180326
            if ($return) {
                $errorInfo = $return->errorInfo();
                $errorMessage = isset($errorInfo[2]) ? $errorInfo[2] : $errorMessage;
            } else {
                $errorMessage = 'the database server cannot successfully prepare the statement';
            }

            $return = false;
        }

        if($this->notORM->debug){
            $debugTrace['endTime'] = microtime(true);

            $sqlInfo = sprintf("[%s - %sms]%s", 
                self::$queryTimes, 
                round(($debugTrace['endTime'] - $debugTrace['startTime']) * 1000, 2), 
                $debugTrace['sql']
            );

            if ($this->notORM->debugTimer && is_callable($this->notORM->debugTimer)) {
                call_user_func_array($this->notORM->debugTimer, array($sqlInfo));
            }
        }

        //显式抛出异常，以让开发同学尽早发现SQL语法问题 @dogstar 20150426
        if($return === false && $errorMessage !== null){
            throw new PDOException($errorMessage);
        }

        return $return;
    }

    protected function formatValue($val){
        if($val instanceof DateTime){
            return $val->format("Y-m-d H:i:s"); //! may be driver specific
        }
        return $val;
    }

    protected function quote($val){
        if(!isset($val)){
            return "NULL";
        }
        if(is_array($val)){ // (a, b) IN ((1, 2), (3, 4))
            return "(" . implode(", ", array_map(array($this, 'quote'), $val)) . ")";
        }
        $val = $this->formatValue($val);
        if(is_float($val)){
            return sprintf("%F", $val); // otherwise depends on setlocale()
        }
        if($val === false){
            return "0";
        }
        if(is_int($val) || $val instanceof NotORM_Literal){ // number or SQL code - for example "NOW()"
            return (string) $val;
        }
        return $this->notORM->connection->quote($val);
    }

    /** Shortcut for call_user_func_array(array($this, 'insert'), $rows)
     *
     * @param array
     *
     * @return int number of affected rows or false in case of an error
     */
    function insert_multi(array $rows){
        if($this->notORM->freeze){
            return false;
        }
        if(!$rows){
            return 0;
        }
        $data       = reset($rows);
        $parameters = array();
        if($data instanceof NotORM_Result){
            $parameters = $data->parameters; //! other parameters
            $data       = (string) $data;
        }elseif($data instanceof Traversable){
            $data = iterator_to_array($data);
        }
        $insert = $data;
        if(is_array($data)){
            $values = array();
            foreach($rows as $value){
                if($value instanceof Traversable){
                    $value = iterator_to_array($value);
                }
                $values[] = $this->quote($value);
                foreach($value as $val){
                    if($val instanceof NotORM_Literal && $val->parameters){
                        $parameters = array_merge($parameters, $val->parameters);
                    }
                }
            }

            $quoteChar = $this->getQuoteChar();

            //! driver specific extended insert
            $insert = ($data || $this->notORM->driver == "mysql")
                ? "({$quoteChar}" . implode("{$quoteChar}, {$quoteChar}", array_keys($data)) . "{$quoteChar}) VALUES " . implode(", ", $values) 
                : "DEFAULT VALUES";
        }
        // requires empty $this->parameters
        $return = $this->query("INSERT INTO $this->table $insert", $parameters);
        if(!$return){
            return false;
        }
        $this->rows = null;
        return $return->rowCount();
    }

    /** Insert row in a table
     *
     * @param mixed array($column => $value)|Traversable for single row insert or NotORM_Result|string for INSERT ... SELECT
     * @param ... used for extended insert
     *
     * @return mixed inserted NotORM_Row or false in case of an error or number of affected rows for INSERT ... SELECT
     */
    function insert($data){
        $rows   = func_get_args();
        $return = $this->insert_multi($rows);
        if(!$return){
            return false;
        }
        if(!is_array($data)){
            return $return;
        }

        // #56 postgresql无法获取新增数据的主键ID @ clov4r-连友 201608
        if ($this->notORM->driver == "pgsql") {
            if (!isset($data[$this->primary])) {
                //获取序列名称
                $pgss = $this->query("SELECT pg_get_serial_sequence('" . $this->table . "', '" . $this->primary . "') pgss", $this->parameters)->fetch();
                if (isset($pgss['pgss'])) {
                    $rs                   = $this->query("select last_value id from " . $pgss['pgss'], $this->parameters)->fetch();
                    $data[$this->primary] = $rs['id'];
                    $this->sequence       = $rs['id'];
                }
            }
        } else {
            if (!isset($data[$this->primary]) && ($id = $this->notORM->connection->lastInsertId($this->notORM->structure->getSequence($this->table)))) {
                $data[$this->primary] = $id;
            }
        }
        return new $this->notORM->rowClass($data, $this);
    }

    /** Update all rows in result set
     *
     * @param array ($column => $value)
     *
     * @return int number of affected rows or false in case of an error
     */
    function update(array $data){
        if($this->notORM->freeze){
            return false;
        }
        if(!$data){
            return 0;
        }
        $values     = array();
        $parameters = array();
        $quoteChar  = $this->getQuoteChar();
        foreach($data as $key => $val){
            // doesn't use binding because $this->parameters can be filled by ? or :name
            $values[] = "{$quoteChar}{$key}{$quoteChar} = " . $this->quote($val);
            if($val instanceof NotORM_Literal && $val->parameters){
                $parameters = array_merge($parameters, $val->parameters);
            }
        }
        if($this->parameters){
            $parameters = array_merge($parameters, $this->parameters);
        }
        // joins in UPDATE are supported only in MySQL
        $return = $this->query("UPDATE" . $this->topString($this->limit) . " $this->table SET " . implode(", ", $values) . $this->whereString(), $parameters);
        if(!$return){
            return false;
        }
        return $return->rowCount();
    }

    /**
     * 获取数据库字段的引号
     * @author dogstar 20170617
     */
    protected function getQuoteChar() {
        $allowDBs = array('mysql');
        return in_array($this->notORM->driver, $allowDBs) ? '`' : '';
    }

    /** Insert row or update if it already exists
     *
     * @param array ($column => $value)
     * @param array ($column => $value)
     * @param array ($column => $value), empty array means use $insert
     *
     * @return int number of affected rows or false in case of an error
     */
    function insert_update(array $unique, array $insert, array $update = array()){
        if(!$update){
            $update = $insert;
        }
        $insert = $unique + $insert;
        $values = "(" . implode(", ", array_keys($insert)) . ") VALUES " . $this->quote($insert);
        //! parameters
        if($this->notORM->driver == "mysql"){
            $set = array();
            if(!$update){
                $update = $unique;
            }
            $quoteChar  = $this->getQuoteChar();

            foreach($update as $key => $val){
                $set[] = "{$quoteChar}{$key}{$quoteChar} = " . $this->quote($val);
                //! parameters
            }
            return $this->insert("$values ON DUPLICATE KEY UPDATE " . implode(", ", $set));
        }else{
            $connection = $this->notORM->connection;
            $errorMode  = $connection->getAttribute(PDO::ATTR_ERRMODE);
            $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            try{
                $return = $this->insert($values);
                $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
                return $return;
            }
            catch(PDOException $e){
                $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
                if($e->getCode() == "23000" || $e->getCode() == "23505"){ // "23000" - duplicate key, "23505" unique constraint pgsql
                    if(!$update){
                        return 0;
                    }
                    $clone  = clone $this;
                    $return = $clone->where($unique)->update($update);
                    return ($return ? $return + 1 : $return);
                }
                if($errorMode == PDO::ERRMODE_EXCEPTION){
                    throw $e;
                }elseif($errorMode == PDO::ERRMODE_WARNING){
                    trigger_error("PDOStatement::execute(): " . $e->getMessage(), E_USER_WARNING); // E_WARNING is unusable
                }
            }
        }
        return 0;
    }

    /** Get last insert ID
     * @return string number
     */
    function insert_id(){
        return $this->notORM->connection->lastInsertId();
    }

    /** Delete all rows in result set
     * @return bool|int number of affected rows or false in case of an error
     * @throws Exception
     */
    function delete(){
        if($this->notORM->freeze){
            return false;
        }
        //防止误删，禁止全表的删除
        //@dogstar - 2014-10-24
        $where = $this->whereString();
        if(empty($where)){
            throw new Exception('sorry, you can not delete the whole table --dogstar');
        }

        $return = $this->query("DELETE" . $this->topString($this->limit) . " FROM $this->table" . $where, $this->parameters);
        if(!$return){
            return false;
        }
        return $return->rowCount();
    }

    /** Add select clause, more calls appends to the end
     *
     * @param string $columns for example "column, MD5(column) AS column_md5", empty string to reset previously set columns
     * @param string ...
     *
     * @return NotORM_Result fluent interface
     */
    function select($columns){
        $this->__destruct();
        if($columns != ""){
            foreach(func_get_args() as $columns){
                $this->select[] = $columns;
            }
        }else{
            $this->select = array();
        }
        return $this;
    }

    /** Add where condition, more calls appends with AND
     *
     * @param mixed string possibly containing ? or :name; or array($condition => $parameters, ...)
     * @param mixed array accepted by PDOStatement::execute or a scalar value
     * @param mixed ...
     *
     * @return NotORM_Result fluent interface
     */
    function where($condition, $parameters = array()){
        $args = func_get_args();
        return $this->whereOperator("AND", $args);
    }

    protected function whereOperator($operator, array $args){
        $condition  = $args[0];
        $parameters = (count($args) > 1 ? $args[1] : array());
        if(is_array($condition)){ // where(array("column1" => 1, "column2 > ?" => 2))
            foreach($condition as $key => $val){
                $this->where($key, $val);
            }
            return $this;
        }
        $this->__destruct();
        $this->conditions[] = "$operator $condition";
        $condition          = $this->removeExtraDots($condition);
        if(count($args) != 2 || strpbrk($condition, "?:")){ // where("column < ? OR column > ?", array(1, 2))
            if(count($args) != 2 || !is_array($parameters)){ // where("column < ? OR column > ?", 1, 2)
                $parameters = array_slice($args, 1);
            }
            $this->parameters = array_merge($this->parameters, $parameters);
        }elseif($parameters === null){ // where("column", null)
            $condition .= " IS NULL";
        }elseif($parameters instanceof NotORM_Result){ // where("column", $db->$table())
            $clone = clone $parameters;
            if(!$clone->select){
                $clone->select($this->notORM->structure->getPrimary($clone->table));
            }
            if($this->notORM->driver != "mysql"){
                if($clone instanceof NotORM_MultiResult){
                    array_shift($clone->select);
                    $clone->single();
                }
                $condition .= " IN ($clone)";
                $this->parameters = array_merge($this->parameters, $clone->parameters);
            }else{
                $in = array();
                foreach($clone as $row){
                    $row = array_values(iterator_to_array($row));
                    if($clone instanceof NotORM_MultiResult && count($row) > 1){
                        array_shift($row);
                    }
                    if(count($row) == 1){
                        $in[] = $this->quote($row[0]);
                    }else{
                        $in[] = $this->quote($row);
                    }
                }
                if($in){
                    $condition .= " IN (" . implode(", ", $in) . ")";
                }else{
                    $condition = "($condition) IS NOT NULL AND $condition IS NULL"; // $condition = "NOT id"
                }
            }
        }elseif(!is_array($parameters)){ // where("column", "x")
            $condition .= " = " . $this->quote($parameters);
        }else{ // where("column", array(1, 2))
            $condition = $this->whereIn($condition, $parameters);
        }
        $this->where[] = (preg_match('~^\)+$~', $condition)
            ? $condition : ($this->where ? " $operator "
                : "") . "($condition)");
        return $this;
    }

    protected function whereIn($condition, $parameters){
        if(!$parameters){
            $condition = "($condition) IS NOT NULL AND $condition IS NULL";
        }elseif($this->notORM->driver != "oci"){
            $column = $condition;
            $condition .= " IN " . $this->quote($parameters);
            $nulls = array_filter($parameters, 'is_null');
            if($nulls){
                $condition = "$condition OR $column IS NULL";
            }
        }else{ // http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/expressions014.htm
            $or = array();
            for($i = 0; $i < count($parameters); $i += 1000){
                $or[] = "$condition IN " . $this->quote(array_slice($parameters, $i, 1000));
            }
            $condition = implode(" OR ", $or);
        }
        return $condition;
    }

    function __call($name, array $args){
        $operator = strtoupper($name);
        switch($operator){
            case "AND":
            case "OR":
                return $this->whereOperator($operator, $args);
        }
        trigger_error("Call to undefined method NotORM_Result::$name()", E_USER_ERROR);
        return FALSE;
    }

    /** Shortcut for where()
     *
     * @param string
     * @param mixed
     * @param mixed ...
     *
     * @return NotORM_Result fluent interface
     */
    function __invoke($where, $parameters = array()){
        $args = func_get_args();
        return $this->whereOperator("AND", $args);
    }

    /** Add order clause, more calls appends to the end
     *
     * @param string $columns for example "column1, column2 DESC", empty string to reset previous order
     * @param string ...
     *
     * @return NotORM_Result fluent interface
     */
    function order($columns){
        $this->rows = null;
        if($columns != ""){
            foreach(func_get_args() as $columns){
                if($this->union){
                    $this->unionOrder[] = $columns;
                }else{
                    $this->order[] = $columns;
                }
            }
        }elseif($this->union){
            $this->unionOrder = array();
        }else{
            $this->order = array();
        }
        return $this;
    }

    /**
     * 对查询进行limit操作
     * 请注意以下两种用法，与NotORM原来的定义有所区别
     * - limit(数量)
     * - limit(开始位置，数量)
     * Set limit clause, more calls rewrite old values
     *
     * @param int
     * @param int
     *
     * @return NotORM_Result fluent interface
     */
    function limit($limit, $offset = null){
        $this->rows = null;
        if($this->union){
            $this->unionLimit  = +$limit;
            $this->unionOffset = +$offset;
        }else{
            $this->limit  = +$limit;
            $this->offset = +$offset;
        }
        return $this;
    }

    /** Set group clause, more calls rewrite old values
     *
     * @param string
     * @param string
     *
     * @return NotORM_Result fluent interface
     */
    function group($columns, $having = ""){
        $this->__destruct();
        $this->group  = $columns;
        $this->having = $having;
        return $this;
    }

    /** Set select FOR UPDATE or LOCK IN SHARE MODE
     *
     * @param bool
     *
     * @return NotORM_Result fluent interface
     */
    function lock($exclusive = true){
        $this->lock = $exclusive;
        return $this;
    }

    /**
     * @param NotORM_Result
     * @param bool
     *
     * @return NotORM_Result fluent interface
     */
    function union(NotORM_Result $result, $all = false){
        $this->union[]    = " UNION " . ($all ? "ALL "
                : "") . ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $result : "($result)");
        $this->parameters = array_merge($this->parameters, $result->parameters);
        return $this;
    }

    /** Execute aggregation function
     *
     * @param string $function
     *
     * @return string
     */
    function aggregation($function){
        $join  = $this->createJoins(implode(",", $this->conditions) . ",$function");
        $query = "SELECT $function FROM $this->table" . implode($join);
        if($this->where){
            $query .= " WHERE " . implode($this->where);
        }

        foreach($this->query($query, $this->parameters)->fetch() as $return){
            return $return;
        }
    }

    /** Count number of rows
     *
     * @param string
     *
     * @return int
     */
    function count($column = "*"){
        return $this->aggregation("COUNT($column)");
    }

    /** Return minimum value from a column
     *
     * @param string
     *
     * @return int
     */
    function min($column){
        return $this->aggregation("MIN($column)");
    }

    /** Return maximum value from a column
     *
     * @param string
     *
     * @return int
     */
    function max($column){
        return $this->aggregation("MAX($column)");
    }

    /** Return sum of values in a column
     *
     * @param string
     *
     * @return int
     */
    function sum($column){
        return $this->aggregation("SUM($column)");
    }

    /** Execute the built query
     *
     */
    protected function execute(){
        if(!isset($this->rows)){
            $result     = false;
            $exception  = null;
            $parameters = array();
            foreach(array_merge($this->select, array(
                $this,
                $this->group,
                $this->having
            ), $this->order, $this->unionOrder) as $val){
                if(($val instanceof NotORM_Literal || $val instanceof self) && $val->parameters){
                    $parameters = array_merge($parameters, $val->parameters);
                }
            }
            try{
                $result = $this->query($this->__toString(), $parameters);
            }
            catch(PDOException $exception){
                // handled later
            }
            if(!$result){
                if(!$this->select && $this->accessed){
                    $this->accessed = '';
                    $this->access   = array();
                    $result         = $this->query($this->__toString(), $parameters);
                }elseif($exception){
                    throw $exception;
                }
            }
            $this->rows = array();
            if($result){
                $result->setFetchMode(PDO::FETCH_ASSOC);
                foreach($result as $key => $row){
                    if(isset($row[$this->primary])){
                        $key = $row[$this->primary];
                        if(!is_string($this->access)){
                            $this->access[$this->primary] = true;
                        }
                    }
                    //$this->rows[$key] = new $this->notORM->rowClass($row, $this);
                    if ($this->notORM->isKeepPrimaryKeyIndex) {
                        //@dogstar 采用主键作为下标 2015-12-30
                        $this->rows[$key] = $row;
                    } else {
                        //@dogstar 改用返回数组 2014-11-01
                        $this->rows[] = $row;
                    }
                }
            }
            $this->data = $this->rows;
        }
    }

    /** Fetch next row of result
     *
     * @param string $column name to return or an empty string for the whole row
     *
     * @return mixed string or null with $column, NotORM_Row without $column, false if there is no row
     */
    function fetch($column = ''){
        // no $this->select($column) because next calls can access different columns
        $this->execute();
        $return = current($this->data);
        next($this->data);
        if($return && $column != ''){
            return $return[$column];
        }
        return $return;
    }

    /**
     * fetchRow别名，等效于NotORM_Result::fetchRow()
     * @author: dogstar 2015-04-26
     *
     * @param string $column
     *
     * @return mixed
     */
    function fetchOne($column = ''){
        return $this->fetchRow($column);
    }

    /**
     * 只查询第一行纪录，等效于NotORM_Result::fetchOne()
     * @author: dogstar 2015-04-18
     *
     * @param string $column
     *
     * @return mixed
     */
    function fetchRow($column = ''){
        $this->limit(1)->execute();
        return $this->fetch($column);
    }

    /**
     * 返回全部的数据，等效于NotORM_Result::fetchRows()
     * @author: dogstar 2014-11-01
     */
    function fetchAll(){
        $this->execute();
        return $this->data;
    }

    /**
     * fetchAll别名，等效于NotORM_Result::fetchAll()
     * @author: dogstar 2015-04-26
     */
    function fetchRows(){
        return $this->fetchAll();
    }

    /**
     * queryRows别名，等效于NotORM_Result::queryRows($sql, $parmas)
     * @author: dogstar 2015-04-26
     *
     * @param string    $sql
     * @param array     $parmas
     *
     * @return array
     */
    function queryAll($sql, $parmas = array()){
        return $this->queryRows($sql, $parmas);
    }

    /**
     * 根据SQL查询全部数据，等效于NotORM_Result::queryAll($sql, $parmas)
     *
     * @param string    $sql
     * @param array     $parmas
     *
     * @return array
     * @author: dogstar 2014-11-01
     */
    function queryRows($sql, $parmas = array()){
        $result = $this->query($sql, $parmas);

        $rows = array();
        if($result){
            $result->setFetchMode(PDO::FETCH_ASSOC);
            foreach($result as $row){
                if(isset($row[$this->primary])){
                    if(!is_string($this->access)){
                        $this->access[$this->primary] = true;
                    }
                }
                $rows[] = $row;
            }
        }

        return $rows;
    }

    /** Fetch all rows as associative array
     *
     * @param string $key
     * @param string $value column name used for an array value or an empty string for the whole row
     *
     * @return array
     */
    function fetchPairs($key, $value = ''){
        $return = array();
        $clone  = clone $this;
        if($value != ""){
            $clone->select = array();
            $clone->select("$key, $value"); // MultiResult adds its column
        }elseif($clone->select){
            array_unshift($clone->select, $key);
        }else{
            $clone->select = array("$key, $this->table.*");
        }
        foreach($clone as $row){
            //$values = array_values(iterator_to_array($row));
            //bugfixed: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given
            //@Scott @dogstar 20151122
            /** @var Traversable|array $row */
            $values = ($row instanceof Traversable) ? array_values(iterator_to_array($row)) : array_values($row);
            if($value != "" && $clone instanceof NotORM_MultiResult){
                array_shift($values);
            }
            $return[(string) $values[0]] = ($value != "" ? $values[(array_key_exists(1, $values) ? 1 : 0)]
                : $row); // isset($values[1]) - fetchPairs("id", "id")
        }
        return $return;
    }

    protected function access($key, $delete = false){
        if($delete){
            if(is_array($this->access)){
                $this->access[$key] = false;
            }
            return false;
        }
        if(!isset($key)){
            $this->access = '';
        }elseif(!is_string($this->access)){
            $this->access[$key] = true;
        }
        if(!$this->select && $this->accessed && (!isset($key) || !isset($this->accessed[$key]))){
            $this->accessed = '';
            $this->rows     = null;
            return true;
        }
        return false;
    }

    protected function single(){
    }

    // Iterator implementation (not IteratorAggregate because $this->data can be changed during iteration)

    function rewind(){
        $this->execute();
        $this->keys = array_keys($this->data);
        reset($this->keys);
    }

    /** @return NotORM_Row */
    function current(){
        return $this->data[current($this->keys)];
    }

    /** @return string row ID */
    function key(){
        return current($this->keys);
    }

    function next(){
        next($this->keys);
    }

    function valid(){
        return current($this->keys) !== false;
    }

    // ArrayAccess implementation

    /** Test if row exists
     *
     * @param mixed $key
     *
     * @return bool
     *
     */
    function offsetExists($key){
        $row = $this->offsetGet($key);
        return isset($row);
    }

    /** Get specified row
     *
     * @param string $key row ID or array for where conditions
     *
     * @return NotORM_Row or null if there is no such row
     */
    function offsetGet($key){
        if($this->single && !isset($this->data)){
            $clone = clone $this;
            if(is_array($key)){
                $clone->where($key)->limit(1);
            }else{
                $clone->where($this->primary, $key);
            }
            $return = $clone->fetch();
            if($return){
                return $return;
            }
        }else{
            $this->execute();
            if(is_array($key)){
                foreach($this->data as $row){
                    foreach($key as $k => $v){
                        if((isset($v) && $row[$k] !== null ? $row[$k] != $v : $row[$k] !== $v)){
                            continue 2;
                        }
                    }
                    return $row;
                }
            }elseif(isset($this->data[$key])){
                return $this->data[$key];
            }
        }
        return NULL;
    }

    /** Mimic row
     *
     * @param string $key row ID
     * @param NotORM_Row $value
     *
     */
    function offsetSet($key, $value){
        $this->execute();
        $this->data[$key] = $value;
    }

    /** Remove row from result set
     *
     * @param string $key row ID
     *
     */
    function offsetUnset($key){
        $this->execute();
        unset($this->data[$key]);
    }

    // JsonSerializable implementation

    function jsonSerialize(){
        $this->execute();
        if($this->notORM->jsonAsArray){
            return array_values($this->data);
        }else{
            return $this->data;
        }
    }

}
