Enhancement(機能追加・改善) #3836 » 0002-refs-3836-Upgrade-opPDODatabaseSessionStorage.class..patch
lib/config/opProjectConfiguration.class.php | ||
---|---|---|
elseif ('database' === $name)
|
||
{
|
||
sfConfig::set('sf_factory_storage', 'opPDODatabaseSessionStorage');
|
||
sfConfig::set('sf_factory_storage_parameters', array_merge(array(
|
||
'db_table' => 'session',
|
||
'database' => 'doctrine',
|
||
'db_id_col' => 'id',
|
||
'db_data_col' => 'session_data',
|
||
'db_time_col' => 'time',
|
||
), (array)$options, $params));
|
||
sfConfig::set('sf_factory_storage_parameters', array_merge(array('database' => 'doctrine'), (array)$options, $params));
|
||
}
|
||
elseif ('redis' === $name)
|
||
{
|
lib/user/opPDODatabaseSessionStorage.class.php | ||
---|---|---|
*/
|
||
/**
|
||
* opPDODatabaseSessionStorage
|
||
* Session handler using a PDO connection to read and write data.
|
||
*
|
||
* @package OpenPNE
|
||
* @subpackage user
|
||
* @author Kousuke Ebihara <ebihara@tejimaya.com>
|
||
* It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements
|
||
* different locking strategies to handle concurrent access to the same session.
|
||
* Locking is necessary to prevent loss of data due to race conditions and to keep
|
||
* the session data consistent between read() and write(). With locking, requests
|
||
* for the same session will wait until the other one finished writing. For this
|
||
* reason it's best practice to close a session as early as possible to improve
|
||
* concurrency. PHPs internal files session handler also implements locking.
|
||
*
|
||
* Attention: Since SQLite does not support row level locks but locks the whole database,
|
||
* it means only one session can be accessed at a time. Even different sessions would wait
|
||
* for another to finish. So saving session in SQLite should only be considered for
|
||
* development or prototypes.
|
||
*
|
||
* Session data is a binary string that can contain non-printable characters like the null byte.
|
||
* For this reason it must be saved in a binary column in the database like BLOB in MySQL.
|
||
* Saving it in a character column could corrupt the data.
|
||
*
|
||
* @see http://php.net/sessionhandlerinterface
|
||
*
|
||
* @author Fabien Potencier <fabien@symfony.com>
|
||
* @author Michael Williams <michael.williams@funsational.com>
|
||
* @author Tobias Schultze <http://tobion.de>
|
||
* @author Shinichi Urabe <urabe@tejimaya.com>
|
||
*/
|
||
class opPDODatabaseSessionStorage extends sfPDOSessionStorage
|
||
{
|
||
public function sessionOpen($path = null, $name = null)
|
||
/**
|
||
* No locking is done. This means sessions are prone to loss of data due to
|
||
* race conditions of concurrent requests to the same session. The last session
|
||
* write will win in this case. It might be useful when you implement your own
|
||
* logic to deal with this like an optimistic approach.
|
||
*/
|
||
const LOCK_NONE = 0;
|
||
/**
|
||
* Creates an application-level lock on a session. The disadvantage is that the
|
||
* lock is not enforced by the database and thus other, unaware parts of the
|
||
* application could still concurrently modify the session. The advantage is it
|
||
* does not require a transaction.
|
||
* This mode is not available for SQLite and not yet implemented for oci and sqlsrv.
|
||
*/
|
||
const LOCK_ADVISORY = 1;
|
||
/**
|
||
* Issues a real row lock. Since it uses a transaction between opening and
|
||
* closing a session, you have to be careful when you use same database connection
|
||
* that you also use for your application logic. This mode is the default because
|
||
* it's the only reliable solution across DBMSs.
|
||
*/
|
||
const LOCK_TRANSACTIONAL = 2;
|
||
/**
|
||
* @var string Database driver
|
||
*/
|
||
private $driver;
|
||
/**
|
||
* @var string Table name
|
||
*/
|
||
private $table = 'session';
|
||
/**
|
||
* @var string Column for session id
|
||
*/
|
||
private $idCol = 'id';
|
||
/**
|
||
* @var string Column for session data
|
||
*/
|
||
private $dataCol = 'session_data';
|
||
/**
|
||
* @var string Column for lifetime
|
||
*/
|
||
private $lifetimeCol = 'session_lifetime';
|
||
/**
|
||
* @var string Column for timestamp
|
||
*/
|
||
private $timeCol = 'time';
|
||
/**
|
||
* @var int The strategy for locking, see constants
|
||
*/
|
||
private $lockMode = self::LOCK_TRANSACTIONAL;
|
||
/**
|
||
* It's an array to support multiple reads before closing which is manual, non-standard usage.
|
||
*
|
||
* @var PDOStatement[] An array of statements to release advisory locks
|
||
*/
|
||
private $unlockStatements = array();
|
||
/**
|
||
* @var bool True when the current session exists but expired according to session.gc_maxlifetime
|
||
*/
|
||
private $sessionExpired = false;
|
||
/**
|
||
* @var bool Whether a transaction is active
|
||
*/
|
||
private $inTransaction = false;
|
||
/**
|
||
* @var bool Whether gc() has been called
|
||
*/
|
||
private $gcCalled = false;
|
||
/**
|
||
* Constructor.
|
||
*
|
||
* List of available options:
|
||
* * db_table: The name of the table [default: session]
|
||
* * db_id_col: The column where to store the session id [default: id]
|
||
* * db_data_col: The column where to store the session data [default: session_data]
|
||
* * db_time_col: The column where to store the timestamp [default: session_time]
|
||
* * db_lifetime_col: The column where to store the lifetime [default: session_lifetime]
|
||
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
|
||
*
|
||
* @param array $options An associative array of options
|
||
*
|
||
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
|
||
*/
|
||
public function initialize($options = array())
|
||
{
|
||
$this->table = isset($options['db_table']) ? $options['db_table'] : $this->table;
|
||
$this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol;
|
||
$this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol;
|
||
$this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol;
|
||
$this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol;
|
||
$this->lockMode = isset($options['lock_mode']) ? $options['lock_mode'] : $this->lockMode;
|
||
if (opDoctrineQuery::getMasterConnectionDirect()->getName() === $options['database'])
|
||
{
|
||
// Skip "lockmode' when session connection use master.
|
||
$this->lockMode = self::LOCK_NONE;
|
||
}
|
||
parent::initialize($options);
|
||
}
|
||
/**
|
||
* Returns true when the current session exists but expired according to session.gc_maxlifetime.
|
||
*
|
||
* Can be used to distinguish between a new session and one that expired due to inactivity.
|
||
*
|
||
* @return bool Whether current session expired
|
||
*/
|
||
public function isSessionExpired()
|
||
{
|
||
return $this->sessionExpired;
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionOpen($savePath, $sessionName)
|
||
{
|
||
if (is_string($this->options['database']))
|
||
{
|
||
$this->options['database'] = sfContext::getInstance()->getDatabaseManager()->getDatabase($this->options['database']);
|
||
}
|
||
return parent::sessionOpen($path, $name);
|
||
$bool = parent::sessionOpen($savePath, $sessionName);
|
||
$this->driver = $this->db->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||
return $bool;
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionRead($sessionId)
|
||
{
|
||
try
|
||
{
|
||
return $this->doRead($sessionId);
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
$this->rollback();
|
||
throw $e;
|
||
}
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionGc($maxlifetime)
|
||
{
|
||
// We delay gc() to close() so that it is executed outside the transactional and blocking read-write process.
|
||
// This way, pruning expired sessions does not block them from being started while the current session is used.
|
||
$this->gcCalled = true;
|
||
return true;
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionDestroy($sessionId)
|
||
{
|
||
// delete the record associated with this id
|
||
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
|
||
try
|
||
{
|
||
$stmt = $this->db->prepare($sql);
|
||
$stmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$stmt->execute();
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
$this->rollback();
|
||
throw $e;
|
||
}
|
||
return true;
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionWrite($sessionId, $data)
|
||
{
|
||
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
|
||
try
|
||
{
|
||
// We use a single MERGE SQL query when supported by the database.
|
||
$mergeSql = $this->getMergeSql();
|
||
if (null !== $mergeSql)
|
||
{
|
||
$mergeStmt = $this->db->prepare($mergeSql);
|
||
$mergeStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$mergeStmt->bindParam(':data', $data, PDO::PARAM_LOB);
|
||
$mergeStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
|
||
$mergeStmt->bindValue(':time', time(), PDO::PARAM_INT);
|
||
$mergeStmt->execute();
|
||
return true;
|
||
}
|
||
$updateStmt = $this->db->prepare(
|
||
"UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
|
||
);
|
||
$updateStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$updateStmt->bindParam(':data', $data, PDO::PARAM_LOB);
|
||
$updateStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
|
||
$updateStmt->bindValue(':time', time(), PDO::PARAM_INT);
|
||
$updateStmt->execute();
|
||
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
|
||
// duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior).
|
||
// We can just catch such an error and re-execute the update. This is similar to a serializable
|
||
// transaction with retry logic on serialization failures but without the overhead and without possible
|
||
// false positives due to longer gap locking.
|
||
if (!$updateStmt->rowCount())
|
||
{
|
||
try
|
||
{
|
||
$insertStmt = $this->db->prepare(
|
||
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
|
||
);
|
||
$insertStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$insertStmt->bindParam(':data', $data, PDO::PARAM_LOB);
|
||
$insertStmt->bindParam(':lifetime', $maxlifetime, PDO::PARAM_INT);
|
||
$insertStmt->bindValue(':time', time(), PDO::PARAM_INT);
|
||
$insertStmt->execute();
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
|
||
if (0 === strpos($e->getCode(), '23'))
|
||
{
|
||
$updateStmt->execute();
|
||
}
|
||
else
|
||
{
|
||
throw $e;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
$this->rollback();
|
||
throw $e;
|
||
}
|
||
return true;
|
||
}
|
||
/**
|
||
* {@inheritdoc}
|
||
*/
|
||
public function sessionClose()
|
||
{
|
||
$this->commit();
|
||
while ($unlockStmt = array_shift($this->unlockStatements))
|
||
{
|
||
$unlockStmt->execute();
|
||
}
|
||
if ($this->gcCalled)
|
||
{
|
||
$this->gcCalled = false;
|
||
// delete the session records that have expired
|
||
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
|
||
$stmt = $this->db->prepare($sql);
|
||
$stmt->bindValue(':time', time(), PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
}
|
||
return true;
|
||
}
|
||
/**
|
||
* Helper method to begin a transaction.
|
||
*
|
||
* Since SQLite does not support row level locks, we have to acquire a reserved lock
|
||
* on the database immediately. Because of https://bugs.php.net/42766 we have to create
|
||
* such a transaction manually which also means we cannot use PDO::commit or
|
||
* PDO::rollback or PDO::inTransaction for SQLite.
|
||
*
|
||
* Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions
|
||
* due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ .
|
||
* So we change it to READ COMMITTED.
|
||
*/
|
||
private function beginTransaction()
|
||
{
|
||
if (!$this->inTransaction)
|
||
{
|
||
if ('sqlite' === $this->driver)
|
||
{
|
||
$this->db->exec('BEGIN IMMEDIATE TRANSACTION');
|
||
}
|
||
else
|
||
{
|
||
if ('mysql' === $this->driver)
|
||
{
|
||
$this->db->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
|
||
}
|
||
$this->db->beginTransaction();
|
||
}
|
||
$this->inTransaction = true;
|
||
}
|
||
}
|
||
/**
|
||
* Helper method to commit a transaction.
|
||
*/
|
||
private function commit()
|
||
{
|
||
if ($this->inTransaction)
|
||
{
|
||
try
|
||
{
|
||
// commit read-write transaction which also releases the lock
|
||
if ('sqlite' === $this->driver)
|
||
{
|
||
$this->db->exec('COMMIT');
|
||
}
|
||
else
|
||
{
|
||
$this->db->commit();
|
||
}
|
||
$this->inTransaction = false;
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
$this->rollback();
|
||
throw $e;
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Helper method to rollback a transaction.
|
||
*/
|
||
private function rollback()
|
||
{
|
||
// We only need to rollback if we are in a transaction. Otherwise the resulting
|
||
// error would hide the real problem why rollback was called. We might not be
|
||
// in a transaction when not using the transactional locking behavior or when
|
||
// two callbacks (e.g. destroy and write) are invoked that both fail.
|
||
if ($this->inTransaction)
|
||
{
|
||
if ('sqlite' === $this->driver)
|
||
{
|
||
$this->db->exec('ROLLBACK');
|
||
}
|
||
else
|
||
{
|
||
$this->db->rollBack();
|
||
}
|
||
$this->inTransaction = false;
|
||
}
|
||
}
|
||
/**
|
||
* Reads the session data in respect to the different locking strategies.
|
||
*
|
||
* We need to make sure we do not return session data that is already considered garbage according
|
||
* to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
|
||
*
|
||
* @param string $sessionId Session ID
|
||
*
|
||
* @return string The session data
|
||
*/
|
||
private function doRead($sessionId)
|
||
{
|
||
$this->sessionExpired = false;
|
||
if (self::LOCK_ADVISORY === $this->lockMode)
|
||
{
|
||
$this->unlockStatements[] = $this->doAdvisoryLock($sessionId);
|
||
}
|
||
$selectSql = $this->getSelectSql();
|
||
$selectStmt = $this->db->prepare($selectSql);
|
||
$selectStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$selectStmt->execute();
|
||
$sessionRows = $selectStmt->fetchAll(PDO::FETCH_NUM);
|
||
if ($sessionRows)
|
||
{
|
||
if ($sessionRows[0][1] + $sessionRows[0][2] < time())
|
||
{
|
||
$this->sessionExpired = true;
|
||
return '';
|
||
}
|
||
return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
|
||
}
|
||
if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver)
|
||
{
|
||
// Exclusive-reading of non-existent rows does not block, so we need to do an insert to block
|
||
// until other connections to the session are committed.
|
||
try
|
||
{
|
||
$insertStmt = $this->db->prepare(
|
||
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
|
||
);
|
||
$insertStmt->bindParam(':id', $sessionId, PDO::PARAM_STR);
|
||
$insertStmt->bindValue(':data', '', PDO::PARAM_LOB);
|
||
$insertStmt->bindValue(':lifetime', 0, PDO::PARAM_INT);
|
||
$insertStmt->bindValue(':time', time(), PDO::PARAM_INT);
|
||
$insertStmt->execute();
|
||
}
|
||
catch (PDOException $e)
|
||
{
|
||
// Catch duplicate key error because other connection created the session already.
|
||
// It would only not be the case when the other connection destroyed the session.
|
||
if (0 === strpos($e->getCode(), '23'))
|
||
{
|
||
// Retrieve finished session data written by concurrent connection. SELECT
|
||
// FOR UPDATE is necessary to avoid deadlock of connection that starts reading
|
||
// before we write (transform intention to real lock).
|
||
$selectStmt->execute();
|
||
$sessionRows = $selectStmt->fetchAll(PDO::FETCH_NUM);
|
||
if ($sessionRows)
|
||
{
|
||
return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
|
||
}
|
||
return '';
|
||
}
|
||
throw $e;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
/**
|
||
* Executes an application-level lock on the database.
|
||
*
|
||
* @param string $sessionId Session ID
|
||
*
|
||
* @return PDOStatement The statement that needs to be executed later to release the lock
|
||
*
|
||
* @throws DomainException When an unsupported PDO driver is used
|
||
*
|
||
* @todo implement missing advisory locks
|
||
* - for oci using DBMS_LOCK.REQUEST
|
||
* - for sqlsrv using sp_getapplock with LockOwner = Session
|
||
*/
|
||
private function doAdvisoryLock($sessionId)
|
||
{
|
||
switch ($this->driver)
|
||
{
|
||
case 'mysql':
|
||
// should we handle the return value? 0 on timeout, null on error
|
||
// we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
|
||
$stmt = $this->db->prepare('SELECT GET_LOCK(:key, 50)');
|
||
$stmt->bindValue(':key', $sessionId, PDO::PARAM_STR);
|
||
$stmt->execute();
|
||
$releaseStmt = $this->db->prepare('DO RELEASE_LOCK(:key)');
|
||
$releaseStmt->bindValue(':key', $sessionId, PDO::PARAM_STR);
|
||
return $releaseStmt;
|
||
case 'pgsql':
|
||
// Obtaining an exclusive session level advisory lock requires an integer key.
|
||
// So we convert the HEX representation of the session id to an integer.
|
||
// Since integers are signed, we have to skip one hex char to fit in the range.
|
||
if (4 === PHP_INT_SIZE)
|
||
{
|
||
$sessionInt1 = hexdec(substr($sessionId, 0, 7));
|
||
$sessionInt2 = hexdec(substr($sessionId, 7, 7));
|
||
$stmt = $this->db->prepare('SELECT pg_advisory_lock(:key1, :key2)');
|
||
$stmt->bindValue(':key1', $sessionInt1, PDO::PARAM_INT);
|
||
$stmt->bindValue(':key2', $sessionInt2, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
$releaseStmt = $this->db->prepare('SELECT pg_advisory_unlock(:key1, :key2)');
|
||
$releaseStmt->bindValue(':key1', $sessionInt1, PDO::PARAM_INT);
|
||
$releaseStmt->bindValue(':key2', $sessionInt2, PDO::PARAM_INT);
|
||
}
|
||
else
|
||
{
|
||
$sessionBigInt = hexdec(substr($sessionId, 0, 15));
|
||
$stmt = $this->db->prepare('SELECT pg_advisory_lock(:key)');
|
||
$stmt->bindValue(':key', $sessionBigInt, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
$releaseStmt = $this->db->prepare('SELECT pg_advisory_unlock(:key)');
|
||
$releaseStmt->bindValue(':key', $sessionBigInt, PDO::PARAM_INT);
|
||
}
|
||
return $releaseStmt;
|
||
case 'sqlite':
|
||
throw new DomainException('SQLite does not support advisory locks.');
|
||
default:
|
||
throw new DomainException(sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver));
|
||
}
|
||
}
|
||
/**
|
||
* Return a locking or nonlocking SQL query to read session information.
|
||
*
|
||
* @return string The SQL string
|
||
*
|
||
* @throws DomainException When an unsupported PDO driver is used
|
||
*/
|
||
private function getSelectSql()
|
||
{
|
||
if (self::LOCK_TRANSACTIONAL === $this->lockMode)
|
||
{
|
||
$this->beginTransaction();
|
||
switch ($this->driver)
|
||
{
|
||
case 'mysql':
|
||
case 'oci':
|
||
case 'pgsql':
|
||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE";
|
||
case 'sqlsrv':
|
||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id";
|
||
case 'sqlite':
|
||
// we already locked when starting transaction
|
||
break;
|
||
default:
|
||
throw new DomainException(sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver));
|
||
}
|
||
}
|
||
return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id";
|
||
}
|
||
/**
|
||
* Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database for writing session data.
|
||
*
|
||
* @return string|null The SQL string or null when not supported
|
||
*/
|
||
private function getMergeSql()
|
||
{
|
||
switch ($this->driver)
|
||
{
|
||
case 'mysql':
|
||
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
|
||
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
|
||
case 'oci':
|
||
// DUAL is Oracle specific dummy table
|
||
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
|
||
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
|
||
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time";
|
||
case 'sqlsrv' === $this->driver && version_compare($this->db->getAttribute(PDO::ATTR_SERVER_VERSION), '10', '>='):
|
||
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
|
||
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
|
||
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
|
||
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
|
||
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;";
|
||
case 'sqlite':
|
||
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
|
||
}
|
||
}
|
||
}
|