sqlPersistenceId = $params[self::OPTION_PERSISTENCE_SQL]; $this->sqlPersistence = common_persistence_SqlPersistence::getPersistence($params[self::OPTION_PERSISTENCE_SQL]); $this->garbageCollection = isset($params['gc']) ? $params['gc'] : self::DEFAULT_GC_PROBABILITY; return new common_persistence_KeyValuePersistence($params, $this); } /** * * @author "Lionel Lecaque, " * @param string $id * @param string $value * @param int|null $ttl * @param boolean $nx * @throws common_Exception * @return boolean */ public function set($id, $value, $ttl = null, $nx = false) { $returnValue = false; if ($nx) { throw new common_exception_NotImplemented('NX not implemented in ' . __CLASS__); } try { $expire = is_null($ttl) ? 0 : time() + $ttl; // we need int to have safe incr and decr methods $encoded = is_int($value) ? $value : base64_encode($value); $platformName = $this->sqlPersistence->getPlatForm()->getName(); $params = [':data' => $encoded, ':time' => $expire, ':id' => $id]; if ($platformName == 'mysql') { //query found in Symfony PdoSessionHandler $statement = "INSERT INTO kv_store (kv_id, kv_value, kv_time) VALUES (:id, :data, :time) ON DUPLICATE KEY UPDATE kv_value = VALUES(kv_value), kv_time = VALUES(kv_time)"; $returnValue = $this->sqlPersistence->exec($statement, $params); } elseif ($platformName == 'oracle') { $statement = "MERGE INTO kv_store USING DUAL ON(kv_id = :id) WHEN NOT MATCHED THEN INSERT (kv_id, kv_value, kv_time) VALUES (:id, :data, sysdate) WHEN MATHED THEN UPDATE SET kv_value = :data WHERE kv_id = :id"; } else { $statement = 'UPDATE kv_store SET kv_value = :data , kv_time = :time WHERE kv_id = :id'; $returnValue = $this->sqlPersistence->exec($statement, $params, ['data' => ParameterType::STRING, 'time' => ParameterType::INTEGER, 'id' => ParameterType::STRING]); if (0 === $returnValue) { $returnValue = $this->sqlPersistence->insert( 'kv_store', ['kv_id' => $id, 'kv_time' => $expire, 'kv_value' => $encoded], ['kv_id' => ParameterType::STRING, 'kv_time' => ParameterType::INTEGER, 'kv_value' => ParameterType::STRING] ); } } if ($this->garbageCollection != 0 && rand(0, $this->garbageCollection) == 1) { $this->gc(); } } catch (Exception $e) { throw new common_Exception("Unable to write the key value storage table in the database " . $e->getMessage()); } return (bool)$returnValue; } /** * * @author "Lionel Lecaque, " * @param string $id * @throws common_Exception * @return string|boolean */ public function get($id) { try { $statement = 'SELECT kv_value, kv_time FROM kv_store WHERE kv_id = ?'; $statement = $this->sqlPersistence->getPlatForm()->limitStatement($statement, 1); $sessionValue = $this->sqlPersistence->query($statement, [$id]); while ($row = $sessionValue->fetch()) { if ($row["kv_time"] == 0 || $row["kv_time"] >= time()) { return (filter_var($row['kv_value'], FILTER_VALIDATE_INT) !== false) ? (int)$row['kv_value'] : base64_decode($row['kv_value']); } } } catch (Exception $e) { throw new common_Exception("Unable to read value from key value storage"); } return false; } /** * * @author "Lionel Lecaque, " * @param string $id * @throws common_Exception * @return boolean */ public function exists($id) { try { $statement = 'SELECT kv_value FROM kv_store WHERE kv_id = ?'; $statement = $this->sqlPersistence->getPlatForm()->limitStatement($statement, 1); $sessionValue = $this->sqlPersistence->query($statement, [$id]); return ($sessionValue->fetch() !== false); } catch (Exception $e) { throw new common_Exception("Unable to read value from key value storage"); } } /** * * @author "Lionel Lecaque, " * @param string $id * @throws common_Exception * @return boolean */ public function del($id) { try { $statement = 'DELETE FROM kv_store WHERE kv_id = ?'; $sessionValue = $this->sqlPersistence->exec($statement, [$id]); return (bool)$sessionValue; } catch (Exception $e) { throw new common_Exception("Unable to write the key value table in the database " . $e->getMessage()); } return false; } /** * Increment existing value * @param string $id * @return int The number of affected rows. */ public function incr($id) { switch ($this->sqlPersistence->getPlatForm()->getName()) { case 'postgresql': $statement = 'UPDATE kv_store SET kv_value = kv_value::integer + 1 WHERE kv_id = :id'; break; case 'gcp-spanner': $statement = 'UPDATE kv_store SET kv_value = CAST(CAST(kv_value as INT64) + 1 as string) WHERE kv_id = :id'; break; default: $statement = 'UPDATE kv_store SET kv_value = kv_value + 1 WHERE kv_id = :id'; } $params = [':id' => $id]; return $this->sqlPersistence->exec($statement, $params); } /** * Decrement existing value * @param $id * @return int The number of affected rows. */ public function decr($id) { switch ($this->sqlPersistence->getPlatForm()->getName()) { case 'postgresql': $statement = 'UPDATE kv_store SET kv_value = kv_value::integer - 1 WHERE kv_id = :id'; break; case 'gcp-spanner': $statement = 'UPDATE kv_store SET kv_value = CAST(CAST(kv_value as INT64) - 1 as string) WHERE kv_id = :id'; break; default: $statement = 'UPDATE kv_store SET kv_value = kv_value - 1 WHERE kv_id = :id'; } $params = [':id' => $id]; return $this->sqlPersistence->exec($statement, $params); } /** * Should be moved to another interface (session handler) than the persistence, * this class implementing only the persistence side and another class implementing * the handler interface and relying on the persitence. */ protected function gc() { $statement = 'DELETE FROM kv_store WHERE kv_time > 0 AND kv_time < ? '; return (bool)$this->sqlPersistence->exec($statement, [time()]); } /** * @inheritdoc * * @throws common_exception_InconsistentData */ public function provideSchema(SchemaCollection $schemaCollection) { $schema = $schemaCollection->getSchema($this->sqlPersistenceId); $table = $schema->createTable("kv_store"); $table->addColumn('kv_id', "string", ["notnull" => null,"length" => 255]); $table->addColumn('kv_value', "text", ["notnull" => null]); $table->addColumn('kv_time', "integer", ["notnull" => null,"length" => 30]); $table->setPrimaryKey(["kv_id"]); $table->addOption('engine', 'MyISAM'); } }