thomascube
2006-04-04 f3704e18d89e4065cede8509256d7fbf483b7fe6
commit | author | age
c9462d 1 <?php
S 2 // vim: set et ts=4 sw=4 fdm=marker:
3 // +----------------------------------------------------------------------+
4 // | PHP versions 4 and 5                                                 |
5 // +----------------------------------------------------------------------+
6 // | Copyright (c) 1998-2004 Manuel Lemos, Tomas V.V.Cox,                 |
7 // | Stig. S. Bakken, Lukas Smith                                         |
8 // | All rights reserved.                                                 |
9 // +----------------------------------------------------------------------+
10 // | MDB2 is a merge of PEAR DB and Metabases that provides a unified DB  |
11 // | API as well as database abstraction for PHP applications.            |
12 // | This LICENSE is in the BSD license style.                            |
13 // |                                                                      |
14 // | Redistribution and use in source and binary forms, with or without   |
15 // | modification, are permitted provided that the following conditions   |
16 // | are met:                                                             |
17 // |                                                                      |
18 // | Redistributions of source code must retain the above copyright       |
19 // | notice, this list of conditions and the following disclaimer.        |
20 // |                                                                      |
21 // | Redistributions in binary form must reproduce the above copyright    |
22 // | notice, this list of conditions and the following disclaimer in the  |
23 // | documentation and/or other materials provided with the distribution. |
24 // |                                                                      |
25 // | Neither the name of Manuel Lemos, Tomas V.V.Cox, Stig. S. Bakken,    |
26 // | Lukas Smith nor the names of his contributors may be used to endorse |
27 // | or promote products derived from this software without specific prior|
28 // | written permission.                                                  |
29 // |                                                                      |
30 // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
31 // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
32 // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
33 // | FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE      |
34 // | REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,          |
35 // | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
36 // | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS|
37 // |  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED  |
38 // | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT          |
39 // | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY|
40 // | WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE          |
41 // | POSSIBILITY OF SUCH DAMAGE.                                          |
42 // +----------------------------------------------------------------------+
43 // | Author: Paul Cooper <pgc@ucecom.com>                                 |
44 // +----------------------------------------------------------------------+
45 //
46 // $Id$
47
48 /**
49  * MDB2 PostGreSQL driver
50  *
51  * @package MDB2
52  * @category Database
53  * @author  Paul Cooper <pgc@ucecom.com>
54  */
55 class MDB2_Driver_pgsql extends MDB2_Driver_Common
56 {
57     // {{{ properties
58     var $escape_quotes = "\\";
59
60     // }}}
61     // {{{ constructor
62
63     /**
64     * Constructor
65     */
66     function __construct()
67     {
68         parent::__construct();
69
70         $this->phptype = 'pgsql';
71         $this->dbsyntax = 'pgsql';
72
73         $this->supported['sequences'] = true;
74         $this->supported['indexes'] = true;
75         $this->supported['affected_rows'] = true;
76         $this->supported['summary_functions'] = true;
77         $this->supported['order_by_text'] = true;
78         $this->supported['transactions'] = true;
79         $this->supported['current_id'] = true;
80         $this->supported['limit_queries'] = true;
81         $this->supported['LOBs'] = true;
82         $this->supported['replace'] = 'emulated';
83         $this->supported['sub_selects'] = true;
84         $this->supported['auto_increment'] = 'emulated';
85         $this->supported['primary_key'] = true;
86     }
87
88     // }}}
89     // {{{ errorInfo()
90
91     /**
92      * This method is used to collect information about an error
93      *
94      * @param integer $error
95      * @return array
96      * @access public
97      */
98     function errorInfo($error = null)
99     {
100         // Fall back to MDB2_ERROR if there was no mapping.
101         $error_code = MDB2_ERROR;
102
103         $native_msg = '';
104         if (is_resource($error)) {
105             $native_msg = @pg_result_error($error);
106         } elseif ($this->connection) {
107             $native_msg = @pg_last_error($this->connection);
108             if (!$native_msg && @pg_connection_status($this->connection) === PGSQL_CONNECTION_BAD) {
109                 $native_msg = 'Database connection has been lost.';
110                 $error_code = MDB2_ERROR_CONNECT_FAILED;
111             }
112         }
113
114         static $error_regexps;
115         if (empty($error_regexps)) {
116             $error_regexps = array(
117                 '/column .* (of relation .*)?does not exist/i'
118                     => MDB2_ERROR_NOSUCHFIELD,
119                 '/(relation|sequence|table).*does not exist|class .* not found/i'
120                     => MDB2_ERROR_NOSUCHTABLE,
121                 '/index .* does not exist/'
122                     => MDB2_ERROR_NOT_FOUND,
123                 '/relation .* already exists/i'
124                     => MDB2_ERROR_ALREADY_EXISTS,
125                 '/(divide|division) by zero$/i'
126                     => MDB2_ERROR_DIVZERO,
127                 '/pg_atoi: error in .*: can\'t parse /i'
128                     => MDB2_ERROR_INVALID_NUMBER,
129                 '/invalid input syntax for( type)? (integer|numeric)/i'
130                     => MDB2_ERROR_INVALID_NUMBER,
131                 '/value .* is out of range for type \w*int/i'
132                     => MDB2_ERROR_INVALID_NUMBER,
133                 '/integer out of range/i'
134                     => MDB2_ERROR_INVALID_NUMBER,
135                 '/value too long for type character/i'
136                     => MDB2_ERROR_INVALID,
137                 '/attribute .* not found|relation .* does not have attribute/i'
138                     => MDB2_ERROR_NOSUCHFIELD,
139                 '/column .* specified in USING clause does not exist in (left|right) table/i'
140                     => MDB2_ERROR_NOSUCHFIELD,
141                 '/parser: parse error at or near/i'
142                     => MDB2_ERROR_SYNTAX,
143                 '/syntax error at/'
144                     => MDB2_ERROR_SYNTAX,
145                 '/column reference .* is ambiguous/i'
146                     => MDB2_ERROR_SYNTAX,
147                 '/permission denied/'
148                     => MDB2_ERROR_ACCESS_VIOLATION,
149                 '/violates not-null constraint/'
150                     => MDB2_ERROR_CONSTRAINT_NOT_NULL,
151                 '/violates [\w ]+ constraint/'
152                     => MDB2_ERROR_CONSTRAINT,
153                 '/referential integrity violation/'
154                     => MDB2_ERROR_CONSTRAINT,
155                 '/more expressions than target columns/i'
156                     => MDB2_ERROR_VALUE_COUNT_ON_ROW,
157             );
158         }
159         foreach ($error_regexps as $regexp => $code) {
160             if (preg_match($regexp, $native_msg)) {
161                 $error_code = $code;
162                 break;
163             }
164         }
165
166         return array($error_code, null, $native_msg);
167     }
168
169     // }}}
170     // {{{ beginTransaction()
171
172     /**
173      * Start a transaction.
174      *
175      * @return mixed MDB2_OK on success, a MDB2 error on failure
176      * @access public
177      */
178     function beginTransaction()
179     {
180         $this->debug('starting transaction', 'beginTransaction');
181         if ($this->in_transaction) {
182             return MDB2_OK;  //nothing to do
183         }
184         if (!$this->destructor_registered && $this->opened_persistent) {
185             $this->destructor_registered = true;
186             register_shutdown_function('MDB2_closeOpenTransactions');
187         }
188         $result = $this->_doQuery('BEGIN', true);
189         if (PEAR::isError($result)) {
190             return $result;
191         }
192         $this->in_transaction = true;
193         return MDB2_OK;
194     }
195
196     // }}}
197     // {{{ commit()
198
199     /**
200      * Commit the database changes done during a transaction that is in
201      * progress.
202      *
203      * @return mixed MDB2_OK on success, a MDB2 error on failure
204      * @access public
205      */
206     function commit()
207     {
208         $this->debug('commit transaction', 'commit');
209         if (!$this->in_transaction) {
210             return $this->raiseError(MDB2_ERROR, null, null,
211                 'commit: transaction changes are being auto committed');
212         }
213         $result = $this->_doQuery('COMMIT', true);
214         if (PEAR::isError($result)) {
215             return $result;
216         }
217         $this->in_transaction = false;
218         return MDB2_OK;
219     }
220
221     // }}}
222     // {{{ rollback()
223
224     /**
225      * Cancel any database changes done during a transaction that is in
226      * progress.
227      *
228      * @return mixed MDB2_OK on success, a MDB2 error on failure
229      * @access public
230      */
231     function rollback()
232     {
233         $this->debug('rolling back transaction', 'rollback');
234         if (!$this->in_transaction) {
235             return $this->raiseError(MDB2_ERROR, null, null,
236                 'rollback: transactions can not be rolled back when changes are auto committed');
237         }
238         $result = $this->_doQuery('ROLLBACK', true);
239         if (PEAR::isError($result)) {
240             return $result;
241         }
242         $this->in_transaction = false;
243         return MDB2_OK;
244     }
245
246     // }}}
247     // {{{ _doConnect()
248
249     /**
250      * Does the grunt work of connecting to the database
251      *
252      * @return mixed connection resource on success, MDB2 Error Object on failure
253      * @access protected
254      **/
255     function _doConnect($database_name, $persistent = false)
256     {
257         if ($database_name == '') {
258             $database_name = 'template1';
259         }
260
261         $protocol = $this->dsn['protocol'] ? $this->dsn['protocol'] : 'tcp';
262
263         $params = array('');
264         if ($protocol == 'tcp') {
265             if ($this->dsn['hostspec']) {
266                 $params[0].= 'host=' . $this->dsn['hostspec'];
267             }
268             if ($this->dsn['port']) {
269                 $params[0].= ' port=' . $this->dsn['port'];
270             }
271         } elseif ($protocol == 'unix') {
272             // Allow for pg socket in non-standard locations.
273             if ($this->dsn['socket']) {
274                 $params[0].= 'host=' . $this->dsn['socket'];
275             }
276             if ($this->dsn['port']) {
277                 $params[0].= ' port=' . $this->dsn['port'];
278             }
279         }
280         if ($database_name) {
281             $params[0].= ' dbname=\'' . addslashes($database_name) . '\'';
282         }
283         if ($this->dsn['username']) {
284             $params[0].= ' user=\'' . addslashes($this->dsn['username']) . '\'';
285         }
286         if ($this->dsn['password']) {
287             $params[0].= ' password=\'' . addslashes($this->dsn['password']) . '\'';
288         }
289         if (!empty($this->dsn['options'])) {
290             $params[0].= ' options=' . $this->dsn['options'];
291         }
292         if (!empty($this->dsn['tty'])) {
293             $params[0].= ' tty=' . $this->dsn['tty'];
294         }
295         if (!empty($this->dsn['connect_timeout'])) {
296             $params[0].= ' connect_timeout=' . $this->dsn['connect_timeout'];
297         }
298         if (!empty($this->dsn['sslmode'])) {
299             $params[0].= ' sslmode=' . $this->dsn['sslmode'];
300         }
301         if (!empty($this->dsn['service'])) {
302             $params[0].= ' service=' . $this->dsn['service'];
303         }
304
305         if (isset($this->dsn['new_link'])
306             && ($this->dsn['new_link'] == 'true' || $this->dsn['new_link'] === true))
307         {
308             if (version_compare(phpversion(), '4.3.0', '>=')) {
309                 $params[] = PGSQL_CONNECT_FORCE_NEW;
310             }
311         }
312
313         $connect_function = $persistent ? 'pg_pconnect' : 'pg_connect';
314
315         putenv('PGDATESTYLE=ISO');
316
317         @ini_set('track_errors', true);
318         $php_errormsg = '';
319         $connection = @call_user_func_array($connect_function, $params);
320         @ini_restore('track_errors');
321         if (!$connection) {
322             return $this->raiseError(MDB2_ERROR_CONNECT_FAILED,
323                 null, null, strip_tags($php_errormsg));
324         }
325         return $connection;
326     }
327
328     // }}}
329     // {{{ connect()
330
331     /**
332      * Connect to the database
333      *
334      * @return true on success, MDB2 Error Object on failure
335      * @access public
336      **/
337     function connect()
338     {
339         if (is_resource($this->connection)) {
340             if (count(array_diff($this->connected_dsn, $this->dsn)) == 0
341                 && $this->connected_database_name == $this->database_name
342                 && ($this->opened_persistent == $this->options['persistent'])
343             ) {
344                 return MDB2_OK;
345             }
346             $this->disconnect(false);
347         }
348
349         if (!PEAR::loadExtension($this->phptype)) {
350             return $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null,
351                 'connect: extension '.$this->phptype.' is not compiled into PHP');
352         }
353
354         if ($this->database_name) {
355             $connection = $this->_doConnect($this->database_name, $this->options['persistent']);
356             if (PEAR::isError($connection)) {
357                 return $connection;
358             }
359             $this->connection = $connection;
360             $this->connected_dsn = $this->dsn;
361             $this->connected_database_name = $this->database_name;
362             $this->opened_persistent = $this->options['persistent'];
363             $this->dbsyntax = $this->dsn['dbsyntax'] ? $this->dsn['dbsyntax'] : $this->phptype;
364         }
365         return MDB2_OK;
366     }
367
368     // }}}
369     // {{{ disconnect()
370
371     /**
372      * Log out and disconnect from the database.
373      *
374      * @return mixed true on success, false if not connected and error
375      *                object on error
376      * @access public
377      */
378     function disconnect($force = true)
379     {
380         if (is_resource($this->connection)) {
381             if (!$this->opened_persistent || $force) {
382                 @pg_close($this->connection);
383             }
384             $this->connection = 0;
385         }
386         return MDB2_OK;
387     }
388
389     // }}}
390     // {{{ standaloneQuery()
391
392    /**
393      * execute a query as DBA
394      *
395      * @param string $query the SQL query
396      * @param mixed   $types  array that contains the types of the columns in
397      *                        the result set
398      * @return mixed MDB2_OK on success, a MDB2 error on failure
399      * @access public
400      */
401     function &standaloneQuery($query, $types = null)
402     {
403         $connection = $this->_doConnect('template1', false);
404         if (PEAR::isError($connection)) {
405             $err =& $this->raiseError(MDB2_ERROR_CONNECT_FAILED, null, null,
406                 'Cannot connect to template1');
407             return $err;
408         }
409
410         $isManip = MDB2::isManip($query);
411         $offset = $this->row_offset;
412         $limit = $this->row_limit;
413         $this->row_offset = $this->row_limit = 0;
414         $query = $this->_modifyQuery($query, $isManip, $limit, $offset);
415
416         $result = $this->_doQuery($query, $isManip, $connection, false);
417         @pg_close($connection);
418         if (PEAR::isError($result)) {
419             return $result;
420         }
421
422         if ($isManip) {
423             return $result;
424         }
425
426         $result =& $this->_wrapResult($result, $types, true, false, $limit, $offset);
427         return $result;
428     }
429
430     // }}}
431     // {{{ _doQuery()
432
433     /**
434      * Execute a query
435      * @param string $query  query
436      * @param boolean $isManip  if the query is a manipulation query
437      * @param resource $connection
438      * @param string $database_name
439      * @return result or error object
440      * @access protected
441      */
442     function _doQuery($query, $isManip = false, $connection = null, $database_name = null)
443     {
444         $this->last_query = $query;
445         $this->debug($query, 'query');
446         if ($this->options['disable_query']) {
447             if ($isManip) {
448                 return 0;
449             }
450             return null;
451         }
452
453         if (is_null($connection)) {
454             $err = $this->connect();
455             if (PEAR::isError($err)) {
456                 return $err;
457             }
458             $connection = $this->connection;
459         }
460
461         $result = @pg_query($connection, $query);
462         if (!$result) {
463             return $this->raiseError();
464         }
465
466         if ($isManip) {
467             return @pg_affected_rows($result);
468         }  elseif (!preg_match('/^\s*\(*\s*(SELECT|EXPLAIN|FETCH|SHOW)\s/si', $query)) {
469             return 0;
470         }
471         return $result;
472     }
473
474     // }}}
475     // {{{ _modifyQuery()
476
477     /**
478      * Changes a query string for various DBMS specific reasons
479      *
480      * @param string $query  query to modify
481      * @return the new (modified) query
482      * @access protected
483      */
484     function _modifyQuery($query, $isManip, $limit, $offset)
485     {
486         if ($limit > 0
487             && !preg_match('/LIMIT\s*\d(\s*(,|OFFSET)\s*\d+)?/i', $query)
488         ) {
489             $query = rtrim($query);
490             if (substr($query, -1) == ';') {
491                 $query = substr($query, 0, -1);
492             }
493             if ($isManip) {
494                 $manip = preg_replace('/^(DELETE FROM|UPDATE).*$/', '\\1', $query);
495                 $from = $match[2];
496                 $where = $match[3];
497                 $query = $manip.' '.$from.' WHERE ctid=(SELECT ctid FROM '.$from.' '.$where.' LIMIT '.$limit.')';
498             } else {
499                 $query.= " LIMIT $limit OFFSET $offset";
500             }
501         }
502         return $query;
503     }
504
505     // }}}
506     // {{{ nextID()
507
508     /**
509      * returns the next free id of a sequence
510      *
511      * @param string $seq_name name of the sequence
512      * @param boolean $ondemand when true the seqence is
513      *                          automatic created, if it
514      *                          not exists
515      * @return mixed MDB2 Error Object or id
516      * @access public
517      */
518     function nextID($seq_name, $ondemand = true)
519     {
520         $sequence_name = $this->getSequenceName($seq_name);
521         $query = "SELECT NEXTVAL('$sequence_name')";
522         $this->expectError(MDB2_ERROR_NOSUCHTABLE);
523         $result = $this->queryOne($query, 'integer');
524         $this->popExpect();
525         if (PEAR::isError($result)) {
526             if ($ondemand && $result->getCode() == MDB2_ERROR_NOSUCHTABLE) {
527                 $this->loadModule('Manager');
528                 $result = $this->manager->createSequence($seq_name, 1);
529                 if (PEAR::isError($result)) {
530                     return $this->raiseError(MDB2_ERROR, null, null,
531                         'nextID: on demand sequence could not be created');
532                 }
533                 return $this->nextId($seq_name, false);
534             }
535         }
536         return $result;
537     }
538
539     // }}}
540     // {{{ currID()
541
542     /**
543      * returns the current id of a sequence
544      *
545      * @param string $seq_name name of the sequence
546      * @return mixed MDB2 Error Object or id
547      * @access public
548      */
549     function currID($seq_name)
550     {
551         $sequence_name = $this->getSequenceName($seq_name);
552         return $this->queryOne("SELECT last_value FROM $sequence_name", 'integer');
553     }
554 }
555
556 class MDB2_Result_pgsql extends MDB2_Result_Common
557 {
558     // }}}
559     // {{{ fetchRow()
560
561     /**
562      * Fetch a row and insert the data into an existing array.
563      *
564      * @param int       $fetchmode  how the array data should be indexed
565      * @param int    $rownum    number of the row where the data can be found
566      * @return int data array on success, a MDB2 error on failure
567      * @access public
568      */
569     function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null)
570     {
571         if (!is_null($rownum)) {
572             $seek = $this->seek($rownum);
573             if (PEAR::isError($seek)) {
574                 return $seek;
575             }
576         }
577         if ($fetchmode == MDB2_FETCHMODE_DEFAULT) {
578             $fetchmode = $this->db->fetchmode;
579         }
580         if ($fetchmode & MDB2_FETCHMODE_ASSOC) {
581             $row = @pg_fetch_array($this->result, null, PGSQL_ASSOC);
582             if (is_array($row)
583                 && $this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE
584             ) {
585                 $row = array_change_key_case($row, $this->db->options['field_case']);
586             }
587         } else {
588             $row = @pg_fetch_row($this->result);
589         }
590         if (!$row) {
591             if (is_null($this->result)) {
592                 $err =& $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
593                     'fetchRow: resultset has already been freed');
594                 return $err;
595             }
596             $null = null;
597             return $null;
598         }
599         if ($this->db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL) {
600             $this->db->_fixResultArrayValues($row, MDB2_PORTABILITY_EMPTY_TO_NULL);
601         }
602         if (!empty($this->values)) {
603             $this->_assignBindColumns($row);
604         }
605         if (!empty($this->types)) {
606             $row = $this->db->datatype->convertResultRow($this->types, $row);
607         }
608         if ($fetchmode === MDB2_FETCHMODE_OBJECT) {
609             $object_class = $this->db->options['fetch_class'];
610             if ($object_class == 'stdClass') {
611                 $row = (object) $row;
612             } else {
613                 $row = &new $object_class($row);
614             }
615         }
616         ++$this->rownum;
617         return $row;
618     }
619
620     // }}}
621     // {{{ _getColumnNames()
622
623     /**
624      * Retrieve the names of columns returned by the DBMS in a query result.
625      *
626      * @return mixed                an associative array variable
627      *                              that will hold the names of columns. The
628      *                              indexes of the array are the column names
629      *                              mapped to lower case and the values are the
630      *                              respective numbers of the columns starting
631      *                              from 0. Some DBMS may not return any
632      *                              columns when the result set does not
633      *                              contain any rows.
634      *
635      *                              a MDB2 error on failure
636      * @access private
637      */
638     function _getColumnNames()
639     {
640         $columns = array();
641         $numcols = $this->numCols();
642         if (PEAR::isError($numcols)) {
643             return $numcols;
644         }
645         for ($column = 0; $column < $numcols; $column++) {
646             $column_name = @pg_field_name($this->result, $column);
647             $columns[$column_name] = $column;
648         }
649         if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) {
650             $columns = array_change_key_case($columns, $this->db->options['field_case']);
651         }
652         return $columns;
653     }
654
655     // }}}
656     // {{{ numCols()
657
658     /**
659      * Count the number of columns returned by the DBMS in a query result.
660      *
661      * @access public
662      * @return mixed integer value with the number of columns, a MDB2 error
663      *                       on failure
664      */
665     function numCols()
666     {
667         $cols = @pg_num_fields($this->result);
668         if (is_null($cols)) {
669             if (is_null($this->result)) {
670                 return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
671                     'numCols: resultset has already been freed');
672             }
673             return $this->db->raiseError();
674         }
675         return $cols;
676     }
677
678     // }}}
679     // {{{ free()
680
681     /**
682      * Free the internal resources associated with result.
683      *
684      * @return boolean true on success, false if result is invalid
685      * @access public
686      */
687     function free()
688     {
689         $free = @pg_free_result($this->result);
690         if (!$free) {
691             if (is_null($this->result)) {
692                 return MDB2_OK;
693             }
694             return $this->db->raiseError();
695         }
696         $this->result = null;
697         return MDB2_OK;
698     }
699 }
700
701 class MDB2_BufferedResult_pgsql extends MDB2_Result_pgsql
702 {
703     // {{{ seek()
704
705     /**
706     * seek to a specific row in a result set
707     *
708     * @param int    $rownum    number of the row where the data can be found
709     * @return mixed MDB2_OK on success, a MDB2 error on failure
710     * @access public
711     */
712     function seek($rownum = 0)
713     {
714         if ($this->rownum != ($rownum - 1) && !@pg_result_seek($this->result, $rownum)) {
715             if (is_null($this->result)) {
716                 return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
717                     'seek: resultset has already been freed');
718             }
719             return $this->db->raiseError(MDB2_ERROR_INVALID, null, null,
720                 'seek: tried to seek to an invalid row number ('.$rownum.')');
721         }
722         $this->rownum = $rownum - 1;
723         return MDB2_OK;
724     }
725
726     // }}}
727     // {{{ valid()
728
729     /**
730     * check if the end of the result set has been reached
731     *
732     * @return mixed true or false on sucess, a MDB2 error on failure
733     * @access public
734     */
735     function valid()
736     {
737         $numrows = $this->numRows();
738         if (PEAR::isError($numrows)) {
739             return $numrows;
740         }
741         return $this->rownum < ($numrows - 1);
742     }
743
744     // }}}
745     // {{{ numRows()
746
747     /**
748     * returns the number of rows in a result object
749     *
750     * @return mixed MDB2 Error Object or the number of rows
751     * @access public
752     */
753     function numRows()
754     {
755         $rows = @pg_num_rows($this->result);
756         if (is_null($rows)) {
757             if (is_null($this->result)) {
758                 return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
759                     'numRows: resultset has already been freed');
760             }
761             return $this->raiseError();
762         }
763         return $rows;
764     }
765 }
766
767 class MDB2_Statement_pgsql extends MDB2_Statement_Common
768 {
769
770 }
771 ?>