Project

General

Profile

0002-refs-3836-Upgrade-opPDODatabaseSessionStorage.class..patch

Shinichi Urabe, 2015-09-05 03:50

Download (22.9 KB)

View differences:

lib/config/opProjectConfiguration.class.php
81 81
    elseif ('database' === $name)
82 82
    {
83 83
      sfConfig::set('sf_factory_storage', 'opPDODatabaseSessionStorage');
84
      sfConfig::set('sf_factory_storage_parameters', array_merge(array(
85
        'db_table'    => 'session',
86
        'database'    => 'doctrine',
87
        'db_id_col'   => 'id',
88
        'db_data_col' => 'session_data',
89
        'db_time_col' => 'time',
90
      ), (array)$options, $params));
84
      sfConfig::set('sf_factory_storage_parameters', array_merge(array('database' => 'doctrine'), (array)$options, $params));
91 85
    }
92 86
    elseif ('redis' === $name)
93 87
    {
lib/user/opPDODatabaseSessionStorage.class.php
9 9
 */
10 10

  
11 11
/**
12
 * opPDODatabaseSessionStorage
12
 * Session handler using a PDO connection to read and write data.
13 13
 *
14
 * @package    OpenPNE
15
 * @subpackage user
16
 * @author     Kousuke Ebihara <ebihara@tejimaya.com>
14
 * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements
15
 * different locking strategies to handle concurrent access to the same session.
16
 * Locking is necessary to prevent loss of data due to race conditions and to keep
17
 * the session data consistent between read() and write(). With locking, requests
18
 * for the same session will wait until the other one finished writing. For this
19
 * reason it's best practice to close a session as early as possible to improve
20
 * concurrency. PHPs internal files session handler also implements locking.
21
 *
22
 * Attention: Since SQLite does not support row level locks but locks the whole database,
23
 * it means only one session can be accessed at a time. Even different sessions would wait
24
 * for another to finish. So saving session in SQLite should only be considered for
25
 * development or prototypes.
26
 *
27
 * Session data is a binary string that can contain non-printable characters like the null byte.
28
 * For this reason it must be saved in a binary column in the database like BLOB in MySQL.
29
 * Saving it in a character column could corrupt the data.
30
 *
31
 * @see http://php.net/sessionhandlerinterface
32
 *
33
 * @author Fabien Potencier <fabien@symfony.com>
34
 * @author Michael Williams <michael.williams@funsational.com>
35
 * @author Tobias Schultze <http://tobion.de>
36
 * @author Shinichi Urabe <urabe@tejimaya.com>
17 37
 */
18 38
class opPDODatabaseSessionStorage extends sfPDOSessionStorage
19 39
{
20
  public function sessionOpen($path = null, $name = null)
40
  /**
41
   * No locking is done. This means sessions are prone to loss of data due to
42
   * race conditions of concurrent requests to the same session. The last session
43
   * write will win in this case. It might be useful when you implement your own
44
   * logic to deal with this like an optimistic approach.
45
   */
46
  const LOCK_NONE = 0;
47

  
48
  /**
49
   * Creates an application-level lock on a session. The disadvantage is that the
50
   * lock is not enforced by the database and thus other, unaware parts of the
51
   * application could still concurrently modify the session. The advantage is it
52
   * does not require a transaction.
53
   * This mode is not available for SQLite and not yet implemented for oci and sqlsrv.
54
   */
55
  const LOCK_ADVISORY = 1;
56

  
57
  /**
58
   * Issues a real row lock. Since it uses a transaction between opening and
59
   * closing a session, you have to be careful when you use same database connection
60
   * that you also use for your application logic. This mode is the default because
61
   * it's the only reliable solution across DBMSs.
62
   */
63
  const LOCK_TRANSACTIONAL = 2;
64

  
65
  /**
66
   * @var string Database driver
67
   */
68
  private $driver;
69

  
70
  /**
71
   * @var string Table name
72
   */
73
  private $table = 'session';
74

  
75
  /**
76
   * @var string Column for session id
77
   */
78
  private $idCol = 'id';
79

  
80
  /**
81
   * @var string Column for session data
82
   */
83
  private $dataCol = 'session_data';
84

  
85
  /**
86
   * @var string Column for lifetime
87
   */
88
  private $lifetimeCol = 'session_lifetime';
89

  
90
  /**
91
   * @var string Column for timestamp
92
   */
93
  private $timeCol = 'time';
94

  
95
  /**
96
   * @var int The strategy for locking, see constants
97
   */
98
  private $lockMode = self::LOCK_TRANSACTIONAL;
99

  
100
  /**
101
   * It's an array to support multiple reads before closing which is manual, non-standard usage.
102
   *
103
   * @var PDOStatement[] An array of statements to release advisory locks
104
   */
105
  private $unlockStatements = array();
106

  
107
  /**
108
   * @var bool True when the current session exists but expired according to session.gc_maxlifetime
109
   */
110
  private $sessionExpired = false;
111

  
112
  /**
113
   * @var bool Whether a transaction is active
114
   */
115
  private $inTransaction = false;
116

  
117
  /**
118
   * @var bool Whether gc() has been called
119
   */
120
  private $gcCalled = false;
121

  
122
  /**
123
   * Constructor.
124
   *
125
   * List of available options:
126
   *  * db_table: The name of the table [default: session]
127
   *  * db_id_col: The column where to store the session id [default: id]
128
   *  * db_data_col: The column where to store the session data [default: session_data]
129
   *  * db_time_col: The column where to store the timestamp [default: session_time]
130
   *  * db_lifetime_col: The column where to store the lifetime [default: session_lifetime]
131
   *  * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
132
   *
133
   * @param array $options  An associative array of options
134
   *
135
   * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
136
   */
137
  public function initialize($options = array())
138
  {
139
    $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
140
    $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
141
    $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
142
    $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
143
    $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
144
    $this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode;
145
    if (opDoctrineQuery::getMasterConnectionDirect()->getName() === $options['database'])
146
    {
147
      // Skip "lockmode' when session connection use master.
148
      $this->lockMode = self::LOCK_NONE;
149
    }
150

  
151
    parent::initialize($options);
152
  }
153

  
154
  /**
155
   * Returns true when the current session exists but expired according to session.gc_maxlifetime.
156
   *
157
   * Can be used to distinguish between a new session and one that expired due to inactivity.
158
   *
159
   * @return bool Whether current session expired
160
   */
161
  public function isSessionExpired()
162
  {
163
    return $this->sessionExpired;
164
  }
165

  
166
  /**
167
   * {@inheritdoc}
168
   */
169
  public function sessionOpen($savePath, $sessionName)
21 170
  {
22 171
    if (is_string($this->options['database']))
23 172
    {
24 173
      $this->options['database'] = sfContext::getInstance()->getDatabaseManager()->getDatabase($this->options['database']);
25 174
    }
26 175

  
27
    return parent::sessionOpen($path, $name);
176
    $bool = parent::sessionOpen($savePath, $sessionName);
177
    $this->driver = $this->db->getAttribute(PDO::ATTR_DRIVER_NAME);
178

  
179
    return $bool;
180
  }
181

  
182
  /**
183
   * {@inheritdoc}
184
   */
185
  public function sessionRead($sessionId)
186
  {
187
    try
188
    {
189
      return $this->doRead($sessionId);
190
    }
191
    catch (PDOException $e)
192
    {
193
      $this->rollback();
194

  
195
      throw $e;
196
    }
197
  }
198

  
199
  /**
200
   * {@inheritdoc}
201
   */
202
  public function sessionGc($maxlifetime)
203
  {
204
    // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
205
    // This way, pruning expired sessions does not block them from being started while the current session is used.
206
    $this->gcCalled = true;
207

  
208
    return true;
209
  }
210

  
211
  /**
212
   * {@inheritdoc}
213
   */
214
  public function sessionDestroy($sessionId)
215
  {
216
    // delete the record associated with this id
217
    $sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
218

  
219
    try
220
    {
221
      $stmt = $this->db->prepare($sql);
222
      $stmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
223
      $stmt->execute();
224
    }
225
    catch (PDOException $e)
226
    {
227
      $this->rollback();
228

  
229
      throw $e;
230
    }
231

  
232
    return true;
233
  }
234

  
235
  /**
236
   * {@inheritdoc}
237
   */
238
  public function sessionWrite($sessionId, $data)
239
  {
240
    $maxlifetime = (int) ini_get('session.gc_maxlifetime');
241

  
242
    try
243
    {
244
      // We use a single MERGE SQL query when supported by the database.
245
      $mergeSql = $this->getMergeSql();
246

  
247
      if (null !== $mergeSql)
248
      {
249
        $mergeStmt = $this->db->prepare($mergeSql);
250
        $mergeStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
251
        $mergeStmt->bindParam(':data', $data, PDO::PARAM_LOB);
252
        $mergeStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
253
        $mergeStmt->bindValue(':time', time(), PDO::PARAM_INT);
254
        $mergeStmt->execute();
255

  
256
        return true;
257
      }
258

  
259
      $updateStmt = $this->db->prepare(
260
        "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
261
      );
262
      $updateStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
263
      $updateStmt->bindParam(':data', $data, PDO::PARAM_LOB);
264
      $updateStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
265
      $updateStmt->bindValue(':time', time(), PDO::PARAM_INT);
266
      $updateStmt->execute();
267

  
268
      // When MERGE is not supported, like in Postgres, we have to use this approach that can result in
269
      // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
270
      // We can just catch such an error and re-execute the update. This is similar to a serializable
271
      // transaction with retry logic on serialization failures but without the overhead and without possible
272
      // false positives due to longer gap locking.
273
      if (!$updateStmt->rowCount())
274
      {
275
        try
276
        {
277
          $insertStmt = $this->db->prepare(
278
            "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
279
          );
280
          $insertStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
281
          $insertStmt->bindParam(':data', $data, PDO::PARAM_LOB);
282
          $insertStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
283
          $insertStmt->bindValue(':time', time(), PDO::PARAM_INT);
284
          $insertStmt->execute();
285
        }
286
        catch (PDOException $e)
287
        {
288
          // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
289
          if (0 === strpos($e->getCode(), '23'))
290
          {
291
            $updateStmt->execute();
292
          }
293
          else
294
          {
295
            throw $e;
296
          }
297
        }
298
      }
299
    }
300
    catch (PDOException $e)
301
    {
302
      $this->rollback();
303

  
304
      throw $e;
305
    }
306

  
307
    return true;
308
  }
309

  
310
  /**
311
   * {@inheritdoc}
312
   */
313
  public function sessionClose()
314
  {
315
    $this->commit();
316

  
317
    while ($unlockStmt = array_shift($this->unlockStatements))
318
    {
319
      $unlockStmt->execute();
320
    }
321

  
322
    if ($this->gcCalled)
323
    {
324
      $this->gcCalled = false;
325

  
326
      // delete the session records that have expired
327
      $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
328

  
329
      $stmt = $this->db->prepare($sql);
330
      $stmt->bindValue(':time', time(), PDO::PARAM_INT);
331
      $stmt->execute();
332
    }
333

  
334
    return true;
335
  }
336

  
337
  /**
338
   * Helper method to begin a transaction.
339
   *
340
   * Since SQLite does not support row level locks, we have to acquire a reserved lock
341
   * on the database immediately. Because of https://bugs.php.net/42766 we have to create
342
   * such a transaction manually which also means we cannot use PDO::commit or
343
   * PDO::rollback or PDO::inTransaction for SQLite.
344
   *
345
   * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
346
   * due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
347
   * So we change it to READ COMMITTED.
348
   */
349
  private function beginTransaction()
350
  {
351
    if (!$this->inTransaction)
352
    {
353
      if ('sqlite' === $this->driver)
354
      {
355
        $this->db->exec('BEGIN IMMEDIATE TRANSACTION');
356
      }
357
      else
358
      {
359
        if ('mysql' === $this->driver)
360
        {
361
          $this->db->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
362
        }
363
        $this->db->beginTransaction();
364
      }
365
      $this->inTransaction = true;
366
    }
367
  }
368

  
369
  /**
370
   * Helper method to commit a transaction.
371
   */
372
  private function commit()
373
  {
374
    if ($this->inTransaction)
375
    {
376
      try
377
      {
378
        // commit read-write transaction which also releases the lock
379
        if ('sqlite' === $this->driver)
380
        {
381
          $this->db->exec('COMMIT');
382
        }
383
        else
384
        {
385
          $this->db->commit();
386
        }
387
        $this->inTransaction = false;
388
      }
389
      catch (PDOException $e)
390
      {
391
        $this->rollback();
392

  
393
        throw $e;
394
      }
395
    }
396
  }
397

  
398
  /**
399
   * Helper method to rollback a transaction.
400
   */
401
  private function rollback()
402
  {
403
    // We only need to rollback if we are in a transaction. Otherwise the resulting
404
    // error would hide the real problem why rollback was called. We might not be
405
    // in a transaction when not using the transactional locking behavior or when
406
    // two callbacks (e.g. destroy and write) are invoked that both fail.
407
    if ($this->inTransaction)
408
    {
409
      if ('sqlite' === $this->driver)
410
      {
411
        $this->db->exec('ROLLBACK');
412
      }
413
      else
414
      {
415
        $this->db->rollBack();
416
      }
417
      $this->inTransaction = false;
418
    }
419
  }
420

  
421
  /**
422
   * Reads the session data in respect to the different locking strategies.
423
   *
424
   * We need to make sure we do not return session data that is already considered garbage according
425
   * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
426
   *
427
   * @param string $sessionId Session ID
428
   *
429
   * @return string The session data
430
   */
431
  private function doRead($sessionId)
432
  {
433
    $this->sessionExpired = false;
434

  
435
    if (self::LOCK_ADVISORY === $this->lockMode)
436
    {
437
      $this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
438
    }
439

  
440
    $selectSql = $this->getSelectSql();
441
    $selectStmt = $this->db->prepare($selectSql);
442
    $selectStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
443
    $selectStmt->execute();
444

  
445
    $sessionRows = $selectStmt->fetchAll(PDO::FETCH_NUM);
446

  
447
    if ($sessionRows)
448
    {
449
      if ($sessionRows[0][1] + $sessionRows[0][2] < time())
450
      {
451
        $this->sessionExpired = true;
452

  
453
        return '';
454
      }
455

  
456
      return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
457
    }
458

  
459
    if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver)
460
    {
461
      // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
462
      // until other connections to the session are committed.
463
      try
464
      {
465
        $insertStmt = $this->db->prepare(
466
          "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
467
        );
468
        $insertStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
469
        $insertStmt->bindValue(':data', '', PDO::PARAM_LOB);
470
        $insertStmt->bindValue(':lifetime', 0, PDO::PARAM_INT);
471
        $insertStmt->bindValue(':time', time(), PDO::PARAM_INT);
472
        $insertStmt->execute();
473
      }
474
      catch (PDOException $e)
475
      {
476
        // Catch duplicate key error because other connection created the session already.
477
        // It would only not be the case when the other connection destroyed the session.
478
        if (0 === strpos($e->getCode(), '23'))
479
        {
480
          // Retrieve finished session data written by concurrent connection. SELECT
481
          // FOR UPDATE is necessary to avoid deadlock of connection that starts reading
482
          // before we write (transform intention to real lock).
483
          $selectStmt->execute();
484
          $sessionRows = $selectStmt->fetchAll(PDO::FETCH_NUM);
485

  
486
          if ($sessionRows)
487
          {
488
            return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
489
          }
490

  
491
          return '';
492
        }
493

  
494
        throw $e;
495
      }
496
    }
497

  
498
    return '';
499
  }
500

  
501
  /**
502
   * Executes an application-level lock on the database.
503
   *
504
   * @param string $sessionId Session ID
505
   *
506
   * @return PDOStatement The statement that needs to be executed later to release the lock
507
   *
508
   * @throws DomainException When an unsupported PDO driver is used
509
   *
510
   * @todo implement missing advisory locks
511
   *       - for oci using DBMS_LOCK.REQUEST
512
   *       - for sqlsrv using sp_getapplock with LockOwner = Session
513
   */
514
  private function doAdvisoryLock($sessionId)
515
  {
516
    switch ($this->driver)
517
    {
518
    case 'mysql':
519
      // should we handle the return value? 0 on timeout, null on error
520
      // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
521
      $stmt = $this->db->prepare('SELECT GET_LOCK(:key, 50)');
522
      $stmt->bindValue(':key', $sessionId, PDO::PARAM_STR);
523
      $stmt->execute();
524

  
525
      $releaseStmt = $this->db->prepare('DO RELEASE_LOCK(:key)');
526
      $releaseStmt->bindValue(':key', $sessionId, PDO::PARAM_STR);
527

  
528
      return $releaseStmt;
529
    case 'pgsql':
530
      // Obtaining an exclusive session level advisory lock requires an integer key.
531
      // So we convert the HEX representation of the session id to an integer.
532
      // Since integers are signed, we have to skip one hex char to fit in the range.
533
      if (4 === PHP_INT_SIZE)
534
      {
535
        $sessionInt1 = hexdec(substr($sessionId, 0, 7));
536
        $sessionInt2 = hexdec(substr($sessionId, 7, 7));
537

  
538
        $stmt = $this->db->prepare('SELECT pg_advisory_lock(:key1, :key2)');
539
        $stmt->bindValue(':key1', $sessionInt1, PDO::PARAM_INT);
540
        $stmt->bindValue(':key2', $sessionInt2, PDO::PARAM_INT);
541
        $stmt->execute();
542

  
543
        $releaseStmt = $this->db->prepare('SELECT pg_advisory_unlock(:key1, :key2)');
544
        $releaseStmt->bindValue(':key1', $sessionInt1, PDO::PARAM_INT);
545
        $releaseStmt->bindValue(':key2', $sessionInt2, PDO::PARAM_INT);
546
      }
547
      else
548
      {
549
        $sessionBigInt = hexdec(substr($sessionId, 0, 15));
550

  
551
        $stmt = $this->db->prepare('SELECT pg_advisory_lock(:key)');
552
        $stmt->bindValue(':key', $sessionBigInt, PDO::PARAM_INT);
553
        $stmt->execute();
554

  
555
        $releaseStmt = $this->db->prepare('SELECT pg_advisory_unlock(:key)');
556
        $releaseStmt->bindValue(':key', $sessionBigInt, PDO::PARAM_INT);
557
      }
558

  
559
      return $releaseStmt;
560
    case 'sqlite':
561
      throw new DomainException('SQLite does not support advisory locks.');
562
    default:
563
      throw new DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver));
564
    }
565
  }
566

  
567
  /**
568
   * Return a locking or nonlocking SQL query to read session information.
569
   *
570
   * @return string The SQL string
571
   *
572
   * @throws DomainException When an unsupported PDO driver is used
573
   */
574
  private function getSelectSql()
575
  {
576
    if (self::LOCK_TRANSACTIONAL === $this->lockMode)
577
    {
578
      $this->beginTransaction();
579

  
580
      switch ($this->driver)
581
      {
582
      case 'mysql':
583
      case 'oci':
584
      case 'pgsql':
585
        return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
586
      case 'sqlsrv':
587
        return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
588
      case 'sqlite':
589
        // we already locked when starting transaction
590
        break;
591
      default:
592
        throw new DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver));
593
      }
594
    }
595

  
596
    return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id";
597
  }
598

  
599
  /**
600
   * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database for writing session data.
601
   *
602
   * @return string|null The SQL string or null when not supported
603
   */
604
  private function getMergeSql()
605
  {
606
    switch ($this->driver)
607
    {
608
    case 'mysql':
609
      return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
610
        "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
611
    case 'oci':
612
      // DUAL is Oracle specific dummy table
613
      return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
614
        "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
615
        "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time";
616
    case 'sqlsrv' === $this->driver && version_compare($this->db->getAttribute(PDO::ATTR_SERVER_VERSION), '10', '>='):
617
      // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
618
      // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
619
      return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
620
        "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
621
        "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;";
622
    case 'sqlite':
623
      return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
624
    }
28 625
  }
29 626
}
30
-