<?php
/**
 * PostgreSQL RDO_Storage implementation.
 *
 * $Horde: framework/RDO/RDO/Storage/pgsql.php,v 1.9.2.1 2005/10/18 11:01:23 jan Exp $
 *
 * @package RDO
 */
class RDO_Storage_pgsql extends RDO_Storage_sql {

    /**
     * Human-readable name for this Storage driver.
     *
     * @var string $name
     */
    public $name = 'PostgreSQL';

    /**
     * PostgreSQL database resource.
     *
     * @var resource $db
     */
    private $db = null;

    /**
     * PostgreSQL RDO_Storage constructor. Sets parameters and
     * connects to the database.
     *
     * @param array $params Connection and other parameters.
     */
    protected function __construct($params)
    {
        parent::__construct($params);

        $this->connect();
    }

    /**
     * Free any resources that are open.
     */
    public function __destruct()
    {
        if ($this->db) {
            pg_close($this->db);
        }
    }

    public function create(RDO_Mapper $mapper, $fields)
    {
        $metadata = $mapper->describe();
        $result = @pg_insert($this->db,
                             $metadata->container,
                             $fields);
        if (!$result) {
            throw new RDO_Exception(pg_last_error());
        }

        return $this->queryOne('SELECT CURRVAL(\'' . $metadata->container . '_' . $metadata->key . '_seq\')');
    }

    /**
     * Updates a backend object.
     *
     * @param RDO_Mapper $mapper The RDO_Mapper requesting the update.
     * @param scalar $id The unique key of the object being updated.
     * @param array $fields Hash of field names/new values.
     *
     * @return boolean True if one object is updated, false if no
     * object is updated, or throw an exception if any errors occur.
     */
    public function update(RDO_Mapper $mapper, $id, $fields)
    {
        $metadata = $mapper->describe();
        $query = @pg_update($this->db,
                            $metadata->container,
                            $fields,
                            array($metadata->key => $id),
                            PGSQL_DML_STRING);
        if (!$query) {
            throw new RDO_Exception(pg_last_error());
        }

        return $this->execute($query) == 1;
    }

    /**
     * Delete one or more objects from the backend storage.
     *
     * @param RDO_Mapper $mapper The RDO_Mapper requesting the deletion.
     * @param array|RDO_Criteria $criteria Description of what to delete.
     *
     * @return integer Number of objects deleted.
     */
    public function delete(RDO_Mapper $mapper, $criteria)
    {
        if ($criteria instanceof RDO_Criteria) {
            throw new RDO_Exception('Criteria objects not yet implemented.');
        } else {
            $metadata = $mapper->describe();
            $query = @pg_delete($this->db,
                                $metadata->container,
                                $criteria,
                                PGSQL_DML_STRING);
            if (!$query) {
                throw new RDO_Exception(pg_last_error());
            }

            return $this->execute($query);
        }
    }

    /**
     */
    public function find(RDO_Mapper $mapper, $mode, $criteria)
    {
        return new RDO_Results_pgsql($this, $mapper, $mode, $criteria);
    }

    /**
     * Take a string, wrap it in single quotes, and escape any quotes
     * or other special characters in it.
     *
     * @param string $data The string to quote.
     *
     * @return string Quoted string wrapped in single quotes.
     */
    public function quoteString($data)
    {
        return "'" . pg_escape_string($data) . "'";
    }

    /**
     * Use for SELECT and anything that returns rows.
     *
     * @param string $sql A full SQL query to run.
     *
     * @return resource The PostgreSQL result object.
     */
    public function query($sql)
    {
        $result = @pg_query($this->db, $sql);
        if (!$result) {
            throw new RDO_Exception(pg_last_error() . ' ' . $sql);
        }
        return $result;
    }

    /**
     * Return a single value from a query. Useful for quickly getting
     * a value such as with a COUNT(*) query.
     *
     * @param string $sql The SQL to get one result from.
     *
     * @return mixed The first value of the first row matched by $sql.
     */
    public function queryOne($sql)
    {
        $result = @pg_query($this->db, $sql);
        if (!$result) {
            throw new RDO_Exception(pg_last_error());
        }
        return @pg_fetch_result($result, 0, 0);
    }

    /**
     * Use for INSERT, UPDATE, DELETE, and other queries that don't
     * return rows. Returns number of affected rows.
     *
     * @param string $sql The query to run.
     *
     * @return integer The number of rows affected by $sql.
     */
    public function execute($sql)
    {
        $result = @pg_query($this->db, $sql);
        if (!$result) {
            throw new RDO_Exception(pg_last_error());
        }
        return pg_affected_rows($result);
    }

    /**
     * Get metadata on the database table that $mapper's objects are
     * stored in, and set it in the passed $metadata object.
     *
     * @param RDO_Mapper $mapper The Mapper object to describe.
     * @param RDO_MetaData $metadata The MetaData instance to set field information on.
     */
    public function describe(RDO_Mapper $mapper, RDO_MetaData $metadata)
    {
        $fields = @pg_meta_data($this->db, $metadata->container);
        if (!$fields) {
            throw new RDO_Exception(pg_last_error());
        }

        $indices = array();
        foreach ($fields as $field => $vals) {
            $metadata->addField($field, $vals);
            $metadata->setFieldType($field, $vals['type']);
            $indices[$vals['num']] = $field;
        }

        $idxresult = @pg_query($this->db,
                               'SELECT i.indisunique, i.indisprimary, i.indkey FROM pg_class c, pg_index i WHERE c.oid = i.indrelid AND c.relname = \'' . $metadata->container . '\'');
        if (!$idxresult) {
            throw new RDO_Exception('Failed to get index information: ' . pg_last_error());
        }

        $idxinfo = pg_fetch_all($idxresult);
        if (!$idxinfo) {
            return;
        }

        foreach ($idxinfo as $idx) {
            if ($idx['indisprimary'] == 't') {
                $metadata->key = $indices[$idx['indkey']];
            }
        }
    }

    /**
     */
    public function begin()
    {
        return $this->query('BEGIN');
    }

    /**
     */
    public function commit()
    {
        return $this->query('COMMIT');
    }

    /**
     */
    public function rollback()
    {
        return $this->query('ROLLBACK');
    }

    /**
     * Free a PostgreSQL result resource.
     *
     * @param resource $result The result to free.
     *
     * @return boolean Success/failure.
     */
    public function free($result)
    {
        return @pg_free_result($result);
    }

    /**
     * Build a connection string and connect to the PostgreSQL server.
     */
    private function connect()
    {
        $connstr = '';

        $protocol = $this->protocol ? $this->protocol : 'tcp';
        if ($protocol == 'tcp') {
            if ($this->hostspec) {
                $connstr .= 'host=' . $this->hostspec;
            }
            if ($this->port) {
                $connstr .= ' port=' . $this->port;
            }
        } elseif ($protocol == 'unix') {
            // Allow for pg socket in non-standard locations.
            if ($this->socket) {
                $connstr .= 'host=' . $this->socket;
            }
            if ($this->port) {
                $connstr .= ' port=' . $this->port;
            }
        }

        if ($this->database) {
            $connstr .= ' dbname=\'' . addslashes($this->database) . '\'';
        }
        if ($this->username) {
            $connstr .= ' user=\'' . addslashes($this->username) . '\'';
        }
        if ($this->password) {
            $connstr .= ' password=\'' . addslashes($this->password) . '\'';
        }
        if ($this->options) {
            $connstr .= ' options=' . $this->options;
        }
        if ($this->tty) {
            $connstr .= ' tty=' . $this->tty;
        }
        if ($this->connect_timeout) {
            $connstr .= ' connect_timeout=' . $this->connect_timeout;
        }
        if ($this->sslmode) {
            $connstr .= ' sslmode=' . $this->sslmode;
        }
        if ($this->service) {
            $connstr .= ' service=' . $this->service;
        }

        $function = $this->persistent ? 'pg_pconnect' : 'pg_connect';
        $ini = ini_get('track_errors');
        $php_errormsg = '';
        if ($ini) {
            $this->db = @call_user_func($function, $connstr);
        } else {
            ini_set('track_errors', 1);
            $this->db = @call_user_func($function, $connstr);
            ini_set('track_errors', $ini);
        }

        if (!$this->db) {
            throw new RDO_Exception($php_errormsg);
        }
    }

}

/**
 * @package RDO
 */
class RDO_Results_pgsql extends RDO_Results {

    /**
     * Find the number of objects in this result set.
     *
     * @return integer Number of objects in this set.
     */
    public function count()
    {
        if (is_null($this->result)) {
            $this->rewind();
        }
        return pg_num_rows($this->result);
    }

    /**
     * Implementation of the rewind() method for Pgsql result sets.
     */
    public function rewind()
    {
        // Rewind only if never queried before or query was
        // successful.
        if (is_null($this->result) || is_resource($this->result)) {
            if ($this->result) {
                $this->storage->free($this->result);
            }
            $this->current = null;
            $this->index = null;
            $this->eof = true;

            $this->result = $this->storage->findNow($this->mapper, $this->mode, $this->criteria);
            if (!$this->result) {
                throw new RDO_Exception(pg_last_error());
            }
            $this->next();
        }
    }

    /**
     * Implementation of the next() method for Pgsql result sets.
     *
     * @return RDO|null The next RDO instance in the set or null if no
     * more results.
     */
    public function next()
    {
        if (is_null($this->result)) {
            $this->rewind();
        }

        if ($this->result) {
            $this->current = pg_fetch_assoc($this->result);
            if (!$this->current) {
                $this->eof = true;
            } else {
                $this->eof = false;

                if (is_null($this->index)) {
                    $this->index = 0;
                } else {
                    ++$this->index;
                }
            }

            $this->current = $this->mapper->map($this->current);
        }

        return $this->current;
    }

}
