thomascube
2006-05-01 6204390af16bcf50f82da61a1aefc2ad0c0adf94
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: Lorenzo Alberton <l.alberton@quipo.it>                       |
44 // +----------------------------------------------------------------------+
45 //
46 // $Id$
47
48 /**
49  * MDB2 FireBird/InterBase driver
50  *
51  * @package MDB2
52  * @category Database
53  * @author  Lorenzo Alberton <l.alberton@quipo.it>
54  */
55 class MDB2_Driver_ibase extends MDB2_Driver_Common
56 {
57     // {{{ properties
58     var $escape_quotes = "'";
59
60     var $transaction_id = 0;
61
62     var $query_parameters = array();
63     var $query_parameter_values = array();
64
65     // }}}
66     // {{{ constructor
67
68     /**
69      * Constructor
70      */
71     function __construct()
72     {
73         parent::__construct();
74
75         $this->phptype  = 'ibase';
76         $this->dbsyntax = 'ibase';
77
78         $this->supported['sequences'] = true;
79         $this->supported['indexes'] = true;
80         $this->supported['affected_rows'] = function_exists('ibase_affected_rows');
81         $this->supported['summary_functions'] = true;
82         $this->supported['order_by_text'] = true;
83         $this->supported['transactions'] = true;
84         $this->supported['current_id'] = true;
85         // maybe this needs different handling for ibase and firebird?
86         $this->supported['limit_queries'] = 'emulated';
87         $this->supported['LOBs'] = true;
88         $this->supported['replace'] = false;
89         $this->supported['sub_selects'] = true;
90         $this->supported['auto_increment'] = true;
91         $this->supported['primary_key'] = true;
92
93         $this->options['database_path'] = '';
94         $this->options['database_extension'] = '.gdb';
95         $this->options['default_text_field_length'] = 4096;
96     }
97
98     // }}}
99     // {{{ errorInfo()
100
101     /**
102      * This method is used to collect information about an error
103      *
104      * @param integer $error
105      * @return array
106      * @access public
107      */
108     function errorInfo($error = null)
109     {
110         $native_msg = @ibase_errmsg();
111
112         if (function_exists('ibase_errcode')) {
113             $native_code = @ibase_errcode();
114         } else {
115             // memo for the interbase php module hackers: we need something similar
116             // to mysql_errno() to retrieve error codes instead of this ugly hack
117             if (preg_match('/^([^0-9\-]+)([0-9\-]+)\s+(.*)$/', $native_msg, $m)) {
118                 $native_code = (int)$m[2];
119             } else {
120                 $native_code = null;
121             }
122         }
123         if (is_null($error)) {
124             $error = MDB2_ERROR;
125             if ($native_code) {
126                 // try to interpret Interbase error code (that's why we need ibase_errno()
127                 // in the interbase module to return the real error code)
128                 switch ($native_code) {
129                 case -204:
130                     if (isset($m[3]) && is_int(strpos($m[3], 'Table unknown'))) {
131                         $errno = MDB2_ERROR_NOSUCHTABLE;
132                     }
133                 break;
134                 default:
135                     static $ecode_map;
136                     if (empty($ecode_map)) {
137                         $ecode_map = array(
138                             -104 => MDB2_ERROR_SYNTAX,
139                             -150 => MDB2_ERROR_ACCESS_VIOLATION,
140                             -151 => MDB2_ERROR_ACCESS_VIOLATION,
141                             -155 => MDB2_ERROR_NOSUCHTABLE,
142                             -157 => MDB2_ERROR_NOSUCHFIELD,
143                             -158 => MDB2_ERROR_VALUE_COUNT_ON_ROW,
144                             -170 => MDB2_ERROR_MISMATCH,
145                             -171 => MDB2_ERROR_MISMATCH,
146                             -172 => MDB2_ERROR_INVALID,
147                             // -204 =>  // Covers too many errors, need to use regex on msg
148                             -205 => MDB2_ERROR_NOSUCHFIELD,
149                             -206 => MDB2_ERROR_NOSUCHFIELD,
150                             -208 => MDB2_ERROR_INVALID,
151                             -219 => MDB2_ERROR_NOSUCHTABLE,
152                             -297 => MDB2_ERROR_CONSTRAINT,
153                             -303 => MDB2_ERROR_INVALID,
154                             -413 => MDB2_ERROR_INVALID_NUMBER,
155                             -530 => MDB2_ERROR_CONSTRAINT,
156                             -551 => MDB2_ERROR_ACCESS_VIOLATION,
157                             -552 => MDB2_ERROR_ACCESS_VIOLATION,
158                             // -607 =>  // Covers too many errors, need to use regex on msg
159                             -625 => MDB2_ERROR_CONSTRAINT_NOT_NULL,
160                             -803 => MDB2_ERROR_CONSTRAINT,
161                             -804 => MDB2_ERROR_VALUE_COUNT_ON_ROW,
162                             -904 => MDB2_ERROR_CONNECT_FAILED,
163                             -922 => MDB2_ERROR_NOSUCHDB,
164                             -923 => MDB2_ERROR_CONNECT_FAILED,
165                             -924 => MDB2_ERROR_CONNECT_FAILED
166                         );
167                     }
168                     if (isset($ecode_map[$native_code])) {
169                         $error = $ecode_map[$native_code];
170                     }
171                     break;
172                 }
173             } else {
174                 static $error_regexps;
175                 if (!isset($error_regexps)) {
176                     $error_regexps = array(
177                         '/generator .* is not defined/'
178                             => MDB2_ERROR_SYNTAX,  // for compat. w ibase_errcode()
179                         '/table.*(not exist|not found|unknown)/i'
180                             => MDB2_ERROR_NOSUCHTABLE,
181                         '/table .* already exists/i'
182                             => MDB2_ERROR_ALREADY_EXISTS,
183                         '/unsuccessful metadata update .* failed attempt to store duplicate value/i'
184                             => MDB2_ERROR_ALREADY_EXISTS,
185                         '/unsuccessful metadata update .* not found/i'
186                             => MDB2_ERROR_NOT_FOUND,
187                         '/validation error for column .* value "\*\*\* null/i'
188                             => MDB2_ERROR_CONSTRAINT_NOT_NULL,
189                         '/violation of [\w ]+ constraint/i'
190                             => MDB2_ERROR_CONSTRAINT,
191                         '/conversion error from string/i'
192                             => MDB2_ERROR_INVALID_NUMBER,
193                         '/no permission for/i'
194                             => MDB2_ERROR_ACCESS_VIOLATION,
195                         '/arithmetic exception, numeric overflow, or string truncation/i'
196                             => MDB2_ERROR_INVALID,
197                     );
198                 }
199                 foreach ($error_regexps as $regexp => $code) {
200                     if (preg_match($regexp, $native_msg, $m)) {
201                         $error = $code;
202                         break;
203                     }
204                 }
205             }
206         }
207         return array($error, $native_code, $native_msg);
208     }
209
210     // }}}
211     // {{{ beginTransaction()
212
213     /**
214      * Start a transaction.
215      *
216      * @return mixed MDB2_OK on success, a MDB2 error on failure
217      * @access public
218      */
219     function beginTransaction()
220     {
221         $this->debug('starting transaction', 'beginTransaction');
222         if ($this->in_transaction) {
223             return MDB2_OK;  //nothing to do
224         }
225         if (!$this->destructor_registered && $this->opened_persistent) {
226             $this->destructor_registered = true;
227             register_shutdown_function('MDB2_closeOpenTransactions');
228         }
229         $result = ibase_trans();
230         if (!$result) {
231             return $this->raiseError(MDB2_ERROR, null, null,
232                 'beginTransaction: could not start a transaction');
233         }
234         $this->transaction_id = $result;
235         $this->in_transaction = true;
236         return MDB2_OK;
237     }
238
239     // }}}
240     // {{{ commit()
241
242     /**
243      * Commit the database changes done during a transaction that is in
244      * progress.
245      *
246      * @return mixed MDB2_OK on success, a MDB2 error on failure
247      * @access public
248      */
249     function commit()
250     {
251         $this->debug('commit transaction', 'commit');
252         if (!$this->in_transaction) {
253             return $this->raiseError(MDB2_ERROR, null, null,
254                 'commit: transaction changes are being auto committed');
255         }
256         if (!ibase_commit($this->transaction_id)) {
257             return $this->raiseError(MDB2_ERROR, null, null,
258                 'commit: could not commit a transaction');
259         }
260         $this->in_transaction = false;
261         //$this->transaction_id = 0;
262         return MDB2_OK;
263     }
264
265     // }}}
266     // {{{ rollback()
267
268     /**
269      * Cancel any database changes done during a transaction that is in
270      * progress.
271      *
272      * @return mixed MDB2_OK on success, a MDB2 error on failure
273      * @access public
274      */
275     function rollback()
276     {
277         $this->debug('rolling back transaction', 'rollback');
278         if (!$this->in_transaction) {
279             return $this->raiseError(MDB2_ERROR, null, null,
280                 'rollback: transactions can not be rolled back when changes are auto committed');
281         }
282         if ($this->transaction_id && !ibase_rollback($this->transaction_id)) {
283             return $this->raiseError(MDB2_ERROR, null, null,
284                 'rollback: Could not rollback a pending transaction: '.ibase_errmsg());
285         }
286         $this->in_transaction = false;
287         $this->transaction_id = 0;
288         return MDB2_OK;
289     }
290
291     // }}}
292     // {{{ getDatabaseFile()
293
294     /**
295      * Builds the string with path+dbname+extension
296      *
297      * @return string full database path+file
298      * @access protected
299      */
300     function _getDatabaseFile($database_name)
301     {
302         if ($database_name == '') {
303             return $database_name;
304         }
305         return $this->options['database_path'].$database_name.$this->options['database_extension'];
306     }
307
308     // }}}
309     // {{{ _doConnect()
310
311     /**
312      * Does the grunt work of connecting to the database
313      *
314      * @return mixed connection resource on success, MDB2 Error Object on failure
315      * @access protected
316      */
317     function _doConnect($database_name, $persistent = false)
318     {
319         $user    = $this->dsn['username'];
320         $pw      = $this->dsn['password'];
321         $dbhost  = $this->dsn['hostspec'] ?
322             ($this->dsn['hostspec'].':'.$database_name) : $database_name;
323
324         $params = array();
325         $params[] = $dbhost;
326         $params[] = !empty($user) ? $user : null;
327         $params[] = !empty($pw) ? $pw : null;
328         $params[] = isset($this->dsn['charset']) ? $this->dsn['charset'] : null;
329         $params[] = isset($this->dsn['buffers']) ? $this->dsn['buffers'] : null;
330         $params[] = isset($this->dsn['dialect']) ? $this->dsn['dialect'] : null;
331         $params[] = isset($this->dsn['role'])    ? $this->dsn['role'] : null;
332
333         $connect_function = $persistent ? 'ibase_pconnect' : 'ibase_connect';
334
335         $connection = @call_user_func_array($connect_function, $params);
336         if ($connection <= 0) {
337             return $this->raiseError(MDB2_ERROR_CONNECT_FAILED);
338         }
339         if (function_exists('ibase_timefmt')) {
340             @ibase_timefmt("%Y-%m-%d %H:%M:%S", IBASE_TIMESTAMP);
341             @ibase_timefmt("%Y-%m-%d", IBASE_DATE);
342         } else {
343             @ini_set("ibase.timestampformat", "%Y-%m-%d %H:%M:%S");
344             //@ini_set("ibase.timeformat", "%H:%M:%S");
345             @ini_set("ibase.dateformat", "%Y-%m-%d");
346         }
347
348         return $connection;
349     }
350
351     // }}}
352     // {{{ connect()
353
354     /**
355      * Connect to the database
356      *
357      * @return true on success, MDB2 Error Object on failure
358      * @access public
359      */
360     function connect()
361     {
362         $database_file = $this->_getDatabaseFile($this->database_name);
363         if (is_resource($this->connection)) {
364             if (count(array_diff($this->connected_dsn, $this->dsn)) == 0
365                 && $this->connected_database_name == $database_file
366                 && $this->opened_persistent == $this->options['persistent']
367             ) {
368                 return MDB2_OK;
369             }
370             $this->disconnect(false);
371         }
372
373         if (!PEAR::loadExtension('interbase')) {
374             return $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null,
375                 'connect: extension '.$this->phptype.' is not compiled into PHP');
376         }
377
378         if (!empty($this->database_name)) {
379             $connection = $this->_doConnect($database_file, $this->options['persistent']);
380             if (PEAR::isError($connection)) {
381                 return $connection;
382             }
383             $this->connection =& $connection;
384             $this->connected_dsn = $this->dsn;
385             $this->connected_database_name = $database_file;
386             $this->opened_persistent = $this->options['persistent'];
387             $this->dbsyntax = $this->dsn['dbsyntax'] ? $this->dsn['dbsyntax'] : $this->phptype;
388         }
389         return MDB2_OK;
390     }
391
392     // }}}
393     // {{{ disconnect()
394
395     /**
396      * Log out and disconnect from the database.
397      *
398      * @return mixed true on success, false if not connected and error
399      *                object on error
400      * @access public
401      */
402     function disconnect($force = true)
403     {
404         if (is_resource($this->connection)) {
405             if (!$this->opened_persistent || $force) {
406                 @ibase_close($this->connection);
407             }
408             $this->connection = 0;
409         }
410         return MDB2_OK;
411     }
412
413     // }}}
414     // {{{ _doQuery()
415
416     /**
417      * Execute a query
418      * @param string $query  query
419      * @param boolean $isManip  if the query is a manipulation query
420      * @param resource $connection
421      * @param string $database_name
422      * @return result or error object
423      * @access protected
424      */
425     function _doQuery($query, $isManip = false, $connection = null, $database_name = null)
426     {
427         $this->last_query = $query;
428         $this->debug($query, 'query');
429         if ($this->getOption('disable_query')) {
430             if ($isManip) {
431                 return 0;
432             }
433             return null;
434         }
435
436         if (is_null($connection)) {
437             if ($this->in_transaction) {
438                 $connection = $this->transaction_id;
439             } else {
440                 $err = $this->connect();
441                 if (PEAR::isError($err)) {
442                     return $err;
443                 }
444                 $connection = $this->connection;
445             }
446         }
447         $result = ibase_query($connection, $query);
448
449         if ($result === false) {
450             return $this->raiseError();
451         }
452
453         if ($isManip) {
454             //return $result;
455             return (function_exists('ibase_affected_rows') ? ibase_affected_rows($connection) : 0);
456         }
457         return $result;
458     }
459
460     // }}}
461     // {{{ _modifyQuery()
462
463     /**
464      * Changes a query string for various DBMS specific reasons
465      *
466      * @param string $query  query to modify
467      * @return the new (modified) query
468      * @access protected
469      */
470     function _modifyQuery($query, $isManip, $limit, $offset)
471     {
472         if ($limit > 0 && $this->dsn['dbsyntax'] == 'firebird') {
473             $query = preg_replace('/^([\s(])*SELECT(?!\s*FIRST\s*\d+)/i',
474                 "SELECT FIRST $limit SKIP $offset", $query);
475         }
476         return $query;
477     }
478
479     // }}}
480     // {{{ prepare()
481
482     /**
483      * Prepares a query for multiple execution with execute().
484      * With some database backends, this is emulated.
485      * prepare() requires a generic query as string like
486      * 'INSERT INTO numbers VALUES(?,?)' or
487      * 'INSERT INTO numbers VALUES(:foo,:bar)'.
488      * The ? and :[a-zA-Z] and  are placeholders which can be set using
489      * bindParam() and the query can be send off using the execute() method.
490      *
491      * @param string $query the query to prepare
492      * @param mixed   $types  array that contains the types of the placeholders
493      * @param mixed   $result_types  array that contains the types of the columns in
494      *                        the result set
495      * @return mixed resource handle for the prepared query on success, a MDB2
496      *        error on failure
497      * @access public
498      * @see bindParam, execute
499      */
500     function &prepare($query, $types = null, $result_types = null)
501     {
502         $this->debug($query, 'prepare');
503         $placeholder_type_guess = $placeholder_type = null;
504         $question = '?';
505         $colon = ':';
506         $position = 0;
507         while ($position < strlen($query)) {
508             $q_position = strpos($query, $question, $position);
509             $c_position = strpos($query, $colon, $position);
510             if ($q_position && $c_position) {
511                 $p_position = min($q_position, $c_position);
512             } elseif ($q_position) {
513                 $p_position = $q_position;
514             } elseif ($c_position) {
515                 $p_position = $c_position;
516             } else {
517                 break;
518             }
519             if (is_null($placeholder_type)) {
520                 $placeholder_type_guess = $query[$p_position];
521             }
522             if (is_int($quote = strpos($query, "'", $position)) && $quote < $p_position) {
523                 if (!is_int($end_quote = strpos($query, "'", $quote + 1))) {
524                     $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null,
525                         'prepare: query with an unterminated text string specified');
526                     return $err;
527                 }
528                 switch ($this->escape_quotes) {
529                 case '':
530                 case "'":
531                     $position = $end_quote + 1;
532                     break;
533                 default:
534                     if ($end_quote == $quote + 1) {
535                         $position = $end_quote + 1;
536                     } else {
537                         if ($query[$end_quote-1] == $this->escape_quotes) {
538                             $position = $end_quote;
539                         } else {
540                             $position = $end_quote + 1;
541                         }
542                     }
543                     break;
544                 }
545             } elseif ($query[$position] == $placeholder_type_guess) {
546                 if ($placeholder_type_guess == '?') {
547                     break;
548                 }
549                 if (is_null($placeholder_type)) {
550                     $placeholder_type = $query[$p_position];
551                     $question = $colon = $placeholder_type;
552                 }
553                 $name = preg_replace('/^.{'.($position+1).'}([a-z0-9_]+).*$/si', '\\1', $query);
554                 if ($name === '') {
555                     $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null,
556                         'prepare: named parameter with an empty name');
557                     return $err;
558                 }
559                 $query = substr_replace($query, '?', $position, strlen($name)+1);
560                 $position = $p_position + 1;
561             } else {
562                 $position = $p_position;
563             }
564         }
565         $connection = ($this->in_transaction ? $this->transaction_id : $this->connection);
566         $statement = ibase_prepare($connection, $query);
567
568         $class_name = 'MDB2_Statement_'.$this->phptype;
569         $obj =& new $class_name($this, $statement, $query, $types, $result_types, $this->row_limit, $this->row_offset);
570         return $obj;
571     }
572
573     // }}}
574     // {{{ nextID()
575
576     /**
577      * returns the next free id of a sequence
578      *
579      * @param string $seq_name name of the sequence
580      * @param boolean $ondemand when true the seqence is
581      *                          automatic created, if it
582      *                          not exists
583      * @return mixed MDB2 Error Object or id
584      * @access public
585      */
586     function nextID($seq_name, $ondemand = true)
587     {
588         $sequence_name = $this->getSequenceName($seq_name);
589         $query = 'SELECT GEN_ID('.strtoupper($sequence_name).', 1) as the_value FROM RDB$DATABASE';
590         $this->expectError('*');
591         $result = $this->queryOne($query, 'integer');
592         $this->popExpect();
593         if (PEAR::isError($result)) {
594             if ($ondemand) {
595                 $this->loadModule('Manager');
596                 // Since we are creating the sequence on demand
597                 // we know the first id = 1 so initialize the
598                 // sequence at 2
599                 $result = $this->manager->createSequence($seq_name, 2);
600                 if (PEAR::isError($result)) {
601                     return $this->raiseError(MDB2_ERROR, null, null,
602                         'nextID: on demand sequence could not be created');
603                 } else {
604                     // First ID of a newly created sequence is 1
605                     // return 1;
606                     // BUT generators are not always reset, so return the actual value
607                     return $this->currID($seq_name);
608                 }
609             }
610         }
611         return $result;
612     }
613
614     // }}}
615     // {{{ currID()
616
617     /**
618      * returns the current id of a sequence
619      *
620      * @param string $seq_name name of the sequence
621      * @return mixed MDB2 Error Object or id
622      * @access public
623      */
624     function currID($seq_name)
625     {
626         $sequence_name = $this->getSequenceName($seq_name);
627         $query = 'SELECT GEN_ID('.strtoupper($sequence_name).', 0) as the_value FROM RDB$DATABASE';
628         $value = @$this->queryOne($query);
629         if (PEAR::isError($value)) {
630             return $this->raiseError(MDB2_ERROR, null, null,
631                 'currID: Unable to select from ' . $seq_name) ;
632         }
633         if (!is_numeric($value)) {
634             return $this->raiseError(MDB2_ERROR, null, null,
635                 'currID: could not find value in sequence table');
636         }
637         return $value;
638     }
639
640     // }}}
641 }
642
643 class MDB2_Result_ibase extends MDB2_Result_Common
644 {
645     // {{{ _skipLimitOffset()
646
647     /**
648      * Skip the first row of a result set.
649      *
650      * @param resource $result
651      * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure
652      * @access protected
653      */
654     function _skipLimitOffset()
655     {
656         if ($this->db->dsn['dbsyntax'] == 'firebird') {
657             return true;
658         }
659         if ($this->limit) {
660             if ($this->rownum > $this->limit) {
661                 return false;
662             }
663         }
664         if ($this->offset) {
665             while ($this->offset_count < $this->offset) {
666                 ++$this->offset_count;
667                 if (!is_array(@ibase_fetch_row($this->result))) {
668                     $this->offset_count = $this->offset;
669                     return false;
670                 }
671             }
672         }
673         return true;
674     }
675
676     // }}}
677     // {{{ fetchRow()
678
679     /**
680      * Fetch a row and insert the data into an existing array.
681      *
682      * @param int  $fetchmode how the array data should be indexed
683      * @param int  $rownum    number of the row where the data can be found
684      * @return int data array on success, a MDB2 error on failure
685      * @access public
686      */
687     function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null)
688     {
689         if ($this->result === true) {
690             //query successfully executed, but without results...
691             $null = null;
692             return $null;
693         }
694         if (!$this->_skipLimitOffset()) {
695             $null = null;
696             return $null;
697         }
698         if (!is_null($rownum)) {
699             $seek = $this->seek($rownum);
700             if (PEAR::isError($seek)) {
701                 return $seek;
702             }
703         }
704         if ($fetchmode == MDB2_FETCHMODE_DEFAULT) {
705             $fetchmode = $this->db->fetchmode;
706         }
707         if ($fetchmode & MDB2_FETCHMODE_ASSOC) {
708             $row = @ibase_fetch_assoc($this->result);
709             if (is_array($row)
710                 && $this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE
711             ) {
712                 $row = array_change_key_case($row, $this->db->options['field_case']);
713             }
714         } else {
715             $row = @ibase_fetch_row($this->result);
716         }
717         if (!$row) {
718             if (is_null($this->result)) {
719                 $err =& $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
720                     'fetchRow: resultset has already been freed');
721                 return $err;
722             }
723             $null = null;
724             return $null;
725         }
726         if (($mode = ($this->db->options['portability'] & MDB2_PORTABILITY_RTRIM)
727             + ($this->db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL))
728         ) {
729             $this->db->_fixResultArrayValues($row, $mode);
730         }
731         if (!empty($this->values)) {
732             $this->_assignBindColumns($row);
733         }
734         if (!empty($this->types)) {
735             $row = $this->db->datatype->convertResultRow($this->types, $row);
736         }
737         if ($fetchmode === MDB2_FETCHMODE_OBJECT) {
738             $object_class = $this->db->options['fetch_class'];
739             if ($object_class == 'stdClass') {
740                 $row = (object) $row;
741             } else {
742                 $row = &new $object_class($row);
743             }
744         }
745         ++$this->rownum;
746         return $row;
747     }
748
749     // }}}
750     // {{{ _getColumnNames()
751
752     /**
753      * Retrieve the names of columns returned by the DBMS in a query result.
754      *
755      * @return mixed associative array variable
756      *      that holds the names of columns. The indexes of the array are
757      *      the column names mapped to lower case and the values are the
758      *      respective numbers of the columns starting from 0. Some DBMS may
759      *      not return any columns when the result set does not contain any
760      *      rows.
761      * @access private
762      */
763     function _getColumnNames()
764     {
765         $columns = array();
766         $numcols = $this->numCols();
767         if (PEAR::isError($numcols)) {
768             return $numcols;
769         }
770         for ($column = 0; $column < $numcols; $column++) {
771             $column_info = @ibase_field_info($this->result, $column);
772             $columns[$column_info['alias']] = $column;
773         }
774         if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) {
775             $columns = array_change_key_case($columns, $this->db->options['field_case']);
776         }
777         return $columns;
778     }
779
780     // }}}
781     // {{{ numCols()
782
783     /**
784      * Count the number of columns returned by the DBMS in a query result.
785      *
786      * @return mixed integer value with the number of columns, a MDB2 error
787      *      on failure
788      * @access public
789      */
790     function numCols()
791     {
792         if ($this->result === true) {
793             //query successfully executed, but without results...
794             return 0;
795         }
796
797         if (!is_resource($this->result)) {
798             return $this->db->raiseError('numCols(): not a valid ibase resource');
799         }
800         $cols = @ibase_num_fields($this->result);
801         if (is_null($cols)) {
802             if (is_null($this->result)) {
803                 return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
804                     'numCols: resultset has already been freed');
805             }
806             return $this->db->raiseError();
807         }
808         return $cols;
809     }
810
811     // }}}
812     // {{{ free()
813
814     /**
815      * Free the internal resources associated with $result.
816      *
817      * @return boolean true on success, false if $result is invalid
818      * @access public
819      */
820     function free()
821     {
822         if (is_resource($this->result)) {
823             $free = @ibase_free_result($this->result);
824             if (!$free) {
825                 if (is_null($this->result)) {
826                     return MDB2_OK;
827                 }
828                 return $this->db->raiseError();
829             }
830         }
831         $this->result = null;
832         return MDB2_OK;
833     }
834
835     // }}}
836 }
837
838 class MDB2_BufferedResult_ibase extends MDB2_Result_ibase
839 {
840     // {{{ class vars
841
842     var $buffer;
843     var $buffer_rownum = - 1;
844
845     // }}}
846     // {{{ _fillBuffer()
847
848     /**
849      * Fill the row buffer
850      *
851      * @param int $rownum   row number upto which the buffer should be filled
852      *                      if the row number is null all rows are ready into the buffer
853      * @return boolean true on success, false on failure
854      * @access protected
855      */
856     function _fillBuffer($rownum = null)
857     {
858         if (isset($this->buffer) && is_array($this->buffer)) {
859             if (is_null($rownum)) {
860                 if (!end($this->buffer)) {
861                     return false;
862                 }
863             } elseif (isset($this->buffer[$rownum])) {
864                 return (bool) $this->buffer[$rownum];
865             }
866         }
867
868         if (!$this->_skipLimitOffset()) {
869             return false;
870         }
871
872         $buffer = true;
873         while ((is_null($rownum) || $this->buffer_rownum < $rownum)
874             && (!$this->limit || $this->buffer_rownum < $this->limit)
875             && ($buffer = @ibase_fetch_row($this->result))
876         ) {
877             ++$this->buffer_rownum;
878             $this->buffer[$this->buffer_rownum] = $buffer;
879         }
880
881         if (!$buffer) {
882             ++$this->buffer_rownum;
883             $this->buffer[$this->buffer_rownum] = false;
884             return false;
885         } elseif ($this->limit && $this->buffer_rownum >= $this->limit) {
886             ++$this->buffer_rownum;
887             $this->buffer[$this->buffer_rownum] = false;
888         }
889         return true;
890     }
891
892     // }}}
893     // {{{ fetchRow()
894
895     /**
896      * Fetch a row and insert the data into an existing array.
897      *
898      * @param int       $fetchmode  how the array data should be indexed
899      * @param int    $rownum    number of the row where the data can be found
900      * @return int data array on success, a MDB2 error on failure
901      * @access public
902      */
903     function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null)
904     {
905         if ($this->result === true) {
906             //query successfully executed, but without results...
907             $null = null;
908             return $null;
909         }
910         if (is_null($this->result)) {
911             $err =& $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
912                 'fetchRow: resultset has already been freed');
913             return $err;
914         }
915         if (!is_null($rownum)) {
916             $seek = $this->seek($rownum);
917             if (PEAR::isError($seek)) {
918                 return $seek;
919             }
920         }
921         $target_rownum = $this->rownum + 1;
922         if ($fetchmode == MDB2_FETCHMODE_DEFAULT) {
923             $fetchmode = $this->db->fetchmode;
924         }
925         if (!$this->_fillBuffer($target_rownum)) {
926             $null = null;
927             return $null;
928         }
929         $row = $this->buffer[$target_rownum];
930         if ($fetchmode & MDB2_FETCHMODE_ASSOC) {
931             $column_names = $this->getColumnNames();
932             foreach ($column_names as $name => $i) {
933                 $column_names[$name] = $row[$i];
934             }
935             $row = $column_names;
936         }
937         if (($mode = ($this->db->options['portability'] & MDB2_PORTABILITY_RTRIM)
938             + ($this->db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL))
939         ) {
940             $this->db->_fixResultArrayValues($row, $mode);
941         }
942         if (!empty($this->values)) {
943             $this->_assignBindColumns($row);
944         }
945         if (!empty($this->types)) {
946             $row = $this->db->datatype->convertResultRow($this->types, $row);
947         }
948         if ($fetchmode === MDB2_FETCHMODE_OBJECT) {
949             $object_class = $this->db->options['fetch_class'];
950             if ($object_class == 'stdClass') {
951                 $row = (object) $row;
952             } else {
953                 $row = &new $object_class($row);
954             }
955         }
956         ++$this->rownum;
957         return $row;
958     }
959
960     // }}}
961     // {{{ seek()
962
963     /**
964      * seek to a specific row in a result set
965      *
966      * @param int    $rownum    number of the row where the data can be found
967      * @return mixed MDB2_OK on success, a MDB2 error on failure
968      * @access public
969      */
970     function seek($rownum = 0)
971     {
972         if (is_null($this->result)) {
973             return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
974                 'seek: resultset has already been freed');
975         }
976         $this->rownum = $rownum - 1;
977         return MDB2_OK;
978     }
979
980     // }}}
981     // {{{ valid()
982
983     /**
984      * check if the end of the result set has been reached
985      *
986      * @return mixed true or false on sucess, a MDB2 error on failure
987      * @access public
988      */
989     function valid()
990     {
991         if (is_null($this->result)) {
992             return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
993                 'valid: resultset has already been freed');
994         }
995         if ($this->_fillBuffer($this->rownum + 1)) {
996             return true;
997         }
998         return false;
999     }
1000
1001     // }}}
1002     // {{{ numRows()
1003
1004     /**
1005      * returns the number of rows in a result object
1006      *
1007      * @return mixed MDB2 Error Object or the number of rows
1008      * @access public
1009      */
1010     function numRows()
1011     {
1012         if (is_null($this->result)) {
1013             return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
1014                 'seek: resultset has already been freed');
1015         }
1016         $this->_fillBuffer();
1017         return $this->buffer_rownum;
1018     }
1019
1020     // }}}
1021     // {{{ free()
1022
1023     /**
1024      * Free the internal resources associated with $result.
1025      *
1026      * @return boolean true on success, false if $result is invalid
1027      * @access public
1028      */
1029     function free()
1030     {
1031         $this->buffer = null;
1032         $this->buffer_rownum = null;
1033         $free = parent::free();
1034     }
1035
1036     // }}}
1037 }
1038
1039 class MDB2_Statement_ibase extends MDB2_Statement_Common
1040 {
1041     // {{{ _execute()
1042
1043     /**
1044      * Execute a prepared query statement helper method.
1045      *
1046      * @param mixed $result_class string which specifies which result class to use
1047      * @param mixed $result_wrap_class string which specifies which class to wrap results in
1048      * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure
1049      * @access private
1050      */
1051     function &_execute($result_class = true, $result_wrap_class = false)
1052     {
1053         $isManip = MDB2::isManip($this->query);
1054         $this->db->last_query = $this->query;
1055         $this->db->debug($this->query, 'execute');
1056         if ($this->db->getOption('disable_query')) {
1057             if ($isManip) {
1058                 $return = 0;
1059                 return $return;
1060             }
1061             $null = null;
1062             return $null;
1063         }
1064
1065         $connected = $this->db->connect();
1066         if (PEAR::isError($connected)) {
1067             return $connected;
1068         }
1069         $connection = $this->db->in_transaction
1070             ? $this->db->transaction_id : $this->db->connection;
1071
1072         $parameters = array(0 => $this->statement);
1073         $i = 0;
1074         foreach ($this->values as $parameter => $value) {
1075             $type = array_key_exists($parameter, $this->types) ? $this->types[$parameter] : null;
1076             $parameters[] = $this->db->quote($value, $type, false);
1077             ++$i;
1078         }
1079
1080         $result = call_user_func_array('ibase_execute', $parameters);
1081         if ($result === false) {
1082             $err =& $this->db->raiseError();
1083             return $err;
1084         }
1085
1086         if ($isManip) {
1087             $affected_rows = (function_exists('ibase_affected_rows') ? ibase_affected_rows($connection) : 0);
1088             return $affected_rows;
1089         }
1090
1091         $result =& $this->db->_wrapResult($result, $this->types,
1092             $result_class, $result_wrap_class, $this->row_limit, $this->row_offset);
1093         return $result;
1094     }
1095
1096     // }}}
1097
1098     // }}}
1099     // {{{ free()
1100
1101     /**
1102      * Release resources allocated for the specified prepared query.
1103      *
1104      * @return mixed MDB2_OK on success, a MDB2 error on failure
1105      * @access public
1106      */
1107     function free()
1108     {
1109         if (!@ibase_free_query($this->statement)) {
1110             return $this->db->raiseError();
1111         }
1112         return MDB2_OK;
1113     }
1114 }
1115 ?>