source: aowow/includes/DbSimple/Generic.php

Last change on this file was 823, checked in by chronos, 10 years ago
  • Fixed: PHP SIGSEG because class declaration was after first usage of it.
  • Property svn:executable set to *
File size: 42.8 KB
Line 
1<?php
2
3
4/**
5 * DbSimple_Generic: universal database connected by DSN.
6 * (C) Dk Lab, http://en.dklab.ru
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
12 * See http://www.gnu.org/copyleft/lesser.html
13 *
14 * Use static DbSimple_Generic::connect($dsn) call if you don't know
15 * database type and parameters, but have its DSN.
16 *
17 * Additional keys can be added by appending a URI query string to the
18 * end of the DSN.
19 *
20 * The format of the supplied DSN is in its fullest form:
21 * phptype(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
22 *
23 * Most variations are allowed:
24 * phptype://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
25 * phptype://username:password@hostspec/database_name
26 * phptype://username:password@hostspec
27 * phptype://username@hostspec
28 * phptype://hostspec/database
29 * phptype://hostspec
30 * phptype(dbsyntax)
31 * phptype
32 *
33 * Parsing code is partially grabbed from PEAR DB class,
34 * initial author: Tomas V.V.Cox <cox@idecnet.com>.
35 *
36 * Contains 3 classes:
37 * - DbSimple_Generic: database factory class
38 * - DbSimple_Generic_Database: common database methods
39 * - DbSimple_Generic_Blob: common BLOB support
40 * - DbSimple_Generic_LastError: error reporting and tracking
41 *
42 * Special result-set fields:
43 * - ARRAY_KEY* ("*" means "anything")
44 * - PARENT_KEY
45 *
46 * Transforms:
47 * - GET_ATTRIBUTES
48 * - CALC_TOTAL
49 * - GET_TOTAL
50 * - UNIQ_KEY
51 *
52 * Query attributes:
53 * - BLOB_OBJ
54 * - CACHE
55 *
56 * @author Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
57 * @author Konstantin Zhinko, http://forum.dklab.ru/users/KonstantinGinkoTit/
58 *
59 * @version 2.x $Id: Generic.php 226 2007-09-17 21:00:15Z dk $
60 */
61
62/**
63 * Use this constant as placeholder value to skip optional SQL block [...].
64 */
65define('DBSIMPLE_SKIP', log(0));
66
67/**
68 * Names of special columns in result-set which is used
69 * as array key (or karent key in forest-based resultsets) in
70 * resulting hash.
71 */
72define('DBSIMPLE_ARRAY_KEY', 'ARRAY_KEY'); // hash-based resultset support
73define('DBSIMPLE_PARENT_KEY', 'PARENT_KEY'); // forrest-based resultset support
74
75
76/**
77 * DbSimple factory.
78 */
79class DbSimple_Generic
80{
81 /**
82 * DbSimple_Generic connect(mixed $dsn)
83 *
84 * Universal static function to connect ANY database using DSN syntax.
85 * Choose database driver according to DSN. Return new instance
86 * of this driver.
87 */
88 function& connect($dsn)
89 {
90 // Load database driver and create its instance.
91 $parsed = DbSimple_Generic::parseDSN($dsn);
92 if (!$parsed) {
93 $dummy = null;
94 return $dummy;
95 }
96 $class = 'DbSimple_'.ucfirst($parsed['scheme']);
97 if (!class_exists($class)) {
98 $file = str_replace('_', '/', $class) . ".php";
99 // Try to load library file from standard include_path.
100 if ($f = @fopen($file, "r", true)) {
101 fclose($f);
102 require_once($file);
103 } else {
104 // Wrong include_path; try to load from current directory.
105 $base = basename($file);
106 $dir = dirname(__FILE__);
107 if (@is_file($path = "$dir/$base")) {
108 require_once($path);
109 } else {
110 trigger_error("Error loading database driver: no file $file in include_path; no file $base in $dir", E_USER_ERROR);
111 return null;
112 }
113 }
114 }
115 $object =& new $class($parsed);
116 if (isset($parsed['ident_prefix'])) {
117 $object->setIdentPrefix($parsed['ident_prefix']);
118 }
119 $object->setCachePrefix(md5(serialize($parsed['dsn'])));
120 if (@fopen('Cache/Lite.php', 'r', true)) {
121 $tmp_dirs = array(
122 ini_get('session.save_path'),
123 getenv("TEMP"),
124 getenv("TMP"),
125 getenv("TMPDIR"),
126 '/tmp'
127 );
128 foreach ($tmp_dirs as $dir) {
129 if (!$dir) continue;
130 $fp = @fopen($testFile = $dir . '/DbSimple_' . md5(getmypid() . microtime()), 'w');
131 if ($fp) {
132 fclose($fp);
133 unlink($testFile);
134 require_once 'Cache' . '/Lite.php'; // "." -> no phpEclipse notice
135 $t =& new Cache_Lite(array('cacheDir' => $dir.'/', 'lifeTime' => null, 'automaticSerialization' => true));
136 $object->_cacher =& $t;
137 break;
138 }
139
140 }
141 }
142 return $object;
143 }
144
145
146 /**
147 * array parseDSN(mixed $dsn)
148 * Parse a data source name.
149 * See parse_url() for details.
150 */
151 function parseDSN($dsn)
152 {
153 if (is_array($dsn)) return $dsn;
154 $parsed = @parse_url($dsn);
155 if (!$parsed) return null;
156 $params = null;
157 if (!empty($parsed['query'])) {
158 parse_str($parsed['query'], $params);
159 $parsed += $params;
160 }
161 $parsed['dsn'] = $dsn;
162 return $parsed;
163 }
164}
165
166/**
167 * Support for error tracking.
168 * Can hold error messages, error queries and build proper stacktraces.
169 */
170class DbSimple_Generic_LastError
171{
172 var $error = null;
173 var $errmsg = null;
174 var $errorHandler = null;
175 var $ignoresInTraceRe = 'DbSimple_.*::.* | call_user_func.*';
176
177 /**
178 * abstract void _logQuery($query)
179 * Must be overriden in derived class.
180 */
181 function _logQuery($query)
182 {
183 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);;
184 }
185
186 /**
187 * void _resetLastError()
188 * Reset the last error. Must be called on correct queries.
189 */
190 function _resetLastError()
191 {
192 $this->error = $this->errmsg = null;
193 }
194
195 /**
196 * void _setLastError(int $code, string $message, string $query)
197 * Fill $this->error property with error information. Error context
198 * (code initiated the query outside DbSimple) is assigned automatically.
199 */
200 function _setLastError($code, $msg, $query)
201 {
202 $context = "unknown";
203 if ($t = $this->findLibraryCaller()) {
204 $context = (isset($t['file'])? $t['file'] : '?') . ' line ' . (isset($t['line'])? $t['line'] : '?');
205 }
206 $this->error = array(
207 'code' => $code,
208 'message' => rtrim($msg),
209 'query' => $query,
210 'context' => $context,
211 );
212 $this->errmsg = rtrim($msg) . ($context? " at $context" : "");
213
214 $this->_logQuery(" -- error #".$code.": ".preg_replace('/(\r?\n)+/s', ' ', $this->errmsg));
215
216 if (is_callable($this->errorHandler)) {
217 call_user_func($this->errorHandler, $this->errmsg, $this->error);
218 }
219
220 return null;
221 }
222
223
224 /**
225 * callback setErrorHandler(callback $handler)
226 * Set new error handler called on database errors.
227 * Handler gets 3 arguments:
228 * - error message
229 * - full error context information (last query etc.)
230 */
231 function setErrorHandler($handler)
232 {
233 $prev = $this->errorHandler;
234 $this->errorHandler = $handler;
235 // In case of setting first error handler for already existed
236 // error - call the handler now (usual after connect()).
237 if (!$prev && $this->error) {
238 call_user_func($this->errorHandler, $this->errmsg, $this->error);
239 }
240 return $prev;
241 }
242
243 /**
244 * void addIgnoreInTrace($reName)
245 * Add regular expression matching ClassName::functionName or functionName.
246 * Matched stack frames will be ignored in stack traces passed to query logger.
247 */
248 function addIgnoreInTrace($name)
249 {
250 $this->ignoresInTraceRe .= "|" . $name;
251 }
252
253 /**
254 * array of array findLibraryCaller()
255 * Return part of stacktrace before calling first library method.
256 * Used in debug purposes (query logging etc.).
257 */
258 function findLibraryCaller()
259 {
260 $caller = call_user_func(
261 array(&$this, 'debug_backtrace_smart'),
262 $this->ignoresInTraceRe,
263 true
264 );
265 return $caller;
266 }
267
268 /**
269 * array debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
270 *
271 * Return stacktrace. Correctly work with call_user_func*
272 * (totally skip them correcting caller references).
273 * If $returnCaller is true, return only first matched caller,
274 * not all stacktrace.
275 *
276 * @version 2.03
277 */
278 function debug_backtrace_smart($ignoresRe=null, $returnCaller=false)
279 {
280 if (!is_callable($tracer='debug_backtrace')) return array();
281 $trace = $tracer();
282
283 if ($ignoresRe !== null) $ignoresRe = "/^(?>{$ignoresRe})$/six";
284 $smart = array();
285 $framesSeen = 0;
286 for ($i=0, $n=count($trace); $i<$n; $i++) {
287 $t = $trace[$i];
288 if (!$t) continue;
289
290 // Next frame.
291 $next = isset($trace[$i+1])? $trace[$i+1] : null;
292
293 // Dummy frame before call_user_func* frames.
294 if (!isset($t['file'])) {
295 $t['over_function'] = $trace[$i+1]['function'];
296 $t = $t + $trace[$i+1];
297 $trace[$i+1] = null; // skip call_user_func on next iteration
298 }
299
300 // Skip myself frame.
301 if (++$framesSeen < 2) continue;
302
303 // 'class' and 'function' field of next frame define where
304 // this frame function situated. Skip frames for functions
305 // situated in ignored places.
306 if ($ignoresRe && $next) {
307 // Name of function "inside which" frame was generated.
308 $frameCaller = (isset($next['class'])? $next['class'].'::' : '') . (isset($next['function'])? $next['function'] : '');
309 if (preg_match($ignoresRe, $frameCaller)) continue;
310 }
311
312 // On each iteration we consider ability to add PREVIOUS frame
313 // to $smart stack.
314 if ($returnCaller) return $t;
315 $smart[] = $t;
316 }
317 return $smart;
318 }
319
320}
321
322/**
323 * Base class for all databases.
324 * Can create transactions and new BLOBs, parse DSNs.
325 *
326 * Logger is COMMON for multiple transactions.
327 * Error handler is private for each transaction and database.
328 */
329class DbSimple_Generic_Database extends DbSimple_Generic_LastError
330{
331 /**
332 * Public methods.
333 */
334
335 /**
336 * object blob($blob_id)
337 * Create new blob
338 */
339 function blob($blob_id = null)
340 {
341 $this->_resetLastError();
342 return $this->_performNewBlob($blob_id);
343 }
344
345 /**
346 * void transaction($mode)
347 * Create new transaction.
348 */
349 function transaction($mode=null)
350 {
351 $this->_resetLastError();
352 $this->_logQuery('-- START TRANSACTION '.$mode);
353 return $this->_performTransaction($mode);
354 }
355
356 /**
357 * mixed commit()
358 * Commit the transaction.
359 */
360 function commit()
361 {
362 $this->_resetLastError();
363 $this->_logQuery('-- COMMIT');
364 return $this->_performCommit();
365 }
366
367 /**
368 * mixed rollback()
369 * Rollback the transaction.
370 */
371 function rollback()
372 {
373 $this->_resetLastError();
374 $this->_logQuery('-- ROLLBACK');
375 return $this->_performRollback();
376 }
377
378 /**
379 * mixed select(string $query [, $arg1] [,$arg2] ...)
380 * Execute query and return the result.
381 */
382 function select($query)
383 {
384 $args = func_get_args();
385 $total = false;
386 return $this->_query($args, $total);
387 }
388
389 /**
390 * mixed selectPage(int &$total, string $query [, $arg1] [,$arg2] ...)
391 * Execute query and return the result.
392 * Total number of found rows (independent to LIMIT) is returned in $total
393 * (in most cases second query is performed to calculate $total).
394 */
395 function selectPage(&$total, $query)
396 {
397 $args = func_get_args();
398 array_shift($args);
399 $total = true;
400 return $this->_query($args, $total);
401 }
402
403 /**
404 * hash selectRow(string $query [, $arg1] [,$arg2] ...)
405 * Return the first row of query result.
406 * On errors return null and set last error.
407 * If no one row found, return array()! It is useful while debugging,
408 * because PHP DOES NOT generates notice on $row['abc'] if $row === null
409 * or $row === false (but, if $row is empty array, notice is generated).
410 */
411 function selectRow()
412 {
413 $args = func_get_args();
414 $total = false;
415 $rows = $this->_query($args, $total);
416 if (!is_array($rows)) return $rows;
417 if (!count($rows)) return array();
418 reset($rows);
419 return current($rows);
420 }
421
422 /**
423 * array selectCol(string $query [, $arg1] [,$arg2] ...)
424 * Return the first column of query result as array.
425 */
426 function selectCol()
427 {
428 $args = func_get_args();
429 $total = false;
430 $rows = $this->_query($args, $total);
431 if (!is_array($rows)) return $rows;
432 $this->_shrinkLastArrayDimensionCallback($rows);
433 return $rows;
434 }
435
436 /**
437 * scalar selectCell(string $query [, $arg1] [,$arg2] ...)
438 * Return the first cell of the first column of query result.
439 * If no one row selected, return null.
440 */
441 function selectCell()
442 {
443 $args = func_get_args();
444 $total = false;
445 $rows = $this->_query($args, $total);
446 if (!is_array($rows)) return $rows;
447 if (!count($rows)) return null;
448 reset($rows);
449 $row = current($rows);
450 if (!is_array($row)) return $row;
451 reset($row);
452 return current($row);
453 }
454
455 /**
456 * mixed query(string $query [, $arg1] [,$arg2] ...)
457 * Alias for select(). May be used for INSERT or UPDATE queries.
458 */
459 function query()
460 {
461 $args = func_get_args();
462 $total = false;
463 return $this->_query($args, $total);
464 }
465
466 /**
467 * string escape(mixed $s, bool $isIdent=false)
468 * Enclose the string into database quotes correctly escaping
469 * special characters. If $isIdent is true, value quoted as identifier
470 * (e.g.: `value` in MySQL, "value" in Firebird, [value] in MSSQL).
471 */
472 function escape($s, $isIdent=false)
473 {
474 return $this->_performEscape($s, $isIdent);
475 }
476
477
478 /**
479 * callback setLogger(callback $logger)
480 * Set query logger called before each query is executed.
481 * Returns previous logger.
482 */
483 function setLogger($logger)
484 {
485 $prev = $this->_logger;
486 $this->_logger = $logger;
487 return $prev;
488 }
489
490 /**
491 * callback setCacher(callback $cacher)
492 * Set cache mechanism called during each query if specified.
493 * Returns previous handler.
494 */
495 function setCacher($cacher)
496 {
497 $prev = $this->_cacher;
498 $this->_cacher = $cacher;
499 return $prev;
500 }
501
502 /**
503 * string setIdentPrefix($prx)
504 * Set identifier prefix used for $_ placeholder.
505 */
506 function setIdentPrefix($prx)
507 {
508 $old = $this->_identPrefix;
509 if ($prx !== null) $this->_identPrefix = $prx;
510 return $old;
511 }
512
513 /**
514 * string setIdentPrefix($prx)
515 * Set cache prefix used in key caclulation.
516 */
517 function setCachePrefix($prx)
518 {
519 $old = $this->_cachePrefix;
520 if ($prx !== null) $this->_cachePrefix = $prx;
521 return $old;
522 }
523
524 /**
525 * array getStatistics()
526 * Returns various statistical information.
527 */
528 function getStatistics()
529 {
530 return $this->_statistics;
531 }
532
533
534 /**
535 * Virtual protected methods
536 */
537 function ____________PROTECTED() {} // for phpEclipse outline
538
539
540 /**
541 * string _performEscape(mixed $s, bool $isIdent=false)
542 */
543 function _performEscape($s, $isIdent)
544 {
545 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
546 }
547
548 /**
549 * object _performNewBlob($id)
550 *
551 * Returns new blob object.
552 */
553 function& _performNewBlob($id)
554 {
555 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
556 }
557
558 /**
559 * list _performGetBlobFieldNames($resultResource)
560 * Get list of all BLOB field names in result-set.
561 */
562 function _performGetBlobFieldNames($result)
563 {
564 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
565 }
566
567 /**
568 * mixed _performTransformQuery(array &$query, string $how)
569 *
570 * Transform query different way specified by $how.
571 * May return some information about performed transform.
572 */
573 function _performTransformQuery(&$queryMain, $how)
574 {
575 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
576 }
577
578
579 /**
580 * resource _performQuery($arrayQuery)
581 * Must return:
582 * - For SELECT queries: ID of result-set (PHP resource).
583 * - For other queries: query status (scalar).
584 * - For error queries: null (and call _setLastError()).
585 */
586 function _performQuery($arrayQuery)
587 {
588 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
589 }
590
591 /**
592 * mixed _performFetch($resultResource)
593 * Fetch ONE NEXT row from result-set.
594 * Must return:
595 * - For SELECT queries: all the rows of the query (2d arrray).
596 * - For INSERT queries: ID of inserted row.
597 * - For UPDATE queries: number of updated rows.
598 * - For other queries: query status (scalar).
599 * - For error queries: null (and call _setLastError()).
600 */
601 function _performFetch($result)
602 {
603 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
604 }
605
606 /**
607 * array _performTotal($arrayQuery)
608 */
609 function _performTotal($arrayQuery)
610 {
611 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
612 }
613
614 /**
615 * mixed _performTransaction($mode)
616 * Start new transaction.
617 */
618 function _performTransaction($mode=null)
619 {
620 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
621 }
622
623 /**
624 * mixed _performCommit()
625 * Commit the transaction.
626 */
627 function _performCommit()
628 {
629 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
630 }
631
632 /**
633 * mixed _performRollback()
634 * Rollback the transaction.
635 */
636 function _performRollback()
637 {
638 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
639 }
640
641 /**
642 * string _performGetPlaceholderIgnoreRe()
643 * Return regular expression which matches ignored query parts.
644 * This is needed to skip placeholder replacement inside comments, constants etc.
645 */
646 function _performGetPlaceholderIgnoreRe()
647 {
648 return '';
649 }
650
651 /**
652 * Returns marker for native database placeholder. E.g. in FireBird it is '?',
653 * in PostgreSQL - '$1', '$2' etc.
654 *
655 * @param int $n Number of native placeholder from the beginning of the query (begins from 0!).
656 * @return string String representation of native placeholder marker (by default - '?').
657 */
658 function _performGetNativePlaceholderMarker($n)
659 {
660 return '?';
661 }
662
663
664 /**
665 * Private methods.
666 */
667 function ____________PRIVATE() {} // for phpEclipse outline
668
669
670 /**
671 * array _query($query, &$total)
672 * See _performQuery().
673 */
674 function _query($query, &$total)
675 {
676 $this->_resetLastError();
677
678 // Fetch query attributes.
679 $this->attributes = $this->_transformQuery($query, 'GET_ATTRIBUTES');
680
681 // Modify query if needed for total counting.
682 if ($total) {
683 $this->_transformQuery($query, 'CALC_TOTAL');
684 }
685 $is_cacher_callable = (is_callable($this->_cacher) || (method_exists($this->_cacher, 'get') && method_exists($this->_cacher, 'save')));
686 $rows = null;
687 $cache_it = false;
688 if (!empty($this->attributes['CACHE']) && $is_cacher_callable) {
689
690 $hash = $this->_cachePrefix . md5(serialize($query));
691 // Getting data from cache if possible
692 $fetchTime = $firstFetchTime = 0;
693 $qStart = $this->_microtime();
694 $cacheData = $this->_cache($hash);
695 $queryTime = $this->_microtime() - $qStart;
696
697 $storeTime = isset($cacheData['storeTime']) ? $cacheData['storeTime'] : null;
698 $invalCache = isset($cacheData['invalCache']) ? $cacheData['invalCache'] : null;
699 $result = isset($cacheData['result']) ? $cacheData['result'] : null;
700 $rows = isset($cacheData['rows']) ? $cacheData['rows'] : null;
701
702
703 $cache_params = $this->attributes['CACHE'];
704
705 // Calculating cache time to live
706 $re = '/
707 (
708 ([0-9]+) #2 - hours
709 h)? [ \t]*
710 (
711 ([0-9]+) #4 - minutes
712 m)? [ \t]*
713 (
714 ([0-9]+) #6 - seconds
715 s?)? (,)?
716 /sx';
717 $m = null;
718 preg_match($re, $cache_params, $m);
719 $ttl = @$m[6] + @$m[4] * 60 + @$m[2] * 3600;
720 // Cutting out time param - now there are just fields for uniqKey or nothing
721 $cache_params = trim(preg_replace($re, '', $cache_params, 1));
722
723 $uniq_key = null;
724
725 // UNIQ_KEY calculation
726 if (!empty($cache_params)) {
727 $dummy = null;
728 // There is no need in query, cos' needle in $this->attributes['CACHE']
729 $this->_transformQuery($dummy, 'UNIQ_KEY');
730 $uniq_key = call_user_func_array(array(&$this, 'select'), $dummy);
731 $uniq_key = md5(serialize($uniq_key));
732 }
733 // Check TTL?
734 $ttl = empty($ttl) ? true : (int)$storeTime > (time() - $ttl);
735
736 // Invalidate cache?
737 if ($ttl && $uniq_key == $invalCache) {
738 $this->_logQuery($query);
739 $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows);
740
741 }
742 else $cache_it = true;
743 }
744
745 if (null === $rows || true === $cache_it) {
746 $this->_logQuery($query);
747
748 // Run the query (counting time).
749 $qStart = $this->_microtime();
750 $result = $this->_performQuery($query);
751 $fetchTime = $firstFetchTime = 0;
752
753 if (is_resource($result)) {
754 $rows = array();
755 // Fetch result row by row.
756 $fStart = $this->_microtime();
757 $row = $this->_performFetch($result);
758 $firstFetchTime = $this->_microtime() - $fStart;
759 if ($row !== null) {
760 $rows[] = $row;
761 while ($row=$this->_performFetch($result)) {
762 $rows[] = $row;
763 }
764 }
765 $fetchTime = $this->_microtime() - $fStart;
766 } else {
767 $rows = $result;
768 }
769 $queryTime = $this->_microtime() - $qStart;
770
771 // Log query statistics.
772 $this->_logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows);
773
774 // Prepare BLOB objects if needed.
775 if (is_array($rows) && !empty($this->attributes['BLOB_OBJ'])) {
776 $blobFieldNames = $this->_performGetBlobFieldNames($result);
777 foreach ($blobFieldNames as $name) {
778 for ($r = count($rows)-1; $r>=0; $r--) {
779 $rows[$r][$name] =& $this->_performNewBlob($rows[$r][$name]);
780 }
781 }
782 }
783
784 // Transform resulting rows.
785 $result = $this->_transformResult($rows);
786
787 // Storing data in cache
788 if ($cache_it && $is_cacher_callable) {
789 $this->_cache(
790 $hash,
791 array(
792 'storeTime' => time(),
793 'invalCache' => $uniq_key,
794 'result' => $result,
795 'rows' => $rows
796 )
797 );
798 }
799
800 }
801 // Count total number of rows if needed.
802 if (is_array($result) && $total) {
803 $this->_transformQuery($query, 'GET_TOTAL');
804 $total = call_user_func_array(array(&$this, 'selectCell'), $query);
805 }
806
807 return $result;
808 }
809
810
811 /**
812 * mixed _transformQuery(array &$query, string $how)
813 *
814 * Transform query different way specified by $how.
815 * May return some information about performed transform.
816 */
817 function _transformQuery(&$query, $how)
818 {
819 // Do overriden transformation.
820 $result = $this->_performTransformQuery($query, $how);
821 if ($result === true) return $result;
822 // Common transformations.
823 switch ($how) {
824 case 'GET_ATTRIBUTES':
825 // Extract query attributes.
826 $options = array();
827 $q = $query[0];
828 $m = null;
829 while (preg_match('/^ \s* -- [ \t]+ (\w+): ([^\r\n]+) [\r\n]* /sx', $q, $m)) {
830 $options[$m[1]] = trim($m[2]);
831 $q = substr($q, strlen($m[0]));
832 }
833 return $options;
834 case 'UNIQ_KEY':
835 $q = $this->attributes['CACHE'];
836 $i = 0;
837 $query = " -- UNIQ_KEY\n";
838 while(preg_match('/(\w+)\.\w+/sx', $q, $m)) {
839 if($i > 0)$query .= "\nUNION\n";
840 $query .= 'SELECT MAX('.$m[0].') AS M, COUNT(*) AS C FROM '.$m[1];
841 $q = substr($q, strlen($m[0]));
842 $i++;
843 }
844 return true;
845 }
846 // No such transform.
847 $this->_setLastError(-1, "No such transform type: $how", $query);
848 }
849
850
851 /**
852 * void _expandPlaceholders(array &$queryAndArgs, bool $useNative=false)
853 * Replace placeholders by quoted values.
854 * Modify $queryAndArgs.
855 */
856 function _expandPlaceholders(&$queryAndArgs, $useNative=false)
857 {
858 $cacheCode = null;
859 if ($this->_logger) {
860 // Serialize is much faster than placeholder expansion. So use caching.
861 $cacheCode = md5(serialize($queryAndArgs) . '|' . $useNative . '|' . $this->_identPrefix);
862 if (isset($this->_placeholderCache[$cacheCode])) {
863 $queryAndArgs = $this->_placeholderCache[$cacheCode];
864 return;
865 }
866 }
867
868 if (!is_array($queryAndArgs)) {
869 $queryAndArgs = array($queryAndArgs);
870 }
871
872 $this->_placeholderNativeArgs = $useNative? array() : null;
873 $this->_placeholderArgs = array_reverse($queryAndArgs);
874
875 $query = array_pop($this->_placeholderArgs); // array_pop is faster than array_shift
876
877 // Do all the work.
878 $this->_placeholderNoValueFound = false;
879 $query = $this->_expandPlaceholdersFlow($query);
880
881 if ($useNative) {
882 array_unshift($this->_placeholderNativeArgs, $query);
883 $queryAndArgs = $this->_placeholderNativeArgs;
884 } else {
885 $queryAndArgs = array($query);
886 }
887
888 if ($cacheCode) {
889 $this->_placeholderCache[$cacheCode] = $queryAndArgs;
890 }
891 }
892
893
894 /**
895 * Do real placeholder processing.
896 * Imply that all interval variables (_placeholder_*) already prepared.
897 * May be called recurrent!
898 */
899 function _expandPlaceholdersFlow($query)
900 {
901 $re = '{
902 (?>
903 # Ignored chunks.
904 (?>
905 # Comment.
906 -- [^\r\n]*
907 )
908 |
909 (?>
910 # DB-specifics.
911 ' . trim($this->_performGetPlaceholderIgnoreRe()) . '
912 )
913 )
914 |
915 (?>
916 # Optional blocks
917 \{
918 # Use "+" here, not "*"! Else nested blocks are not processed well.
919 ( (?> (?>[^{}]+) | (?R) )* ) #1
920 \}
921 )
922 |
923 (?>
924 # Placeholder
925 (\?) ( [_dsafn\#]? ) #2 #3
926 )
927 }sx';
928 $query = preg_replace_callback(
929 $re,
930 array(&$this, '_expandPlaceholdersCallback'),
931 $query
932 );
933 return $query;
934 }
935
936
937 /**
938 * string _expandPlaceholdersCallback(list $m)
939 * Internal function to replace placeholders (see preg_replace_callback).
940 */
941 function _expandPlaceholdersCallback($m)
942 {
943 // Placeholder.
944 if (!empty($m[2])) {
945 $type = $m[3];
946
947 // Idenifier prefix.
948 if ($type == '_') {
949 return $this->_identPrefix;
950 }
951
952 // Value-based placeholder.
953 if (!$this->_placeholderArgs) return 'DBSIMPLE_ERROR_NO_VALUE';
954 $value = array_pop($this->_placeholderArgs);
955
956 // Skip this value?
957 if ($value === DBSIMPLE_SKIP) {
958 $this->_placeholderNoValueFound = true;
959 return '';
960 }
961
962 // First process guaranteed non-native placeholders.
963 switch ($type) {
964 case 'a':
965 if (!$value) $this->_placeholderNoValueFound = true;
966 if (!is_array($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_ARRAY';
967 $parts = array();
968 foreach ($value as $k=>$v) {
969 $v = $v === null? 'NULL' : $this->escape($v);
970 if (!is_int($k)) {
971 $k = $this->escape($k, true);
972 $parts[] = "$k=$v";
973 } else {
974 $parts[] = $v;
975 }
976 }
977 return join(', ', $parts);
978 case "#":
979 // Identifier.
980 if (!is_array($value)) return $this->escape($value, true);
981 $parts = array();
982 foreach ($value as $table => $identifier) {
983 if (!is_string($identifier)) return 'DBSIMPLE_ERROR_ARRAY_VALUE_NOT_STRING';
984 $parts[] = (!is_int($table)? $this->escape($table, true) . '.' : '') . $this->escape($identifier, true);
985 }
986 return join(', ', $parts);
987 case 'n':
988 // NULL-based placeholder.
989 return empty($value)? 'NULL' : intval($value);
990 }
991
992 // Native arguments are not processed.
993 if ($this->_placeholderNativeArgs !== null) {
994 $this->_placeholderNativeArgs[] = $value;
995 return $this->_performGetNativePlaceholderMarker(count($this->_placeholderNativeArgs) - 1);
996 }
997
998 // In non-native mode arguments are quoted.
999 if ($value === null) return 'NULL';
1000 switch ($type) {
1001 case '':
1002 if (!is_scalar($value)) return 'DBSIMPLE_ERROR_VALUE_NOT_SCALAR';
1003 return $this->escape($value);
1004 case 'd':
1005 return intval($value);
1006 case 'f':
1007 return str_replace(',', '.', floatval($value));
1008 }
1009 // By default - escape as string.
1010 return $this->escape($value);
1011 }
1012
1013 // Optional block.
1014 if (isset($m[1]) && strlen($block=$m[1])) {
1015 $prev = @$this->_placeholderNoValueFound;
1016 $block = $this->_expandPlaceholdersFlow($block);
1017 $block = $this->_placeholderNoValueFound? '' : ' ' . $block . ' ';
1018 $this->_placeholderNoValueFound = $prev; // recurrent-safe
1019 return $block;
1020 }
1021
1022 // Default: skipped part of the string.
1023 return $m[0];
1024 }
1025
1026
1027 /**
1028 * void _setLastError($code, $msg, $query)
1029 * Set last database error context.
1030 * Aditionally expand placeholders.
1031 */
1032 function _setLastError($code, $msg, $query)
1033 {
1034 if (is_array($query)) {
1035 $this->_expandPlaceholders($query, false);
1036 $query = $query[0];
1037 }
1038 return DbSimple_Generic_LastError::_setLastError($code, $msg, $query);
1039 }
1040
1041
1042 /**
1043 * Return microtime as float value.
1044 */
1045 function _microtime()
1046 {
1047 $t = explode(" ", microtime());
1048 return $t[0] + $t[1];
1049 }
1050
1051
1052 /**
1053 * Convert SQL field-list to COUNT(...) clause
1054 * (e.g. 'DISTINCT a AS aa, b AS bb' -> 'COUNT(DISTINCT a, b)').
1055 */
1056 function _fieldList2Count($fields)
1057 {
1058 $m = null;
1059 if (preg_match('/^\s* DISTINCT \s* (.*)/sx', $fields, $m)) {
1060 $fields = $m[1];
1061 $fields = preg_replace('/\s+ AS \s+ .*? (?=,|$)/sx', '', $fields);
1062 return "COUNT(DISTINCT $fields)";
1063 } else {
1064 return 'COUNT(*)';
1065 }
1066 }
1067
1068
1069 /**
1070 * array _transformResult(list $rows)
1071 * Transform resulting rows to various formats.
1072 */
1073 function _transformResult($rows)
1074 {
1075 // Process ARRAY_KEY feature.
1076 if (is_array($rows) && $rows) {
1077 // Find ARRAY_KEY* AND PARENT_KEY fields in field list.
1078 $pk = null;
1079 $ak = array();
1080 foreach (current($rows) as $fieldName => $dummy) {
1081 if (0 == strncasecmp($fieldName, DBSIMPLE_ARRAY_KEY, strlen(DBSIMPLE_ARRAY_KEY))) {
1082 $ak[] = $fieldName;
1083 } else if (0 == strncasecmp($fieldName, DBSIMPLE_PARENT_KEY, strlen(DBSIMPLE_PARENT_KEY))) {
1084 $pk = $fieldName;
1085 }
1086 }
1087 natsort($ak); // sort ARRAY_KEY* using natural comparision
1088
1089 if ($ak) {
1090 // Tree-based array? Fields: ARRAY_KEY, PARENT_KEY
1091 if ($pk !== null) {
1092 return $this->_transformResultToForest($rows, $ak[0], $pk);
1093 }
1094 // Key-based array? Fields: ARRAY_KEY.
1095 return $this->_transformResultToHash($rows, $ak);
1096 }
1097 }
1098 return $rows;
1099 }
1100
1101
1102 /**
1103 * Converts rowset to key-based array.
1104 *
1105 * @param array $rows Two-dimensional array of resulting rows.
1106 * @param array $ak List of ARRAY_KEY* field names.
1107 * @return array Transformed array.
1108 */
1109 function _transformResultToHash($rows, $arrayKeys)
1110 {
1111 $arrayKeys = (array)$arrayKeys;
1112 $result = array();
1113 foreach ($rows as $row) {
1114 // Iterate over all of ARRAY_KEY* fields and build array dimensions.
1115 $current =& $result;
1116 foreach ($arrayKeys as $ak) {
1117 $key = $row[$ak];
1118 unset($row[$ak]); // remove ARRAY_KEY* field from result row
1119 if ($key !== null) {
1120 $current =& $current[$key];
1121 } else {
1122 // IF ARRAY_KEY field === null, use array auto-indices.
1123 $tmp = array();
1124 $current[] =& $tmp;
1125 $current =& $tmp;
1126 unset($tmp); // we use tmp because don't know the value of auto-index
1127 }
1128 }
1129 $current = $row; // save the row in last dimension
1130 }
1131 return $result;
1132 }
1133
1134
1135 /**
1136 * Converts rowset to the forest.
1137 *
1138 * @param array $rows Two-dimensional array of resulting rows.
1139 * @param string $idName Name of ID field.
1140 * @param string $pidName Name of PARENT_ID field.
1141 * @return array Transformed array (tree).
1142 */
1143 function _transformResultToForest($rows, $idName, $pidName)
1144 {
1145 $children = array(); // children of each ID
1146 $ids = array();
1147 // Collect who are children of whom.
1148 foreach ($rows as $i=>$r) {
1149 $row =& $rows[$i];
1150 $id = $row[$idName];
1151 if ($id === null) {
1152 // Rows without an ID are totally invalid and makes the result tree to
1153 // be empty (because PARENT_ID = null means "a root of the tree"). So
1154 // skip them totally.
1155 continue;
1156 }
1157 $pid = $row[$pidName];
1158 if ($id == $pid) $pid = null;
1159 $children[$pid][$id] =& $row;
1160 if (!isset($children[$id])) $children[$id] = array();
1161 $row['childNodes'] =& $children[$id];
1162 $ids[$id] = true;
1163 }
1164 // Root elements are elements with non-found PIDs.
1165 $forest = array();
1166 foreach ($rows as $i=>$r) {
1167 $row =& $rows[$i];
1168 $id = $row[$idName];
1169 $pid = $row[$pidName];
1170 if ($pid == $id) $pid = null;
1171 if (!isset($ids[$pid])) {
1172 $forest[$row[$idName]] =& $row;
1173 }
1174 unset($row[$idName]);
1175 unset($row[$pidName]);
1176 }
1177 return $forest;
1178 }
1179
1180
1181 /**
1182 * Replaces the last array in a multi-dimensional array $V by its first value.
1183 * Used for selectCol(), when we need to transform (N+1)d resulting array
1184 * to Nd array (column).
1185 */
1186 function _shrinkLastArrayDimensionCallback(&$v)
1187 {
1188 if (!$v) return;
1189 reset($v);
1190 if (!is_array($firstCell = current($v))) {
1191 $v = $firstCell;
1192 } else {
1193 array_walk($v, array(&$this, '_shrinkLastArrayDimensionCallback'));
1194 }
1195 }
1196
1197 /**
1198 * void _logQuery($query, $noTrace=false)
1199 * Must be called on each query.
1200 * If $noTrace is true, library caller is not solved (speed improvement).
1201 */
1202 function _logQuery($query, $noTrace=false)
1203 {
1204 if (!$this->_logger) return;
1205 $this->_expandPlaceholders($query, false);
1206 $args = array();
1207 $args[] =& $this;
1208 $args[] = $query[0];
1209 $args[] = $noTrace? null : $this->findLibraryCaller();
1210 return call_user_func_array($this->_logger, $args);
1211 }
1212
1213
1214 /**
1215 * void _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows)
1216 * Log information about performed query statistics.
1217 */
1218 function _logQueryStat($queryTime, $fetchTime, $firstFetchTime, $rows)
1219 {
1220 // Always increment counters.
1221 $this->_statistics['time'] += $queryTime;
1222 $this->_statistics['count']++;
1223
1224 // If no logger, economize CPU resources and actually log nothing.
1225 if (!$this->_logger) return;
1226
1227 $dt = round($queryTime * 1000);
1228 $firstFetchTime = round($firstFetchTime*1000);
1229 $tailFetchTime = round($fetchTime * 1000) - $firstFetchTime;
1230 $log = " -- ";
1231 if ($firstFetchTime + $tailFetchTime) {
1232 $log = sprintf(" -- %d ms = %d+%d".($tailFetchTime? "+%d" : ""), $dt, $dt-$firstFetchTime-$tailFetchTime, $firstFetchTime, $tailFetchTime);
1233 } else {
1234 $log = sprintf(" -- %d ms", $dt);
1235 }
1236 $log .= "; returned ";
1237
1238 if (!is_array($rows)) {
1239 $log .= $this->escape($rows);
1240 } else {
1241 $detailed = null;
1242 if (count($rows) == 1) {
1243 $len = 0;
1244 $values = array();
1245 foreach ($rows[0] as $k=>$v) {
1246 $len += strlen($v);
1247 if ($len > $this->MAX_LOG_ROW_LEN) {
1248 break;
1249 }
1250 $values[] = $v === null? 'NULL' : $this->escape($v);
1251 }
1252 if ($len <= $this->MAX_LOG_ROW_LEN) {
1253 $detailed = "(" . preg_replace("/\r?\n/", "\\n", join(', ', $values)) . ")";
1254 }
1255 }
1256 if ($detailed) {
1257 $log .= $detailed;
1258 } else {
1259 $log .= count($rows). " row(s)";
1260 }
1261 }
1262
1263 $this->_logQuery($log, true);
1264 }
1265
1266 /**
1267 * mixed _cache($hash, $result=null)
1268 * Calls cache mechanism if possible.
1269 */
1270 function _cache($hash, $result=null)
1271 {
1272 if (is_callable($this->_cacher)) {
1273 return call_user_func($this->_cacher, $hash, $result);
1274 } else if (is_object($this->_cacher) && method_exists($this->_cacher, 'get') && method_exists($this->_cacher, 'save')) {
1275 if (null === $result)
1276 return $this->_cacher->get($hash);
1277 else
1278 $this->_cacher->save($result, $hash);
1279 }
1280 else return false;
1281 }
1282
1283
1284 /**
1285 * protected constructor(string $dsn)
1286 *
1287 * Prevent from direct creation of this object.
1288 */
1289 function DbSimple_Generic_Database()
1290 {
1291 die("This is protected constructor! Do not instantiate directly at ".__FILE__." line ".__LINE__);
1292 }
1293
1294 // Identifiers prefix (used for ?_ placeholder).
1295 var $_identPrefix = '';
1296
1297 // Queries statistics.
1298 var $_statistics = array(
1299 'time' => 0,
1300 'count' => 0,
1301 );
1302
1303 var $_cachePrefix = '';
1304
1305 var $_logger = null;
1306 var $_cacher = null;
1307 var $_placeholderArgs, $_placeholderNativeArgs, $_placeholderCache=array();
1308 var $_placeholderNoValueFound;
1309
1310 /**
1311 * When string representation of row (in characters) is greater than this,
1312 * row data will not be logged.
1313 */
1314 var $MAX_LOG_ROW_LEN = 128;
1315}
1316
1317/**
1318 * Database BLOB.
1319 * Can read blob chunk by chunk, write data to BLOB.
1320 */
1321class DbSimple_Generic_Blob extends DbSimple_Generic_LastError
1322{
1323 /**
1324 * string read(int $length)
1325 * Returns following $length bytes from the blob.
1326 */
1327 function read($len)
1328 {
1329 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1330 }
1331
1332 /**
1333 * string write($data)
1334 * Appends data to blob.
1335 */
1336 function write($data)
1337 {
1338 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1339 }
1340
1341 /**
1342 * int length()
1343 * Returns length of the blob.
1344 */
1345 function length()
1346 {
1347 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1348 }
1349
1350 /**
1351 * blobid close()
1352 * Closes the blob. Return its ID. No other way to obtain this ID!
1353 */
1354 function close()
1355 {
1356 die("Method must be defined in derived class. Abstract function called at ".__FILE__." line ".__LINE__);
1357 }
1358}
1359
1360
Note: See TracBrowser for help on using the repository browser.