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