~aw/ubuntu/lucid/davical/trunk

« back to all changes in this revision

Viewing changes to inc/PdoQuery.php

  • Committer: Bazaar Package Importer
  • Author(s): Andrew McMillan
  • Date: 2009-04-11 00:08:31 UTC
  • Revision ID: james.westby@ubuntu.com-20090411000831-zc97ye8hni3ozfzm
Tags: 0.9.6.3
* General bug fixing.
* Added Italian locale from Alessandro De Zorzi
* Adjust timezone handling in regression testing (Markus Warg).
* Many fixes to caldav-client from Michael Rasmussen.
* Update caldav-client to use direct socket IO from Andres Obrero.
* Added dummy handler for POST CANCEL events for iCal compatibilty (Wolfgang Herget).
* Support usernames with spaces or punctuation.
* Correct errors in handling confidential events.
* Improved response to GET for calendar with a single event.
* Documentation corrections.
* Remove the misguided hide_todo configuration option.
* Fix a bug in hiding alarms.
* Deprecate 'collections_always_exist' config option and restrict it's scope.
* Updated French translation.
* Updated relational integrity constraints.
* Fix database versioning.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/**
 
3
* PDO query class and associated functions
 
4
*
 
5
* This subpackage provides some functions that are useful around database
 
6
* activity and a PdoDialect, PdoDatabase and PdoQuery classes to simplify
 
7
* handling of database queries and provide some access for a limited
 
8
* ability to handle varying database dialects.
 
9
*
 
10
* The class is intended to be a very lightweight wrapper with some features
 
11
* that have proved useful in developing and debugging web-based applications:
 
12
*  - All queries are timed, and an expected time can be provided.
 
13
*  - Parameters replaced into the SQL will be escaped correctly in order to
 
14
*    minimise the chances of SQL injection errors.
 
15
*  - Queries which fail, or which exceed their expected execution time, will
 
16
*    be logged for potential further analysis.
 
17
*  - Debug logging of queries may be enabled globally, or restricted to
 
18
*    particular sets of queries.
 
19
*  - Simple syntax for iterating through a result set.
 
20
*
 
21
* See http://wiki.davical.org/w/PdoQuery for design and usage information.
 
22
*
 
23
* If not already connected, PdoQuery will attempt to connect to the database,
 
24
* successively applying connection parameters from the array in $c->pdo_connect.
 
25
*
 
26
* We will die if the database is not currently connected and we fail to find
 
27
* a working connection.
 
28
*
 
29
* @package   awl
 
30
* @subpackage   PdoQuery
 
31
* @author    Andrew McMillan <andrew@morphoss.com>
 
32
* @copyright Morphoss Ltd
 
33
* @license   http://gnu.org/copyleft/gpl.html GNU GPL v3
 
34
* @compatibility Requires PHP 5.1 or later
 
35
*/
 
36
 
 
37
/**
 
38
* The PdoDialect class handles
 
39
* @package awl
 
40
*/
 
41
class PdoDialect {
 
42
  /**#@+
 
43
  * @access private
 
44
  */
 
45
 
 
46
  /**
 
47
  * Holds the name of the database dialect
 
48
  */
 
49
  protected $dialect;
 
50
 
 
51
  /**#@-*/
 
52
 
 
53
 
 
54
  /**
 
55
  * Parses the connection string to ascertain the database dialect. Returns true if the dialect is supported
 
56
  * and fails if the dialect is not supported. All code to support any given database should be within in an
 
57
  * external include.
 
58
  * @param string $connection_string The full PDO connection string
 
59
  */
 
60
  function __construct( $connection_string ) {
 
61
    if ( preg_match( '/^(pgsql):/', $connection_string, $matches ) ) {
 
62
      $this->dialect = $matches[1];
 
63
    }
 
64
    else {
 
65
      trigger_error("Unsupported database connection '".$connection_string."'", E_USER_ERROR);
 
66
    }
 
67
  }
 
68
 
 
69
 
 
70
 
 
71
  /**
 
72
  * Returns the SQL for the current database dialect which will return a two-column resultset containing a
 
73
  * list of fields and their associated data types.
 
74
  * @param string $tablename_string The name of the table we want fields from
 
75
  */
 
76
  function GetFields( $tablename_string ) {
 
77
    if ( !isset($this->dialect) ) {
 
78
      trigger_error("Unsupported database dialect", E_USER_ERROR);
 
79
    }
 
80
 
 
81
    switch ( $this->dialect ) {
 
82
      case 'pgsql':
 
83
        $tablename_string = $this->Quote($tablename_string, 'identifier');
 
84
        $sql = "SELECT f.attname, t.typname FROM pg_attribute f ";
 
85
        $sql .= "JOIN pg_class c ON ( f.attrelid = c.oid ) ";
 
86
        $sql .= "JOIN pg_type t ON ( f.atttypid = t.oid ) ";
 
87
        $sql .= "WHERE relname = $tablename_string AND attnum >= 0 order by f.attnum;";
 
88
        return $sql;
 
89
    }
 
90
  }
 
91
 
 
92
 
 
93
  /**
 
94
  * Translates the given SQL string into a form that will hopefully work for this database dialect. This hook
 
95
  * is expected to be used by developers to provide support for differences in database operation by translating
 
96
  * the query string in an arbitrary way, such as through a file or database lookup.
 
97
  *
 
98
  * The actual translation to other SQL dialects will usually be application-specific, so that any routines
 
99
  * called by this will usually be external to this library, or will use resources external to this library.
 
100
  */
 
101
  function Translate( $sql_string ) {
 
102
    // Noop for the time being...
 
103
    return $sql_string;
 
104
  }
 
105
 
 
106
 
 
107
 
 
108
  /**
 
109
  * Returns $value escaped in an appropriate way for this database dialect.
 
110
  * @param mixed $value The value to be escaped
 
111
  * @param string $value_type The type of escaping desired.  If blank this will be worked out from gettype($value).  The special
 
112
  *                           type of 'identifier' can also be used for escaping of SQL identifiers.
 
113
  */
 
114
  function Quote( $value, $value_type = null ) {
 
115
 
 
116
    if ( !isset($value_type) ) {
 
117
      $value_type = gettype($value);
 
118
    }
 
119
 
 
120
    switch ( $value_type ) {
 
121
      case 'identifier': // special case will only happen if it is passed in.
 
122
        $rv = '"' . str_replace('"', '\\"', $value ) . '"';
 
123
        break;
 
124
      case 'null':
 
125
        $rv = 'NULL';
 
126
        break;
 
127
      case 'integer':
 
128
      case 'double' :
 
129
        return $str;
 
130
      case 'boolean':
 
131
        $rv = $str ? 'TRUE' : 'FALSE';
 
132
        break;
 
133
      case 'string':
 
134
      default:
 
135
        $str = str_replace("'", "''", $str);
 
136
        if ( strpos( $str, '\\' ) !== false ) {
 
137
          $str = str_replace('\\', '\\\\', $str);
 
138
          if ( $this->dialect == 'pgsql' ) {
 
139
            /** PostgreSQL wants to know when a string might contain escapes */
 
140
            $rv = "E'$str'";
 
141
          }
 
142
        }
 
143
    }
 
144
 
 
145
    return $rv;
 
146
 
 
147
  }
 
148
 
 
149
 
 
150
  /**
 
151
  * Replaces query parameters with appropriately escaped substitutions.
 
152
  *
 
153
  * The function takes a variable number of arguments, the first is the
 
154
  * SQL string, with replaceable '?' characters (a la DBI).  The subsequent
 
155
  * parameters being the values to replace into the SQL string.
 
156
  *
 
157
  * The values passed to the routine are analyzed for type, and quoted if
 
158
  * they appear to need quoting.  This can go wrong for (e.g.) NULL or
 
159
  * other special SQL values which are not straightforwardly identifiable
 
160
  * as needing quoting (or not).  In such cases the parameter can be forced
 
161
  * to be inserted unquoted by passing it as "array( 'plain' => $param )".
 
162
  *
 
163
  * @param  string The query string with replacable '?' characters.
 
164
  * @param mixed The values to replace into the SQL string.
 
165
  * @return The built query string
 
166
  */
 
167
  function ReplaceParameters() {
 
168
    $argc = func_num_args();
 
169
    $qry = func_get_arg(0);
 
170
    $args = func_get_args();
 
171
 
 
172
    if ( is_array($qry) ) {
 
173
      /**
 
174
      * If the first argument is an array we treat that as our arguments instead
 
175
      */
 
176
      $qry = $args[0][0];
 
177
      $args = $args[0];
 
178
      $argc = count($args);
 
179
    }
 
180
 
 
181
    /**
 
182
    * We only split into a maximum of $argc chunks.  Any leftover ? will remain in
 
183
    * the string and may be replaced at Exec rather than Prepare.
 
184
    */
 
185
    $parts = explode( '?', $qry, $argc );
 
186
    $querystring = $parts[0];
 
187
    $z = count($parts);
 
188
 
 
189
    for( $i = 1; $i < $z; $i++ ) {
 
190
      $arg = $args[$i];
 
191
      if ( !isset($arg) ) {
 
192
        $querystring .= 'NULL';
 
193
      }
 
194
      elseif ( is_array($arg) && $arg['plain'] != '' ) {
 
195
        // We abuse this, but people should access it through the PgQuery::Plain($v) function
 
196
        $querystring .= $arg['plain'];
 
197
      }
 
198
      else {
 
199
        $querystring .= $this->Quote($arg);  //parameter
 
200
      }
 
201
      $querystring .= $parts[$i]; //extras eg. ","
 
202
    }
 
203
    if ( isset($parts[$z]) ) $querystring .= $parts[$z]; //puts last part on the end
 
204
 
 
205
    return $querystring;
 
206
  }
 
207
 
 
208
 
 
209
 
 
210
}
 
211
 
 
212
 
 
213
 
 
214
/**
 
215
* Typically there will only be a single instance of the database level class in an application.
 
216
* @package awl
 
217
*/
 
218
class PdoDatabase {
 
219
  /**#@+
 
220
  * @access private
 
221
  */
 
222
 
 
223
  /**
 
224
  * Holds the PDO database connection
 
225
  */
 
226
  private $db;
 
227
 
 
228
  /**
 
229
  * Holds the dialect object
 
230
  */
 
231
  private $dialect;
 
232
 
 
233
  /**
 
234
  * Holds the state of the transaction 0 = not started, 1 = in progress, -1 = error pending rollback/commit
 
235
  */
 
236
  protected $txnstate = 0;
 
237
 
 
238
  /**
 
239
  * Holds the count of queries executed so far
 
240
  */
 
241
  protected $querycount = 0;
 
242
 
 
243
  /**
 
244
  * Holds the total duration of queries executed so far
 
245
  */
 
246
  protected $querytime = 0;
 
247
 
 
248
  /**#@-*/
 
249
 
 
250
  /**
 
251
  * The connection string is in the standard PDO format. The database won't actually be connected until the first
 
252
  * database query is run against it.
 
253
  *
 
254
  * The database object will also initialise and hold an PdoDialect object which will be used to provide database
 
255
  * specific SQL for some queries, as well as translation hooks for instances where it is necessary to modify the
 
256
  * SQL in transit to support additional databases.
 
257
  * @param string $connection_string The PDO connection string, in all it's glory
 
258
  * @param string $dbuser The database username to connect as
 
259
  * @param string $dbpass The database password to connect with
 
260
  * @param array  $options An array of driver options
 
261
  */
 
262
  function __construct( $connection_string, $dbuser=null, $dbpass=null, $options=null ) {
 
263
    $this->dialect = new PdoDialect( $connection_string );
 
264
    $this->db = new PDO( $connection_string, $dbuser, $dbpass, $options );
 
265
  }
 
266
 
 
267
 
 
268
  /**
 
269
  * Returns a PdoQuery object created using this database, the supplied SQL string, and any parameters given.
 
270
  * @param string $sql_query_string The SQL string containing optional variable replacements
 
271
  * @param mixed ... Subsequent arguments are positionally replaced into the $sql_query_string
 
272
  */
 
273
  function Prepare( ) {
 
274
    $qry = new PdoQuery( &$this );
 
275
    $qry->Query(func_get_args());
 
276
    return $qry;
 
277
  }
 
278
 
 
279
 
 
280
  /**
 
281
  * Construct and execute an SQL statement from the sql_string, replacing the parameters into it.
 
282
  *
 
283
  * @param string $sql_query_string The SQL string containing optional variable replacements
 
284
  * @param mixed ... Subsequent arguments are positionally replaced into the $sql_query_string
 
285
  * @return mixed false on error or number of rows affected. Test failure with === false
 
286
  */
 
287
  function Exec( ) {
 
288
    $sql_string = $this->dialect->ReplaceParameters(func_get_args());
 
289
 
 
290
    $start = microtime(true);
 
291
    $result = $db->exec($sql_string);
 
292
    $duration = microtime(true) - $start;
 
293
    $this->querytime += $duration;
 
294
    $this->querycount++;
 
295
 
 
296
    return $result;
 
297
  }
 
298
 
 
299
 
 
300
  /**
 
301
  * Begin a transaction.
 
302
  */
 
303
  function Begin() {
 
304
    if ( $this->txnstate == 0 ) {
 
305
      $this->db->beginTransaction();
 
306
      $this->txnstate = 1;
 
307
    }
 
308
    else {
 
309
      trigger_error("Cannot begin a transaction while a transaction is already active.", E_USER_ERROR);
 
310
    }
 
311
  }
 
312
 
 
313
 
 
314
  /**
 
315
  * Complete a transaction.
 
316
  */
 
317
  function Commit() {
 
318
    $this->txnstate = 0;
 
319
    if ( $this->txnstate != 0 ) {
 
320
      $this->db->commit();
 
321
    }
 
322
  }
 
323
 
 
324
 
 
325
  /**
 
326
  * Cancel a transaction in progress.
 
327
  */
 
328
  function Rollback() {
 
329
    $this->txnstate = 0;
 
330
    if ( $this->txnstate != 0 ) {
 
331
      $this->db->rollBack();
 
332
    }
 
333
    else {
 
334
      trigger_error("Cannot rollback unless a transaction is already active.", E_USER_ERROR);
 
335
    }
 
336
  }
 
337
 
 
338
 
 
339
  /**
 
340
  * Returns the current state of a transaction, indicating if we have begun a transaction, whether the transaction
 
341
  * has failed, or if we are not in a transaction.
 
342
  */
 
343
  function TransactionState() {
 
344
    return $this->txnstate;
 
345
  }
 
346
 
 
347
 
 
348
  /**
 
349
  * Returns the total duration of quries executed so far by this object instance.
 
350
  */
 
351
  function TotalDuration() {
 
352
    return $this->querytime;
 
353
  }
 
354
 
 
355
 
 
356
  /**
 
357
  * Returns the total number of quries executed by this object instance.
 
358
  */
 
359
  function TotalQueries() {
 
360
    return $this->querycount;
 
361
  }
 
362
 
 
363
 
 
364
  /**
 
365
  * Returns an associative array of field types, keyed by field name, for the requested named table. Internally this
 
366
  * calls PdoDialect::GetFields to get the required SQL and then processes the query in the normal manner.
 
367
  */
 
368
  function GetFields( $tablename_string ) {
 
369
  }
 
370
 
 
371
 
 
372
  /**
 
373
  * Operates identically to PdoDatabase::Prepare, except that PdoDialect::Translate() will be called on the query
 
374
  * before any processing.
 
375
  */
 
376
  function PrepareTranslated() {
 
377
  }
 
378
 
 
379
 
 
380
  /**
 
381
  * Switches on or off the processing flag controlling whether subsequent calls to PdoDatabase::Prepare are translated
 
382
  * as if PrepareTranslated() had been called.
 
383
  */
 
384
  function TranslateAll( $onoff_boolean ) {
 
385
  }
 
386
 
 
387
}
 
388
 
 
389
 
 
390
/**
 
391
* A variable of this class is normally constructed through a call to PdoDatabase::Query or PdoDatabase::Prepare,
 
392
* associating it on construction with the database which is to be queried.
 
393
* @package awl
 
394
*/
 
395
class PdoQuery {
 
396
 
 
397
  private $pdb;
 
398
  private $sth;
 
399
  private $max_duration = 2;
 
400
 
 
401
  /**
 
402
  * Where $db is a PdoDatabase object. This constructs the PdoQuery. If there are further parameters they
 
403
  * will be in turn, the sql, and any positional parameters to replace into that, and will be passed to
 
404
  * $this->Query() before returning.
 
405
  */
 
406
  function __construct( $db ) {
 
407
    $this->pdb = $db;
 
408
    if ( isset($db->default_max_duration) ) {
 
409
      $this->max_duration = $db->default_max_duration;
 
410
    }
 
411
  }
 
412
 
 
413
 
 
414
  /**
 
415
  * If the sql is supplied then PDO::prepare will be called with that SQL to prepare the query, and if there
 
416
  * are positional parameters then they will be replaced into the sql_string (with appropriate escaping)
 
417
  * before the call to PDO::prepare.  Query preparation time is counted towards total query execution time.
 
418
  */
 
419
  function Query( ) {
 
420
    $sql_string = $this->dialect->ReplaceParameters(func_get_args());
 
421
 
 
422
    $start = microtime(true);
 
423
    $this->sth = $pdb->db->prepare($sql_string);
 
424
    $duration = microtime(true) - $start;
 
425
    $this->querytime += $duration;
 
426
  }
 
427
 
 
428
 
 
429
  /**
 
430
  * If there are (some) positional parameters in the prepared query, now is the last chance to supply them...
 
431
  * before the query is executed. Returns true on success and false on error.
 
432
  */
 
433
  function Exec( ) {
 
434
    $start = microtime(true);
 
435
    $result = $this->sth->execute(func_get_args());
 
436
    $duration = microtime(true) - $start;
 
437
    $this->querytime += $duration;
 
438
    $this->querycount++;
 
439
 
 
440
    return $result;
 
441
  }
 
442
 
 
443
 
 
444
  /**
 
445
  * Will fetch the next row from the query into an object with elements named for the fields in the result.
 
446
  */
 
447
  function Fetch() {
 
448
    return $this->sth->fetchObject();
 
449
  }
 
450
 
 
451
 
 
452
  /**
 
453
  * Will fetch the next row from the query into an array with numbered elements and with elements named
 
454
  * for the fields in the result.
 
455
  */
 
456
  function FetchArray() {
 
457
    return $this->sth->fetch();
 
458
  }
 
459
 
 
460
 
 
461
  /**
 
462
  * Will fetch all result rows from the query into an array of objects with elements named for the fields in the result.
 
463
  */
 
464
  function FetchAll() {
 
465
    return $this->sth->fetchAll(PDO::FETCH_OBJ);
 
466
  }
 
467
 
 
468
 
 
469
  /**
 
470
  * An accessor for the number of rows affected when the query was executed.
 
471
  */
 
472
  function Rows() {
 
473
    return $this->sth->rowCount();
 
474
  }
 
475
 
 
476
 
 
477
  /**
 
478
  * Used to set the maximum duration for this query before it will be logged as a slow query.
 
479
  * @param double $seconds The maximum duration for this statement before logging it as 'slow'
 
480
  */
 
481
  function MaxDuration( $seconds ) {
 
482
    $this->max_duration = $seconds;
 
483
  }
 
484
 
 
485
}
 
486