Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
common_persistence_SqlKvDriver
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 9
1122
0.00% covered (danger)
0.00%
0 / 1
 connect
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 set
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
110
 get
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 exists
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 del
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 incr
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 decr
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 gc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 provideSchema
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2013-2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22use Doctrine\DBAL\ParameterType;
23use oat\generis\persistence\sql\SchemaProviderInterface;
24use oat\generis\persistence\sql\SchemaCollection;
25
26/**
27 * A key value driver based upon an existing sql persistence
28 *
29 * @todo : Refactor driver specific stuff to dedicated implementation
30 */
31class common_persistence_SqlKvDriver implements common_persistence_KvDriver, SchemaProviderInterface
32{
33    public const DEFAULT_GC_PROBABILITY = 1000;
34
35    public const OPTION_PERSISTENCE_SQL = 'sqlPersistence';
36
37    /**
38     * Identifier of the sql persitence used
39     * @var string
40     */
41    private $sqlPersistenceId;
42
43    /**
44     * @var common_persistence_SqlPersistence
45     */
46    private $sqlPersistence;
47
48    /**
49     * Probability of garbage collection to be triggered
50     * stores the inverse element
51     *
52     * @var int
53     */
54    private $garbageCollection;
55
56    /**
57     * (non-PHPdoc)
58     * @see common_persistence_Driver::connect()
59     */
60    public function connect($id, array $params)
61    {
62        if (!isset($params[self::OPTION_PERSISTENCE_SQL])) {
63            throw new common_exception_Error('Missing underlying sql persistence');
64        }
65
66        $this->sqlPersistenceId = $params[self::OPTION_PERSISTENCE_SQL];
67        $this->sqlPersistence = common_persistence_SqlPersistence::getPersistence(
68            $params[self::OPTION_PERSISTENCE_SQL]
69        );
70        $this->garbageCollection = isset($params['gc']) ? $params['gc'] : self::DEFAULT_GC_PROBABILITY;
71
72
73        return new common_persistence_KeyValuePersistence($params, $this);
74    }
75
76    /**
77     *
78     * @author "Lionel Lecaque, <lionel@taotesting.com>"
79     * @param string $id
80     * @param string $value
81     * @param int|null $ttl
82     * @param boolean $nx
83     * @throws common_Exception
84     * @return boolean
85     */
86    public function set($id, $value, $ttl = null, $nx = false)
87    {
88        $returnValue = false;
89        if ($nx) {
90            throw new common_exception_NotImplemented('NX not implemented in ' . __CLASS__);
91        }
92        try {
93            $expire = is_null($ttl) ? 0 : time() + $ttl;
94
95            // we need int to have safe incr and decr methods
96            $encoded = is_int($value) ? $value : base64_encode($value);
97
98            $platformName = $this->sqlPersistence->getPlatForm()->getName();
99            $params = [':data' => $encoded, ':time' => $expire, ':id' => $id];
100
101            if ($platformName == 'mysql') {
102                //query found in Symfony PdoSessionHandler
103                $statement = "INSERT INTO kv_store (kv_id, kv_value, kv_time) VALUES (:id, :data, :time) 
104                    ON DUPLICATE KEY UPDATE kv_value = VALUES(kv_value), kv_time = VALUES(kv_time)";
105                $returnValue = $this->sqlPersistence->exec($statement, $params);
106            } elseif ($platformName == 'oracle') {
107                $statement = "MERGE INTO kv_store USING DUAL ON(kv_id = :id) 
108                    WHEN NOT MATCHED THEN INSERT (kv_id, kv_value, kv_time) VALUES (:id, :data, sysdate) 
109                    WHEN MATHED THEN UPDATE SET kv_value = :data WHERE kv_id = :id";
110            } else {
111                $statement = 'UPDATE kv_store SET kv_value = :data , kv_time = :time WHERE kv_id = :id';
112                $returnValue = $this->sqlPersistence->exec(
113                    $statement,
114                    $params,
115                    ['data' => ParameterType::STRING, 'time' => ParameterType::INTEGER, 'id' => ParameterType::STRING]
116                );
117                if (0 === $returnValue) {
118                    $returnValue = $this->sqlPersistence->insert(
119                        'kv_store',
120                        ['kv_id' => $id, 'kv_time' => $expire, 'kv_value' => $encoded],
121                        [
122                            'kv_id' => ParameterType::STRING,
123                            'kv_time' => ParameterType::INTEGER,
124                            'kv_value' => ParameterType::STRING
125                        ]
126                    );
127                }
128            }
129
130            if ($this->garbageCollection != 0 && rand(0, $this->garbageCollection) == 1) {
131                $this->gc();
132            }
133        } catch (Exception $e) {
134            throw new common_Exception(
135                "Unable to write the key value storage table in the database "  . $e->getMessage()
136            );
137        }
138        return (bool)$returnValue;
139    }
140
141    /**
142     *
143     * @author "Lionel Lecaque, <lionel@taotesting.com>"
144     * @param string $id
145     * @throws common_Exception
146     * @return string|boolean
147     */
148    public function get($id)
149    {
150        try {
151            $statement = 'SELECT kv_value, kv_time FROM kv_store WHERE kv_id = ?';
152            $statement = $this->sqlPersistence->getPlatForm()->limitStatement($statement, 1);
153            $sessionValue = $this->sqlPersistence->query($statement, [$id]);
154            while ($row = $sessionValue->fetch()) {
155                if ($row["kv_time"] == 0 || $row["kv_time"] >= time()) {
156                    return (filter_var($row['kv_value'], FILTER_VALIDATE_INT) !== false)
157                        ? (int)$row['kv_value']
158                        : base64_decode($row['kv_value']);
159                }
160            }
161        } catch (Exception $e) {
162            throw new common_Exception("Unable to read value from key value storage");
163        }
164        return false;
165    }
166
167    /**
168     *
169     * @author "Lionel Lecaque, <lionel@taotesting.com>"
170     * @param string $id
171     * @throws common_Exception
172     * @return boolean
173     */
174    public function exists($id)
175    {
176        try {
177            $statement = 'SELECT kv_value FROM kv_store WHERE kv_id = ?';
178            $statement = $this->sqlPersistence->getPlatForm()->limitStatement($statement, 1);
179            $sessionValue = $this->sqlPersistence->query($statement, [$id]);
180            return ($sessionValue->fetch() !== false);
181        } catch (Exception $e) {
182            throw new common_Exception("Unable to read value from key value storage");
183        }
184    }
185
186    /**
187     *
188     * @author "Lionel Lecaque, <lionel@taotesting.com>"
189     * @param string $id
190     * @throws common_Exception
191     * @return boolean
192     */
193    public function del($id)
194    {
195        try {
196            $statement = 'DELETE FROM kv_store WHERE kv_id = ?';
197            $sessionValue = $this->sqlPersistence->exec($statement, [$id]);
198            return (bool)$sessionValue;
199        } catch (Exception $e) {
200            throw new common_Exception("Unable to write the key value table in the database " . $e->getMessage());
201        }
202        return false;
203    }
204
205    /**
206     * Increment existing value
207     * @param string $id
208     * @return int The number of affected rows.
209     */
210    public function incr($id)
211    {
212        switch ($this->sqlPersistence->getPlatForm()->getName()) {
213            case 'postgresql':
214                $statement = 'UPDATE kv_store SET kv_value = kv_value::integer + 1 WHERE kv_id = :id';
215                break;
216            case 'gcp-spanner':
217                $statement = 'UPDATE kv_store SET kv_value = CAST(CAST(kv_value as INT64) + 1 as string) WHERE '
218                    . 'kv_id = :id';
219                break;
220            default:
221                $statement = 'UPDATE kv_store SET kv_value = kv_value + 1 WHERE kv_id = :id';
222        }
223        $params = [':id' => $id];
224        return $this->sqlPersistence->exec($statement, $params);
225    }
226
227    /**
228     * Decrement existing value
229     * @param $id
230     * @return int The number of affected rows.
231     */
232    public function decr($id)
233    {
234        switch ($this->sqlPersistence->getPlatForm()->getName()) {
235            case 'postgresql':
236                $statement = 'UPDATE kv_store SET kv_value = kv_value::integer - 1 WHERE kv_id = :id';
237                break;
238            case 'gcp-spanner':
239                $statement = 'UPDATE kv_store SET kv_value = CAST(CAST(kv_value as INT64) - 1 as string) WHERE '
240                    . 'kv_id = :id';
241                break;
242            default:
243                $statement = 'UPDATE kv_store SET kv_value = kv_value - 1 WHERE kv_id = :id';
244        }
245        $params = [':id' => $id];
246        return $this->sqlPersistence->exec($statement, $params);
247    }
248
249    /**
250     * Should be moved to another interface (session handler) than the persistence,
251     * this class implementing only the persistence side and another class implementing
252     * the handler interface and relying on the persitence.
253     */
254    protected function gc()
255    {
256        $statement = 'DELETE FROM kv_store WHERE kv_time > 0 AND kv_time <  ? ';
257        return (bool)$this->sqlPersistence->exec($statement, [time()]);
258    }
259
260    /**
261     * @inheritdoc
262     *
263     * @throws common_exception_InconsistentData
264     */
265    public function provideSchema(SchemaCollection $schemaCollection)
266    {
267        $schema = $schemaCollection->getSchema($this->sqlPersistenceId);
268        $table = $schema->createTable("kv_store");
269        $table->addColumn('kv_id', "string", ["notnull" => null,"length" => 255]);
270        $table->addColumn('kv_value', "text", ["notnull" => null]);
271        $table->addColumn('kv_time', "integer", ["notnull" => null,"length" => 30]);
272        $table->setPrimaryKey(["kv_id"]);
273        $table->addOption('engine', 'MyISAM');
274    }
275}