Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
// vim: set et ts=4 sw=4 fdm=marker:
3
// +----------------------------------------------------------------------+
4
// | PHP versions 4 and 5                                                 |
5
// +----------------------------------------------------------------------+
6
// | Copyright (c) 1998-2006 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: Lukas Smith <smith@pooteeweet.org>                           |
44
// +----------------------------------------------------------------------+
45
//
46
// $Id: mysqli.php,v 1.162 2007/05/02 22:00:08 quipo Exp $
47
//
48
 
49
/**
50
 * MDB2 MySQLi driver
51
 *
52
 * @package MDB2
53
 * @category Database
54
 * @author  Lukas Smith <smith@pooteeweet.org>
55
 */
56
class MDB2_Driver_mysqli extends MDB2_Driver_Common
57
{
58
    // {{{ properties
59
    var $string_quoting = array('start' => "'", 'end' => "'", 'escape' => '\\', 'escape_pattern' => '\\');
60
 
61
    var $identifier_quoting = array('start' => '`', 'end' => '`', 'escape' => '`');
62
 
63
    var $sql_comments = array(
64
        array('start' => '-- ', 'end' => "\n", 'escape' => false),
65
        array('start' => '#', 'end' => "\n", 'escape' => false),
66
        array('start' => '/*', 'end' => '*/', 'escape' => false),
67
    );
68
 
69
    var $start_transaction = false;
70
 
71
    var $varchar_max_length = 255;
72
    // }}}
73
    // {{{ constructor
74
 
75
    /**
76
     * Constructor
77
     */
78
    function __construct()
79
    {
80
        parent::__construct();
81
 
82
        $this->phptype = 'mysqli';
83
        $this->dbsyntax = 'mysql';
84
 
85
        $this->supported['sequences'] = 'emulated';
86
        $this->supported['indexes'] = true;
87
        $this->supported['affected_rows'] = true;
88
        $this->supported['transactions'] = false;
89
        $this->supported['savepoints'] = false;
90
        $this->supported['summary_functions'] = true;
91
        $this->supported['order_by_text'] = true;
92
        $this->supported['current_id'] = 'emulated';
93
        $this->supported['limit_queries'] = true;
94
        $this->supported['LOBs'] = true;
95
        $this->supported['replace'] = true;
96
        $this->supported['sub_selects'] = 'emulated';
97
        $this->supported['auto_increment'] = true;
98
        $this->supported['primary_key'] = true;
99
        $this->supported['result_introspection'] = true;
100
        $this->supported['prepared_statements'] = 'emulated';
101
        $this->supported['identifier_quoting'] = true;
102
        $this->supported['pattern_escaping'] = true;
103
        $this->supported['new_link'] = true;
104
 
105
        $this->options['default_table_type'] = '';
106
        $this->options['multi_query'] = false;
107
    }
108
 
109
    // }}}
110
    // {{{ errorInfo()
111
 
112
    /**
113
     * This method is used to collect information about an error
114
     *
115
     * @param integer $error
116
     * @return array
117
     * @access public
118
     */
119
    function errorInfo($error = null)
120
    {
121
        if ($this->connection) {
122
            $native_code = @mysqli_errno($this->connection);
123
            $native_msg  = @mysqli_error($this->connection);
124
        } else {
125
            $native_code = @mysqli_connect_errno();
126
            $native_msg  = @mysqli_connect_error();
127
        }
128
        if (is_null($error)) {
129
            static $ecode_map;
130
            if (empty($ecode_map)) {
131
                $ecode_map = array(
132
                    1004 => MDB2_ERROR_CANNOT_CREATE,
133
                    1005 => MDB2_ERROR_CANNOT_CREATE,
134
                    1006 => MDB2_ERROR_CANNOT_CREATE,
135
                    1007 => MDB2_ERROR_ALREADY_EXISTS,
136
                    1008 => MDB2_ERROR_CANNOT_DROP,
137
                    1022 => MDB2_ERROR_ALREADY_EXISTS,
138
                    1044 => MDB2_ERROR_ACCESS_VIOLATION,
139
                    1046 => MDB2_ERROR_NODBSELECTED,
140
                    1048 => MDB2_ERROR_CONSTRAINT,
141
                    1049 => MDB2_ERROR_NOSUCHDB,
142
                    1050 => MDB2_ERROR_ALREADY_EXISTS,
143
                    1051 => MDB2_ERROR_NOSUCHTABLE,
144
                    1054 => MDB2_ERROR_NOSUCHFIELD,
145
                    1061 => MDB2_ERROR_ALREADY_EXISTS,
146
                    1062 => MDB2_ERROR_ALREADY_EXISTS,
147
                    1064 => MDB2_ERROR_SYNTAX,
148
                    1091 => MDB2_ERROR_NOT_FOUND,
149
                    1100 => MDB2_ERROR_NOT_LOCKED,
150
                    1136 => MDB2_ERROR_VALUE_COUNT_ON_ROW,
151
                    1142 => MDB2_ERROR_ACCESS_VIOLATION,
152
                    1146 => MDB2_ERROR_NOSUCHTABLE,
153
                    1216 => MDB2_ERROR_CONSTRAINT,
154
                    1217 => MDB2_ERROR_CONSTRAINT,
155
                    1356 => MDB2_ERROR_DIVZERO,
156
                    1451 => MDB2_ERROR_CONSTRAINT,
157
                    1452 => MDB2_ERROR_CONSTRAINT,
158
                );
159
            }
160
            if ($this->options['portability'] & MDB2_PORTABILITY_ERRORS) {
161
                $ecode_map[1022] = MDB2_ERROR_CONSTRAINT;
162
                $ecode_map[1048] = MDB2_ERROR_CONSTRAINT_NOT_NULL;
163
                $ecode_map[1062] = MDB2_ERROR_CONSTRAINT;
164
            } else {
165
                // Doing this in case mode changes during runtime.
166
                $ecode_map[1022] = MDB2_ERROR_ALREADY_EXISTS;
167
                $ecode_map[1048] = MDB2_ERROR_CONSTRAINT;
168
                $ecode_map[1062] = MDB2_ERROR_ALREADY_EXISTS;
169
            }
170
            if (isset($ecode_map[$native_code])) {
171
                $error = $ecode_map[$native_code];
172
            }
173
        }
174
        return array($error, $native_code, $native_msg);
175
    }
176
 
177
    // }}}
178
    // {{{ escape()
179
 
180
    /**
181
     * Quotes a string so it can be safely used in a query. It will quote
182
     * the text so it can safely be used within a query.
183
     *
184
     * @param   string  the input string to quote
185
     * @param   bool    escape wildcards
186
     *
187
     * @return  string  quoted string
188
     *
189
     * @access  public
190
     */
191
    function escape($text, $escape_wildcards = false)
192
    {
193
        if ($escape_wildcards) {
194
            $text = $this->escapePattern($text);
195
        }
196
        $connection = $this->getConnection();
197
        if (PEAR::isError($connection)) {
198
            return $connection;
199
        }
200
        $text = @mysqli_real_escape_string($connection, $text);
201
        return $text;
202
    }
203
 
204
    // }}}
205
    // {{{ beginTransaction()
206
 
207
    /**
208
     * Start a transaction or set a savepoint.
209
     *
210
     * @param   string  name of a savepoint to set
211
     * @return  mixed   MDB2_OK on success, a MDB2 error on failure
212
     *
213
     * @access  public
214
     */
215
    function beginTransaction($savepoint = null)
216
    {
217
        $this->debug('Starting transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint));
218
        $this->_getServerCapabilities();
219
        if (!is_null($savepoint)) {
220
            if (!$this->supports('savepoints')) {
221
                return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
222
                    'savepoints are not supported', __FUNCTION__);
223
            }
224
            if (!$this->in_transaction) {
225
                return $this->raiseError(MDB2_ERROR_INVALID, null, null,
226
                    'savepoint cannot be released when changes are auto committed', __FUNCTION__);
227
            }
228
            $query = 'SAVEPOINT '.$savepoint;
229
            return $this->_doQuery($query, true);
230
        } elseif ($this->in_transaction) {
231
            return MDB2_OK;  //nothing to do
232
        }
233
        $query = $this->start_transaction ? 'START TRANSACTION' : 'SET AUTOCOMMIT = 1';
234
        $result =& $this->_doQuery($query, true);
235
        if (PEAR::isError($result)) {
236
            return $result;
237
        }
238
        $this->in_transaction = true;
239
        return MDB2_OK;
240
    }
241
 
242
    // }}}
243
    // {{{ commit()
244
 
245
    /**
246
     * Commit the database changes done during a transaction that is in
247
     * progress or release a savepoint. This function may only be called when
248
     * auto-committing is disabled, otherwise it will fail. Therefore, a new
249
     * transaction is implicitly started after committing the pending changes.
250
     *
251
     * @param   string  name of a savepoint to release
252
     * @return  mixed   MDB2_OK on success, a MDB2 error on failure
253
     *
254
     * @access  public
255
     */
256
    function commit($savepoint = null)
257
    {
258
        $this->debug('Committing transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint));
259
        if (!$this->in_transaction) {
260
            return $this->raiseError(MDB2_ERROR_INVALID, null, null,
261
                'commit/release savepoint cannot be done changes are auto committed', __FUNCTION__);
262
        }
263
        if (!is_null($savepoint)) {
264
            if (!$this->supports('savepoints')) {
265
                return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
266
                    'savepoints are not supported', __FUNCTION__);
267
            }
268
            $server_info = $this->getServerVersion();
269
            if (version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '5.0.3', '<')) {
270
                return MDB2_OK;
271
            }
272
            $query = 'RELEASE SAVEPOINT '.$savepoint;
273
            return $this->_doQuery($query, true);
274
        }
275
 
276
        if (!$this->supports('transactions')) {
277
            return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
278
                'transactions are not supported', __FUNCTION__);
279
        }
280
 
281
        $result =& $this->_doQuery('COMMIT', true);
282
        if (PEAR::isError($result)) {
283
            return $result;
284
        }
285
        if (!$this->start_transaction) {
286
            $query = 'SET AUTOCOMMIT = 0';
287
            $result =& $this->_doQuery($query, true);
288
            if (PEAR::isError($result)) {
289
                return $result;
290
            }
291
        }
292
        $this->in_transaction = false;
293
        return MDB2_OK;
294
    }
295
 
296
    // }}}
297
    // {{{ rollback()
298
 
299
    /**
300
     * Cancel any database changes done during a transaction or since a specific
301
     * savepoint that is in progress. This function may only be called when
302
     * auto-committing is disabled, otherwise it will fail. Therefore, a new
303
     * transaction is implicitly started after canceling the pending changes.
304
     *
305
     * @param   string  name of a savepoint to rollback to
306
     * @return  mixed   MDB2_OK on success, a MDB2 error on failure
307
     *
308
     * @access  public
309
     */
310
    function rollback($savepoint = null)
311
    {
312
        $this->debug('Rolling back transaction/savepoint', __FUNCTION__, array('is_manip' => true, 'savepoint' => $savepoint));
313
        if (!$this->in_transaction) {
314
            return $this->raiseError(MDB2_ERROR_INVALID, null, null,
315
                'rollback cannot be done changes are auto committed', __FUNCTION__);
316
        }
317
        if (!is_null($savepoint)) {
318
            if (!$this->supports('savepoints')) {
319
                return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
320
                    'savepoints are not supported', __FUNCTION__);
321
            }
322
            $query = 'ROLLBACK TO SAVEPOINT '.$savepoint;
323
            return $this->_doQuery($query, true);
324
        }
325
 
326
        $query = 'ROLLBACK';
327
        $result =& $this->_doQuery($query, true);
328
        if (PEAR::isError($result)) {
329
            return $result;
330
        }
331
        if (!$this->start_transaction) {
332
            $query = 'SET AUTOCOMMIT = 0';
333
            $result =& $this->_doQuery($query, true);
334
            if (PEAR::isError($result)) {
335
                return $result;
336
            }
337
        }
338
        $this->in_transaction = false;
339
        return MDB2_OK;
340
    }
341
 
342
    // }}}
343
    // {{{ function setTransactionIsolation()
344
 
345
    /**
346
     * Set the transacton isolation level.
347
     *
348
     * @param   string  standard isolation level
349
     *                  READ UNCOMMITTED (allows dirty reads)
350
     *                  READ COMMITTED (prevents dirty reads)
351
     *                  REPEATABLE READ (prevents nonrepeatable reads)
352
     *                  SERIALIZABLE (prevents phantom reads)
353
     * @return  mixed   MDB2_OK on success, a MDB2 error on failure
354
     *
355
     * @access  public
356
     * @since   2.1.1
357
     */
358
    function setTransactionIsolation($isolation)
359
    {
360
        $this->debug('Setting transaction isolation level', __FUNCTION__, array('is_manip' => true));
361
        if (!$this->supports('transactions')) {
362
            return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
363
                'transactions are not supported', __FUNCTION__);
364
        }
365
        switch ($isolation) {
366
        case 'READ UNCOMMITTED':
367
        case 'READ COMMITTED':
368
        case 'REPEATABLE READ':
369
        case 'SERIALIZABLE':
370
            break;
371
        default:
372
            return $this->raiseError(MDB2_ERROR_UNSUPPORTED, null, null,
373
                'isolation level is not supported: '.$isolation, __FUNCTION__);
374
        }
375
 
376
        $query = "SET SESSION TRANSACTION ISOLATION LEVEL $isolation";
377
        return $this->_doQuery($query, true);
378
    }
379
 
380
    // }}}
381
    // {{{ connect()
382
 
383
    /**
384
     * Connect to the database
385
     *
386
     * @return true on success, MDB2 Error Object on failure
387
     */
388
    function connect()
389
    {
390
        if (is_object($this->connection)) {
391
            if (count(array_diff($this->connected_dsn, $this->dsn)) == 0) {
392
                return MDB2_OK;
393
            }
394
            $this->connection = 0;
395
        }
396
 
397
        if (!PEAR::loadExtension($this->phptype)) {
398
            return $this->raiseError(MDB2_ERROR_NOT_FOUND, null, null,
399
                'extension '.$this->phptype.' is not compiled into PHP', __FUNCTION__);
400
        }
401
 
402
        if ($this->options['ssl']) {
403
            $init = @mysqli_init();
404
            @mysqli_ssl_set(
405
                $init,
406
                empty($this->dsn['key'])    ? null : $this->dsn['key'],
407
                empty($this->dsn['cert'])   ? null : $this->dsn['cert'],
408
                empty($this->dsn['ca'])     ? null : $this->dsn['ca'],
409
                empty($this->dsn['capath']) ? null : $this->dsn['capath'],
410
                empty($this->dsn['cipher']) ? null : $this->dsn['cipher']
411
            );
412
            if ($connection = @mysqli_real_connect(
413
                    $init,
414
                    $this->dsn['hostspec'],
415
                    $this->dsn['username'],
416
                    $this->dsn['password'],
417
                    $this->database_name,
418
                    $this->dsn['port'],
419
                    $this->dsn['socket']))
420
            {
421
                $connection = $init;
422
            }
423
        } else {
424
            $connection = @mysqli_connect(
425
                $this->dsn['hostspec'],
426
                $this->dsn['username'],
427
                $this->dsn['password'],
428
                $this->database_name,
429
                $this->dsn['port'],
430
                $this->dsn['socket']
431
            );
432
        }
433
 
434
        if (!$connection) {
435
            if (($err = @mysqli_connect_error()) != '') {
436
                return $this->raiseError(null,
437
                    null, null, $err, __FUNCTION__);
438
            } else {
439
                return $this->raiseError(MDB2_ERROR_CONNECT_FAILED, null, null,
440
                    'unable to establish a connection', __FUNCTION__);
441
            }
442
        }
443
 
444
        if (!empty($this->dsn['charset'])) {
445
            $result = $this->setCharset($this->dsn['charset'], $connection);
446
            if (PEAR::isError($result)) {
447
                return $result;
448
            }
449
        }
450
 
451
        $this->connection = $connection;
452
        $this->connected_dsn = $this->dsn;
453
        $this->connected_database_name = $this->database_name;
454
        $this->dbsyntax = $this->dsn['dbsyntax'] ? $this->dsn['dbsyntax'] : $this->phptype;
455
 
456
        $this->supported['transactions'] = $this->options['use_transactions'];
457
        if ($this->options['default_table_type']) {
458
            switch (strtoupper($this->options['default_table_type'])) {
459
            case 'BLACKHOLE':
460
            case 'MEMORY':
461
            case 'ARCHIVE':
462
            case 'CSV':
463
            case 'HEAP':
464
            case 'ISAM':
465
            case 'MERGE':
466
            case 'MRG_ISAM':
467
            case 'ISAM':
468
            case 'MRG_MYISAM':
469
            case 'MYISAM':
470
                $this->supported['transactions'] = false;
471
                $this->warnings[] = $this->options['default_table_type'] .
472
                    ' is not a supported default table type';
473
                break;
474
            }
475
        }
476
 
477
        $this->_getServerCapabilities();
478
 
479
        return MDB2_OK;
480
    }
481
 
482
    // }}}
483
    // {{{ setCharset()
484
 
485
    /**
486
     * Set the charset on the current connection
487
     *
488
     * @param string    charset
489
     * @param resource  connection handle
490
     *
491
     * @return true on success, MDB2 Error Object on failure
492
     */
493
    function setCharset($charset, $connection = null)
494
    {
495
        if (is_null($connection)) {
496
            $connection = $this->getConnection();
497
            if (PEAR::isError($connection)) {
498
                return $connection;
499
            }
500
        }
501
        $query = "SET NAMES '".mysqli_real_escape_string($connection, $charset)."'";
502
        return $this->_doQuery($query, true, $connection);
503
    }
504
 
505
    // }}}
506
    // {{{ disconnect()
507
 
508
    /**
509
     * Log out and disconnect from the database.
510
     *
511
     * @param  boolean $force if the disconnect should be forced even if the
512
     *                        connection is opened persistently
513
     * @return mixed true on success, false if not connected and error
514
     *                object on error
515
     * @access public
516
     */
517
    function disconnect($force = true)
518
    {
519
        if (is_object($this->connection)) {
520
            if ($this->in_transaction) {
521
                $dsn = $this->dsn;
522
                $database_name = $this->database_name;
523
                $persistent = $this->options['persistent'];
524
                $this->dsn = $this->connected_dsn;
525
                $this->database_name = $this->connected_database_name;
526
                $this->options['persistent'] = $this->opened_persistent;
527
                $this->rollback();
528
                $this->dsn = $dsn;
529
                $this->database_name = $database_name;
530
                $this->options['persistent'] = $persistent;
531
            }
532
 
533
            if ($force) {
534
                @mysqli_close($this->connection);
535
            }
536
        }
537
        return parent::disconnect($force);
538
    }
539
 
540
    // }}}
541
    // {{{ _doQuery()
542
 
543
    /**
544
     * Execute a query
545
     * @param string $query  query
546
     * @param boolean $is_manip  if the query is a manipulation query
547
     * @param resource $connection
548
     * @param string $database_name
549
     * @return result or error object
550
     * @access protected
551
     */
552
    function &_doQuery($query, $is_manip = false, $connection = null, $database_name = null)
553
    {
554
        $this->last_query = $query;
555
        $result = $this->debug($query, 'query', array('is_manip' => $is_manip, 'when' => 'pre'));
556
        if ($result) {
557
            if (PEAR::isError($result)) {
558
                return $result;
559
            }
560
            $query = $result;
561
        }
562
        if ($this->options['disable_query']) {
563
            $result = $is_manip ? 0 : null;
564
            return $result;
565
        }
566
 
567
        if (is_null($connection)) {
568
            $connection = $this->getConnection();
569
            if (PEAR::isError($connection)) {
570
                return $connection;
571
            }
572
        }
573
        if (is_null($database_name)) {
574
            $database_name = $this->database_name;
575
        }
576
 
577
        if ($database_name) {
578
            if ($database_name != $this->connected_database_name) {
579
                if (!@mysqli_select_db($connection, $database_name)) {
580
                    $err = $this->raiseError(null, null, null,
581
                        'Could not select the database: '.$database_name, __FUNCTION__);
582
                    return $err;
583
                }
584
                $this->connected_database_name = $database_name;
585
            }
586
        }
587
 
588
        if ($this->options['multi_query']) {
589
            $result = mysqli_multi_query($connection, $query);
590
        } else {
591
            $resultmode = $this->options['result_buffering'] ? MYSQLI_USE_RESULT : MYSQLI_USE_RESULT;
592
            $result = mysqli_query($connection, $query);
593
        }
594
 
595
        if (!$result) {
596
            $err =& $this->raiseError(null, null, null,
597
                'Could not execute statement', __FUNCTION__);
598
            return $err;
599
        }
600
 
601
        if ($this->options['multi_query']) {
602
            if ($this->options['result_buffering']) {
603
                if (!($result = @mysqli_store_result($connection))) {
604
                    $err =& $this->raiseError(null, null, null,
605
                        'Could not get the first result from a multi query', __FUNCTION__);
606
                    return $err;
607
                }
608
            } elseif (!($result = @mysqli_use_result($connection))) {
609
                $err =& $this->raiseError(null, null, null,
610
                        'Could not get the first result from a multi query', __FUNCTION__);
611
                return $err;
612
            }
613
        }
614
 
615
        $this->debug($query, 'query', array('is_manip' => $is_manip, 'when' => 'post', 'result' => $result));
616
        return $result;
617
    }
618
 
619
    // }}}
620
    // {{{ _affectedRows()
621
 
622
    /**
623
     * Returns the number of rows affected
624
     *
625
     * @param resource $result
626
     * @param resource $connection
627
     * @return mixed MDB2 Error Object or the number of rows affected
628
     * @access private
629
     */
630
    function _affectedRows($connection, $result = null)
631
    {
632
        if (is_null($connection)) {
633
            $connection = $this->getConnection();
634
            if (PEAR::isError($connection)) {
635
                return $connection;
636
            }
637
        }
638
        return @mysqli_affected_rows($connection);
639
    }
640
 
641
    // }}}
642
    // {{{ _modifyQuery()
643
 
644
    /**
645
     * Changes a query string for various DBMS specific reasons
646
     *
647
     * @param string $query  query to modify
648
     * @param boolean $is_manip  if it is a DML query
649
     * @param integer $limit  limit the number of rows
650
     * @param integer $offset  start reading from given offset
651
     * @return string modified query
652
     * @access protected
653
     */
654
    function _modifyQuery($query, $is_manip, $limit, $offset)
655
    {
656
        if ($this->options['portability'] & MDB2_PORTABILITY_DELETE_COUNT) {
657
            // "DELETE FROM table" gives 0 affected rows in MySQL.
658
            // This little hack lets you know how many rows were deleted.
659
            if (preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $query)) {
660
                $query = preg_replace('/^\s*DELETE\s+FROM\s+(\S+)\s*$/',
661
                                      'DELETE FROM \1 WHERE 1=1', $query);
662
            }
663
        }
664
        if ($limit > 0
665
            && !preg_match('/LIMIT\s*\d(?:\s*(?:,|OFFSET)\s*\d+)?(?:[^\)]*)?$/i', $query)
666
        ) {
667
            $query = rtrim($query);
668
            if (substr($query, -1) == ';') {
669
                $query = substr($query, 0, -1);
670
            }
671
 
672
            // LIMIT doesn't always come last in the query
673
            // @see http://dev.mysql.com/doc/refman/5.0/en/select.html
674
            $after = '';
675
            if (preg_match('/(\s+INTO\s+(?:OUT|DUMP)FILE\s.*)$/ims', $query, $matches)) {
676
                $after = $matches[0];
677
                $query = preg_replace('/(\s+INTO\s+(?:OUT|DUMP)FILE\s.*)$/ims', '', $query);
678
            } elseif (preg_match('/(\s+FOR\s+UPDATE\s*)$/i', $query, $matches)) {
679
               $after = $matches[0];
680
               $query = preg_replace('/(\s+FOR\s+UPDATE\s*)$/im', '', $query);
681
            } elseif (preg_match('/(\s+LOCK\s+IN\s+SHARE\s+MODE\s*)$/im', $query, $matches)) {
682
               $after = $matches[0];
683
               $query = preg_replace('/(\s+LOCK\s+IN\s+SHARE\s+MODE\s*)$/im', '', $query);
684
            }
685
 
686
            if ($is_manip) {
687
                return $query . " LIMIT $limit";
688
            } else {
689
                return $query . " LIMIT $offset, $limit";
690
            }
691
        }
692
        return $query;
693
    }
694
 
695
    // }}}
696
    // {{{ getServerVersion()
697
 
698
    /**
699
     * return version information about the server
700
     *
701
     * @param bool   $native  determines if the raw version string should be returned
702
     * @return mixed array/string with version information or MDB2 error object
703
     * @access public
704
     */
705
    function getServerVersion($native = false)
706
    {
707
        $connection = $this->getConnection();
708
        if (PEAR::isError($connection)) {
709
            return $connection;
710
        }
711
        if ($this->connected_server_info) {
712
            $server_info = $this->connected_server_info;
713
        } else {
714
            $server_info = @mysqli_get_server_info($connection);
715
        }
716
        if (!$server_info) {
717
            return $this->raiseError(null, null, null,
718
                'Could not get server information', __FUNCTION__);
719
        }
720
        // cache server_info
721
        $this->connected_server_info = $server_info;
722
        if (!$native) {
723
            $tmp = explode('.', $server_info, 3);
724
            if (isset($tmp[2]) && strpos($tmp[2], '-')) {
725
                $tmp2 = explode('-', @$tmp[2], 2);
726
            } else {
727
                $tmp2[0] = isset($tmp[2]) ? $tmp[2] : null;
728
                $tmp2[1] = null;
729
            }
730
            $server_info = array(
731
                'major' => isset($tmp[0]) ? $tmp[0] : null,
732
                'minor' => isset($tmp[1]) ? $tmp[1] : null,
733
                'patch' => $tmp2[0],
734
                'extra' => $tmp2[1],
735
                'native' => $server_info,
736
            );
737
        }
738
        return $server_info;
739
    }
740
 
741
    // }}}
742
    // {{{ _getServerCapabilities()
743
 
744
    /**
745
     * Fetch some information about the server capabilities
746
     * (transactions, subselects, prepared statements, etc).
747
     *
748
     * @access private
749
     */
750
    function _getServerCapabilities()
751
    {
752
        static $already_checked = false;
753
        if (!$already_checked) {
754
            $already_checked = true;
755
 
756
            //set defaults
757
            $this->supported['sub_selects'] = 'emulated';
758
            $this->supported['prepared_statements'] = 'emulated';
759
            $this->start_transaction = false;
760
            $this->varchar_max_length = 255;
761
 
762
            $server_info = $this->getServerVersion();
763
            if (is_array($server_info)) {
764
                if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.1.0', '<')) {
765
                    $this->supported['sub_selects'] = true;
766
                    $this->supported['prepared_statements'] = true;
767
                }
768
 
769
                if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.0.14', '<')
770
                    || !version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.1.1', '<')
771
                ) {
772
                    $this->supported['savepoints'] = true;
773
                }
774
 
775
                if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '4.0.11', '<')) {
776
                    $this->start_transaction = true;
777
                }
778
 
779
                if (!version_compare($server_info['major'].'.'.$server_info['minor'].'.'.$server_info['patch'], '5.0.3', '<')) {
780
                    $this->varchar_max_length = 65532;
781
                }
782
            }
783
        }
784
    }
785
 
786
    // }}}
787
    // {{{ function _skipUserDefinedVariable($query, $position)
788
 
789
    /**
790
     * Utility method, used by prepare() to avoid misinterpreting MySQL user
791
     * defined variables (SELECT @x:=5) for placeholders.
792
     * Check if the placeholder is a false positive, i.e. if it is an user defined
793
     * variable instead. If so, skip it and advance the position, otherwise
794
     * return the current position, which is valid
795
     *
796
     * @param string $query
797
     * @param integer $position current string cursor position
798
     * @return integer $new_position
799
     * @access protected
800
     */
801
    function _skipUserDefinedVariable($query, $position)
802
    {
803
        $found = strpos(strrev(substr($query, 0, $position)), '@');
804
        if ($found === false) {
805
            return $position;
806
        }
807
        $pos = strlen($query) - strlen(substr($query, $position)) - $found - 1;
808
        $substring = substr($query, $pos, $position - $pos + 2);
809
        if (preg_match('/^@\w+:=$/', $substring)) {
810
            return $position + 1; //found an user defined variable: skip it
811
        }
812
        return $position;
813
    }
814
 
815
    // }}}
816
    // {{{ prepare()
817
 
818
    /**
819
     * Prepares a query for multiple execution with execute().
820
     * With some database backends, this is emulated.
821
     * prepare() requires a generic query as string like
822
     * 'INSERT INTO numbers VALUES(?,?)' or
823
     * 'INSERT INTO numbers VALUES(:foo,:bar)'.
824
     * The ? and :[a-zA-Z] and  are placeholders which can be set using
825
     * bindParam() and the query can be send off using the execute() method.
826
     *
827
     * @param string $query the query to prepare
828
     * @param mixed   $types  array that contains the types of the placeholders
829
     * @param mixed   $result_types  array that contains the types of the columns in
830
     *                        the result set or MDB2_PREPARE_RESULT, if set to
831
     *                        MDB2_PREPARE_MANIP the query is handled as a manipulation query
832
     * @param mixed   $lobs   key (field) value (parameter) pair for all lob placeholders
833
     * @return mixed resource handle for the prepared query on success, a MDB2
834
     *        error on failure
835
     * @access public
836
     * @see bindParam, execute
837
     */
838
    function &prepare($query, $types = null, $result_types = null, $lobs = array())
839
    {
840
        if ($this->options['emulate_prepared']
841
            || $this->supported['prepared_statements'] !== true
842
        ) {
843
            $obj =& parent::prepare($query, $types, $result_types, $lobs);
844
            return $obj;
845
        }
846
        $is_manip = ($result_types === MDB2_PREPARE_MANIP);
847
        $offset = $this->offset;
848
        $limit = $this->limit;
849
        $this->offset = $this->limit = 0;
850
        $query = $this->_modifyQuery($query, $is_manip, $limit, $offset);
851
        $result = $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'pre'));
852
        if ($result) {
853
            if (PEAR::isError($result)) {
854
                return $result;
855
            }
856
            $query = $result;
857
        }
858
        $placeholder_type_guess = $placeholder_type = null;
859
        $question = '?';
860
        $colon = ':';
861
        $positions = array();
862
        $position = 0;
863
        while ($position < strlen($query)) {
864
            $q_position = strpos($query, $question, $position);
865
            $c_position = strpos($query, $colon, $position);
866
            if ($q_position && $c_position) {
867
                $p_position = min($q_position, $c_position);
868
            } elseif ($q_position) {
869
                $p_position = $q_position;
870
            } elseif ($c_position) {
871
                $p_position = $c_position;
872
            } else {
873
                break;
874
            }
875
            if (is_null($placeholder_type)) {
876
                $placeholder_type_guess = $query[$p_position];
877
            }
878
 
879
            $new_pos = $this->_skipDelimitedStrings($query, $position, $p_position);
880
            if (PEAR::isError($new_pos)) {
881
                return $new_pos;
882
            }
883
            if ($new_pos != $position) {
884
                $position = $new_pos;
885
                continue; //evaluate again starting from the new position
886
            }
887
 
888
            if ($query[$position] == $placeholder_type_guess) {
889
                if (is_null($placeholder_type)) {
890
                    $placeholder_type = $query[$p_position];
891
                    $question = $colon = $placeholder_type;
892
                }
893
                if ($placeholder_type == ':') {
894
                    //make sure this is not part of an user defined variable
895
                    $new_pos = $this->_skipUserDefinedVariable($query, $position);
896
                    if ($new_pos != $position) {
897
                        $position = $new_pos;
898
                        continue; //evaluate again starting from the new position
899
                    }
900
                    $parameter = preg_replace('/^.{'.($position+1).'}([a-z0-9_]+).*$/si', '\\1', $query);
901
                    if ($parameter === '') {
902
                        $err =& $this->raiseError(MDB2_ERROR_SYNTAX, null, null,
903
                            'named parameter with an empty name', __FUNCTION__);
904
                        return $err;
905
                    }
906
                    $positions[$p_position] = $parameter;
907
                    $query = substr_replace($query, '?', $position, strlen($parameter)+1);
908
                } else {
909
                    $positions[$p_position] = count($positions);
910
                }
911
                $position = $p_position + 1;
912
            } else {
913
                $position = $p_position;
914
            }
915
        }
916
        $connection = $this->getConnection();
917
        if (PEAR::isError($connection)) {
918
            return $connection;
919
        }
920
 
921
        if (!$is_manip) {
922
            $statement_name = sprintf($this->options['statement_format'], $this->phptype, md5(time() + rand()));
923
            $query = "PREPARE $statement_name FROM ".$this->quote($query, 'text');
924
 
925
            $statement =& $this->_doQuery($query, true, $connection);
926
            if (PEAR::isError($statement)) {
927
                return $statement;
928
            }
929
            $statement = $statement_name;
930
        } else {
931
            $statement = @mysqli_prepare($connection, $query);
932
            if (!$statement) {
933
                $err =& $this->raiseError(null, null, null,
934
                    'Unable to create prepared statement handle', __FUNCTION__);
935
                return $err;
936
            }
937
        }
938
 
939
        $class_name = 'MDB2_Statement_'.$this->phptype;
940
        $obj =& new $class_name($this, $statement, $positions, $query, $types, $result_types, $is_manip, $limit, $offset);
941
        $this->debug($query, __FUNCTION__, array('is_manip' => $is_manip, 'when' => 'post', 'result' => $obj));
942
        return $obj;
943
    }
944
 
945
    // }}}
946
    // {{{ replace()
947
 
948
    /**
949
     * Execute a SQL REPLACE query. A REPLACE query is identical to a INSERT
950
     * query, except that if there is already a row in the table with the same
951
     * key field values, the REPLACE query just updates its values instead of
952
     * inserting a new row.
953
     *
954
     * The REPLACE type of query does not make part of the SQL standards. Since
955
     * practically only MySQL implements it natively, this type of query is
956
     * emulated through this method for other DBMS using standard types of
957
     * queries inside a transaction to assure the atomicity of the operation.
958
     *
959
     * @access public
960
     *
961
     * @param string $table name of the table on which the REPLACE query will
962
     *  be executed.
963
     * @param array $fields associative array that describes the fields and the
964
     *  values that will be inserted or updated in the specified table. The
965
     *  indexes of the array are the names of all the fields of the table. The
966
     *  values of the array are also associative arrays that describe the
967
     *  values and other properties of the table fields.
968
     *
969
     *  Here follows a list of field properties that need to be specified:
970
     *
971
     *    value:
972
     *          Value to be assigned to the specified field. This value may be
973
     *          of specified in database independent type format as this
974
     *          function can perform the necessary datatype conversions.
975
     *
976
     *    Default:
977
     *          this property is required unless the Null property
978
     *          is set to 1.
979
     *
980
     *    type
981
     *          Name of the type of the field. Currently, all types Metabase
982
     *          are supported except for clob and blob.
983
     *
984
     *    Default: no type conversion
985
     *
986
     *    null
987
     *          Boolean property that indicates that the value for this field
988
     *          should be set to null.
989
     *
990
     *          The default value for fields missing in INSERT queries may be
991
     *          specified the definition of a table. Often, the default value
992
     *          is already null, but since the REPLACE may be emulated using
993
     *          an UPDATE query, make sure that all fields of the table are
994
     *          listed in this function argument array.
995
     *
996
     *    Default: 0
997
     *
998
     *    key
999
     *          Boolean property that indicates that this field should be
1000
     *          handled as a primary key or at least as part of the compound
1001
     *          unique index of the table that will determine the row that will
1002
     *          updated if it exists or inserted a new row otherwise.
1003
     *
1004
     *          This function will fail if no key field is specified or if the
1005
     *          value of a key field is set to null because fields that are
1006
     *          part of unique index they may not be null.
1007
     *
1008
     *    Default: 0
1009
     *
1010
     * @return mixed MDB2_OK on success, a MDB2 error on failure
1011
     */
1012
    function replace($table, $fields)
1013
    {
1014
        $count = count($fields);
1015
        $query = $values = '';
1016
        $keys = $colnum = 0;
1017
        for (reset($fields); $colnum < $count; next($fields), $colnum++) {
1018
            $name = key($fields);
1019
            if ($colnum > 0) {
1020
                $query .= ',';
1021
                $values.= ',';
1022
            }
1023
            $query.= $name;
1024
            if (isset($fields[$name]['null']) && $fields[$name]['null']) {
1025
                $value = 'NULL';
1026
            } else {
1027
                $type = isset($fields[$name]['type']) ? $fields[$name]['type'] : null;
1028
                $value = $this->quote($fields[$name]['value'], $type);
1029
            }
1030
            $values.= $value;
1031
            if (isset($fields[$name]['key']) && $fields[$name]['key']) {
1032
                if ($value === 'NULL') {
1033
                    return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null,
1034
                        'key value '.$name.' may not be NULL', __FUNCTION__);
1035
                }
1036
                $keys++;
1037
            }
1038
        }
1039
        if ($keys == 0) {
1040
            return $this->raiseError(MDB2_ERROR_CANNOT_REPLACE, null, null,
1041
                'not specified which fields are keys', __FUNCTION__);
1042
        }
1043
 
1044
        $connection = $this->getConnection();
1045
        if (PEAR::isError($connection)) {
1046
            return $connection;
1047
        }
1048
 
1049
        $query = "REPLACE INTO $table ($query) VALUES ($values)";
1050
        $result =& $this->_doQuery($query, true, $connection);
1051
        if (PEAR::isError($result)) {
1052
            return $result;
1053
        }
1054
        return $this->_affectedRows($connection, $result);
1055
    }
1056
 
1057
    // }}}
1058
    // {{{ nextID()
1059
 
1060
    /**
1061
     * Returns the next free id of a sequence
1062
     *
1063
     * @param string $seq_name name of the sequence
1064
     * @param boolean $ondemand when true the sequence is
1065
     *                          automatic created, if it
1066
     *                          not exists
1067
     *
1068
     * @return mixed MDB2 Error Object or id
1069
     * @access public
1070
     */
1071
    function nextID($seq_name, $ondemand = true)
1072
    {
1073
        $sequence_name = $this->quoteIdentifier($this->getSequenceName($seq_name), true);
1074
        $seqcol_name = $this->quoteIdentifier($this->options['seqcol_name'], true);
1075
        $query = "INSERT INTO $sequence_name ($seqcol_name) VALUES (NULL)";
1076
        $this->expectError(MDB2_ERROR_NOSUCHTABLE);
1077
        $result =& $this->_doQuery($query, true);
1078
        $this->popExpect();
1079
        if (PEAR::isError($result)) {
1080
            if ($ondemand && $result->getCode() == MDB2_ERROR_NOSUCHTABLE) {
1081
                $this->loadModule('Manager', null, true);
1082
                $result = $this->manager->createSequence($seq_name);
1083
                if (PEAR::isError($result)) {
1084
                    return $this->raiseError($result, null, null,
1085
                        'on demand sequence '.$seq_name.' could not be created', __FUNCTION__);
1086
                } else {
1087
                    return $this->nextID($seq_name, false);
1088
                }
1089
            }
1090
            return $result;
1091
        }
1092
        $value = $this->lastInsertID();
1093
        if (is_numeric($value)) {
1094
            $query = "DELETE FROM $sequence_name WHERE $seqcol_name < $value";
1095
            $result =& $this->_doQuery($query, true);
1096
            if (PEAR::isError($result)) {
1097
                $this->warnings[] = 'nextID: could not delete previous sequence table values from '.$seq_name;
1098
            }
1099
        }
1100
        return $value;
1101
    }
1102
 
1103
    // }}}
1104
    // {{{ lastInsertID()
1105
 
1106
    /**
1107
     * Returns the autoincrement ID if supported or $id or fetches the current
1108
     * ID in a sequence called: $table.(empty($field) ? '' : '_'.$field)
1109
     *
1110
     * @param string $table name of the table into which a new row was inserted
1111
     * @param string $field name of the field into which a new row was inserted
1112
     * @return mixed MDB2 Error Object or id
1113
     * @access public
1114
     */
1115
    function lastInsertID($table = null, $field = null)
1116
    {
1117
        // not using mysql_insert_id() due to http://pear.php.net/bugs/bug.php?id=8051
1118
        return $this->queryOne('SELECT LAST_INSERT_ID()');
1119
    }
1120
 
1121
    // }}}
1122
    // {{{ currID()
1123
 
1124
    /**
1125
     * Returns the current id of a sequence
1126
     *
1127
     * @param string $seq_name name of the sequence
1128
     * @return mixed MDB2 Error Object or id
1129
     * @access public
1130
     */
1131
    function currID($seq_name)
1132
    {
1133
        $sequence_name = $this->quoteIdentifier($this->getSequenceName($seq_name), true);
1134
        $seqcol_name = $this->quoteIdentifier($this->options['seqcol_name'], true);
1135
        $query = "SELECT MAX($seqcol_name) FROM $sequence_name";
1136
        return $this->queryOne($query, 'integer');
1137
    }
1138
}
1139
 
1140
/**
1141
 * MDB2 MySQLi result driver
1142
 *
1143
 * @package MDB2
1144
 * @category Database
1145
 * @author  Lukas Smith <smith@pooteeweet.org>
1146
 */
1147
class MDB2_Result_mysqli extends MDB2_Result_Common
1148
{
1149
    // }}}
1150
    // {{{ fetchRow()
1151
 
1152
    /**
1153
     * Fetch a row and insert the data into an existing array.
1154
     *
1155
     * @param int       $fetchmode  how the array data should be indexed
1156
     * @param int    $rownum    number of the row where the data can be found
1157
     * @return int data array on success, a MDB2 error on failure
1158
     * @access public
1159
     */
1160
    function &fetchRow($fetchmode = MDB2_FETCHMODE_DEFAULT, $rownum = null)
1161
    {
1162
        if (!is_null($rownum)) {
1163
            $seek = $this->seek($rownum);
1164
            if (PEAR::isError($seek)) {
1165
                return $seek;
1166
            }
1167
        }
1168
        if ($fetchmode == MDB2_FETCHMODE_DEFAULT) {
1169
            $fetchmode = $this->db->fetchmode;
1170
        }
1171
        if ($fetchmode & MDB2_FETCHMODE_ASSOC) {
1172
            $row = @mysqli_fetch_assoc($this->result);
1173
            if (is_array($row)
1174
                && $this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE
1175
            ) {
1176
                $row = array_change_key_case($row, $this->db->options['field_case']);
1177
            }
1178
        } else {
1179
           $row = @mysqli_fetch_row($this->result);
1180
        }
1181
 
1182
        if (!$row) {
1183
            if ($this->result === false) {
1184
                $err =& $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
1185
                    'resultset has already been freed', __FUNCTION__);
1186
                return $err;
1187
            }
1188
            $null = null;
1189
            return $null;
1190
        }
1191
        $mode = $this->db->options['portability'] & MDB2_PORTABILITY_EMPTY_TO_NULL;
1192
        if ($mode) {
1193
            $this->db->_fixResultArrayValues($row, $mode);
1194
        }
1195
        if (!empty($this->types)) {
1196
            $row = $this->db->datatype->convertResultRow($this->types, $row, false);
1197
        }
1198
        if (!empty($this->values)) {
1199
            $this->_assignBindColumns($row);
1200
        }
1201
        if ($fetchmode === MDB2_FETCHMODE_OBJECT) {
1202
            $object_class = $this->db->options['fetch_class'];
1203
            if ($object_class == 'stdClass') {
1204
                $row = (object) $row;
1205
            } else {
1206
                $row = &new $object_class($row);
1207
            }
1208
        }
1209
        ++$this->rownum;
1210
        return $row;
1211
    }
1212
 
1213
    // }}}
1214
    // {{{ _getColumnNames()
1215
 
1216
    /**
1217
     * Retrieve the names of columns returned by the DBMS in a query result.
1218
     *
1219
     * @return  mixed   Array variable that holds the names of columns as keys
1220
     *                  or an MDB2 error on failure.
1221
     *                  Some DBMS may not return any columns when the result set
1222
     *                  does not contain any rows.
1223
     * @access private
1224
     */
1225
    function _getColumnNames()
1226
    {
1227
        $columns = array();
1228
        $numcols = $this->numCols();
1229
        if (PEAR::isError($numcols)) {
1230
            return $numcols;
1231
        }
1232
        for ($column = 0; $column < $numcols; $column++) {
1233
            $column_info = @mysqli_fetch_field_direct($this->result, $column);
1234
            $columns[$column_info->name] = $column;
1235
        }
1236
        if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) {
1237
            $columns = array_change_key_case($columns, $this->db->options['field_case']);
1238
        }
1239
        return $columns;
1240
    }
1241
 
1242
    // }}}
1243
    // {{{ numCols()
1244
 
1245
    /**
1246
     * Count the number of columns returned by the DBMS in a query result.
1247
     *
1248
     * @return mixed integer value with the number of columns, a MDB2 error
1249
     *                       on failure
1250
     * @access public
1251
     */
1252
    function numCols()
1253
    {
1254
        $cols = @mysqli_num_fields($this->result);
1255
        if (is_null($cols)) {
1256
            if ($this->result === false) {
1257
                return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
1258
                    'resultset has already been freed', __FUNCTION__);
1259
            } elseif (is_null($this->result)) {
1260
                return count($this->types);
1261
            }
1262
            return $this->db->raiseError(null, null, null,
1263
                'Could not get column count', __FUNCTION__);
1264
        }
1265
        return $cols;
1266
    }
1267
 
1268
    // }}}
1269
    // {{{ nextResult()
1270
 
1271
    /**
1272
     * Move the internal result pointer to the next available result
1273
     *
1274
     * @return true on success, false if there is no more result set or an error object on failure
1275
     * @access public
1276
     */
1277
    function nextResult()
1278
    {
1279
        $connection = $this->db->getConnection();
1280
        if (PEAR::isError($connection)) {
1281
            return $connection;
1282
        }
1283
 
1284
        if (!@mysqli_more_results($connection)) {
1285
            return false;
1286
        }
1287
        if (!@mysqli_next_result($connection)) {
1288
            return false;
1289
        }
1290
        if (!($this->result = @mysqli_use_result($connection))) {
1291
            return false;
1292
        }
1293
        return MDB2_OK;
1294
    }
1295
 
1296
    // }}}
1297
    // {{{ free()
1298
 
1299
    /**
1300
     * Free the internal resources associated with result.
1301
     *
1302
     * @return boolean true on success, false if result is invalid
1303
     * @access public
1304
     */
1305
    function free()
1306
    {
1307
        if (is_object($this->result) && $this->db->connection) {
1308
            $free = @mysqli_free_result($this->result);
1309
            if ($free === false) {
1310
                return $this->db->raiseError(null, null, null,
1311
                    'Could not free result', __FUNCTION__);
1312
            }
1313
        }
1314
        $this->result = false;
1315
        return MDB2_OK;
1316
    }
1317
}
1318
 
1319
/**
1320
 * MDB2 MySQLi buffered result driver
1321
 *
1322
 * @package MDB2
1323
 * @category Database
1324
 * @author  Lukas Smith <smith@pooteeweet.org>
1325
 */
1326
class MDB2_BufferedResult_mysqli extends MDB2_Result_mysqli
1327
{
1328
    // }}}
1329
    // {{{ seek()
1330
 
1331
    /**
1332
     * Seek to a specific row in a result set
1333
     *
1334
     * @param int    $rownum    number of the row where the data can be found
1335
     * @return mixed MDB2_OK on success, a MDB2 error on failure
1336
     * @access public
1337
     */
1338
    function seek($rownum = 0)
1339
    {
1340
        if ($this->rownum != ($rownum - 1) && !@mysqli_data_seek($this->result, $rownum)) {
1341
            if ($this->result === false) {
1342
                return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
1343
                    'resultset has already been freed', __FUNCTION__);
1344
            } elseif (is_null($this->result)) {
1345
                return MDB2_OK;
1346
            }
1347
            return $this->db->raiseError(MDB2_ERROR_INVALID, null, null,
1348
                'tried to seek to an invalid row number ('.$rownum.')', __FUNCTION__);
1349
        }
1350
        $this->rownum = $rownum - 1;
1351
        return MDB2_OK;
1352
    }
1353
 
1354
    // }}}
1355
    // {{{ valid()
1356
 
1357
    /**
1358
     * Check if the end of the result set has been reached
1359
     *
1360
     * @return mixed true or false on sucess, a MDB2 error on failure
1361
     * @access public
1362
     */
1363
    function valid()
1364
    {
1365
        $numrows = $this->numRows();
1366
        if (PEAR::isError($numrows)) {
1367
            return $numrows;
1368
        }
1369
        return $this->rownum < ($numrows - 1);
1370
    }
1371
 
1372
    // }}}
1373
    // {{{ numRows()
1374
 
1375
    /**
1376
     * Returns the number of rows in a result object
1377
     *
1378
     * @return mixed MDB2 Error Object or the number of rows
1379
     * @access public
1380
     */
1381
    function numRows()
1382
    {
1383
        $rows = @mysqli_num_rows($this->result);
1384
        if (is_null($rows)) {
1385
            if ($this->result === false) {
1386
                return $this->db->raiseError(MDB2_ERROR_NEED_MORE_DATA, null, null,
1387
                    'resultset has already been freed', __FUNCTION__);
1388
            } elseif (is_null($this->result)) {
1389
                return 0;
1390
            }
1391
            return $this->db->raiseError(null, null, null,
1392
                'Could not get row count', __FUNCTION__);
1393
        }
1394
        return $rows;
1395
    }
1396
 
1397
    // }}}
1398
    // {{{ nextResult()
1399
 
1400
    /**
1401
     * Move the internal result pointer to the next available result
1402
     *
1403
     * @param a valid result resource
1404
     * @return true on success, false if there is no more result set or an error object on failure
1405
     * @access public
1406
     */
1407
    function nextResult()
1408
    {
1409
        $connection = $this->db->getConnection();
1410
        if (PEAR::isError($connection)) {
1411
            return $connection;
1412
        }
1413
 
1414
        if (!@mysqli_more_results($connection)) {
1415
            return false;
1416
        }
1417
        if (!@mysqli_next_result($connection)) {
1418
            return false;
1419
        }
1420
        if (!($this->result = @mysqli_store_result($connection))) {
1421
            return false;
1422
        }
1423
        return MDB2_OK;
1424
    }
1425
}
1426
 
1427
/**
1428
 * MDB2 MySQLi statement driver
1429
 *
1430
 * @package MDB2
1431
 * @category Database
1432
 * @author  Lukas Smith <smith@pooteeweet.org>
1433
 */
1434
class MDB2_Statement_mysqli extends MDB2_Statement_Common
1435
{
1436
    // {{{ _execute()
1437
 
1438
    /**
1439
     * Execute a prepared query statement helper method.
1440
     *
1441
     * @param mixed $result_class string which specifies which result class to use
1442
     * @param mixed $result_wrap_class string which specifies which class to wrap results in
1443
     * @return mixed a result handle or MDB2_OK on success, a MDB2 error on failure
1444
     * @access private
1445
     */
1446
    function &_execute($result_class = true, $result_wrap_class = false)
1447
    {
1448
        if (is_null($this->statement)) {
1449
            $result =& parent::_execute($result_class, $result_wrap_class);
1450
            return $result;
1451
        }
1452
        $this->db->last_query = $this->query;
1453
        $this->db->debug($this->query, 'execute', array('is_manip' => $this->is_manip, 'when' => 'pre', 'parameters' => $this->values));
1454
        if ($this->db->getOption('disable_query')) {
1455
            $result = $this->is_manip ? 0 : null;
1456
            return $result;
1457
        }
1458
 
1459
        $connection = $this->db->getConnection();
1460
        if (PEAR::isError($connection)) {
1461
            return $connection;
1462
        }
1463
 
1464
        if (!is_object($this->statement)) {
1465
            $query = 'EXECUTE '.$this->statement;
1466
        }
1467
        if (!empty($this->positions)) {
1468
            $parameters = array(0 => $this->statement, 1 => '');
1469
            $lobs = array();
1470
            $i = 0;
1471
            foreach ($this->positions as $parameter) {
1472
                if (!array_key_exists($parameter, $this->values)) {
1473
                    return $this->db->raiseError(MDB2_ERROR_NOT_FOUND, null, null,
1474
                        'Unable to bind to missing placeholder: '.$parameter, __FUNCTION__);
1475
                }
1476
                $value = $this->values[$parameter];
1477
                $type = array_key_exists($parameter, $this->types) ? $this->types[$parameter] : null;
1478
                if (!is_object($this->statement)) {
1479
                    if (is_resource($value) || $type == 'clob' || $type == 'blob') {
1480
                        if (!is_resource($value) && preg_match('/^(\w+:\/\/)(.*)$/', $value, $match)) {
1481
                            if ($match[1] == 'file://') {
1482
                                $value = $match[2];
1483
                            }
1484
                            $value = @fopen($value, 'r');
1485
                            $close = true;
1486
                        }
1487
                        if (is_resource($value)) {
1488
                            $data = '';
1489
                            while (!@feof($value)) {
1490
                                $data.= @fread($value, $this->db->options['lob_buffer_length']);
1491
                            }
1492
                            if ($close) {
1493
                                @fclose($value);
1494
                            }
1495
                            $value = $data;
1496
                        }
1497
                    }
1498
                    $quoted = $this->db->quote($value, $type);
1499
                    if (PEAR::isError($quoted)) {
1500
                        return $quoted;
1501
                    }
1502
                    $param_query = 'SET @'.$parameter.' = '.$quoted;
1503
                    $result = $this->db->_doQuery($param_query, true, $connection);
1504
                    if (PEAR::isError($result)) {
1505
                        return $result;
1506
                    }
1507
                } else {
1508
                    if (is_resource($value) || $type == 'clob' || $type == 'blob') {
1509
                        $parameters[] = null;
1510
                        $parameters[1].= 'b';
1511
                        $lobs[$i] = $parameter;
1512
                    } else {
1513
                        $parameters[] = $this->db->quote($value, $type, false);
1514
                        $parameters[1].= $this->db->datatype->mapPrepareDatatype($type);
1515
                    }
1516
                    ++$i;
1517
                }
1518
            }
1519
 
1520
            if (!is_object($this->statement)) {
1521
                $query.= ' USING @'.implode(', @', array_values($this->positions));
1522
            } else {
1523
                $result = @call_user_func_array('mysqli_stmt_bind_param', $parameters);
1524
                if ($result === false) {
1525
                    $err =& $this->db->raiseError(null, null, null,
1526
                        'Unable to bind parameters', __FUNCTION__);
1527
                    return $err;
1528
                }
1529
 
1530
                foreach ($lobs as $i => $parameter) {
1531
                    $value = $this->values[$parameter];
1532
                    $close = false;
1533
                    if (!is_resource($value)) {
1534
                        $close = true;
1535
                        if (preg_match('/^(\w+:\/\/)(.*)$/', $value, $match)) {
1536
                            if ($match[1] == 'file://') {
1537
                                $value = $match[2];
1538
                            }
1539
                            $value = @fopen($value, 'r');
1540
                        } else {
1541
                            $fp = @tmpfile();
1542
                            @fwrite($fp, $value);
1543
                            @rewind($fp);
1544
                            $value = $fp;
1545
                        }
1546
                    }
1547
                    while (!@feof($value)) {
1548
                        $data = @fread($value, $this->db->options['lob_buffer_length']);
1549
                        @mysqli_stmt_send_long_data($this->statement, $i, $data);
1550
                    }
1551
                    if ($close) {
1552
                        @fclose($value);
1553
                    }
1554
                }
1555
            }
1556
        }
1557
 
1558
        if (!is_object($this->statement)) {
1559
            $result = $this->db->_doQuery($query, $this->is_manip, $connection);
1560
            if (PEAR::isError($result)) {
1561
                return $result;
1562
            }
1563
 
1564
            if ($this->is_manip) {
1565
                $affected_rows = $this->db->_affectedRows($connection, $result);
1566
                return $affected_rows;
1567
            }
1568
 
1569
            $result =& $this->db->_wrapResult($result, $this->result_types,
1570
                $result_class, $result_wrap_class, $this->limit, $this->offset);
1571
        } else {
1572
            if (!@mysqli_stmt_execute($this->statement)) {
1573
                $err =& $this->db->raiseError(null, null, null,
1574
                    'Unable to execute statement', __FUNCTION__);
1575
                return $err;
1576
            }
1577
 
1578
            if ($this->is_manip) {
1579
                $affected_rows = @mysqli_stmt_affected_rows($this->statement);
1580
                return $affected_rows;
1581
            }
1582
 
1583
            if ($this->db->options['result_buffering']) {
1584
                @mysqli_stmt_store_result($this->statement);
1585
            }
1586
 
1587
            $result =& $this->db->_wrapResult($this->statement, $this->result_types,
1588
                $result_class, $result_wrap_class, $this->limit, $this->offset);
1589
        }
1590
 
1591
        $this->db->debug($this->query, 'execute', array('is_manip' => $this->is_manip, 'when' => 'post', 'result' => $result));
1592
        return $result;
1593
    }
1594
 
1595
    // }}}
1596
    // {{{ free()
1597
 
1598
    /**
1599
     * Release resources allocated for the specified prepared query.
1600
     *
1601
     * @return mixed MDB2_OK on success, a MDB2 error on failure
1602
     * @access public
1603
     */
1604
    function free()
1605
    {
1606
        if (is_null($this->positions)) {
1607
            return $this->db->raiseError(MDB2_ERROR, null, null,
1608
                'Prepared statement has already been freed', __FUNCTION__);
1609
        }
1610
        $result = MDB2_OK;
1611
 
1612
        if (is_object($this->statement)) {
1613
            if (!@mysqli_stmt_close($this->statement)) {
1614
                $result = $this->db->raiseError(null, null, null,
1615
                    'Could not free statement', __FUNCTION__);
1616
            }
1617
        } elseif (!is_null($this->statement)) {
1618
            $connection = $this->db->getConnection();
1619
            if (PEAR::isError($connection)) {
1620
                return $connection;
1621
            }
1622
 
1623
            $query = 'DEALLOCATE PREPARE '.$this->statement;
1624
            $result = $this->db->_doQuery($query, true, $connection);
1625
        }
1626
 
1627
        parent::free();
1628
        return $result;
1629
   }
1630
}
1631
?>