Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.23% covered (success)
90.23%
277 / 307
86.05% covered (warning)
86.05%
37 / 43
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractRdsResultStorage
90.23% covered (success)
90.23%
277 / 307
86.05% covered (warning)
86.05%
37 / 43
90.58
0.00% covered (danger)
0.00%
0 / 1
 storeTestVariable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeTestVariables
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 storeItemVariable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeItemVariables
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 replaceItemVariables
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 replaceTestVariables
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 storeRelatedTestTaker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeRelatedDelivery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 storeRelatedData
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 getVariables
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getDeliveryVariables
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getVariable
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getVariableProperty
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getVariablesSortingField
n/a
0 / 0
n/a
0 / 0
0
 getTestTaker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDelivery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelatedData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getAllCallIds
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getRelatedItemCallIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelatedTestCallIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelatedCallIds
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getAllTestTakerIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllDeliveryIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllIds
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getResultByDelivery
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 getOrderField
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getOrderDirection
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 countResultByDelivery
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 deleteResult
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 prepareItemVariableData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 prepareTestVariableData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prepareVariableData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 prepareVariableDataForSchema
n/a
0 / 0
n/a
0 / 0
0
 getResultRow
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getQueryBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPersistence
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 unserializeVariableValue
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
3.82
 serializeVariableValue
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
3.88
 insertMultiple
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 spawnResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configure
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sortTimeStamps
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 createResultsTable
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createVariablesTable
n/a
0 / 0
n/a
0 / 0
0
 createTableConstraints
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getTypes
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
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) 2014-2023 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoOutcomeRds\model;
23
24use Doctrine\DBAL\Connection;
25use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
26use Doctrine\DBAL\ParameterType;
27use Doctrine\DBAL\Query\QueryBuilder;
28use Doctrine\DBAL\Schema\Schema;
29use Doctrine\DBAL\Schema\SchemaException;
30use Doctrine\DBAL\Schema\Table;
31use Exception;
32use oat\generis\persistence\PersistenceManager;
33use oat\oatbox\log\LoggerAwareTrait;
34use oat\oatbox\service\ConfigurableService;
35use oat\taoResultServer\models\classes\ResultDeliveryExecutionDelete;
36use oat\taoResultServer\models\classes\ResultManagement;
37use oat\taoResultServer\models\Exceptions\DuplicateVariableException;
38use Psr\Log\LoggerAwareInterface;
39use taoResultServer_models_classes_ReadableResultStorage as ReadableResultStorage;
40use taoResultServer_models_classes_Variable as Variable;
41use taoResultServer_models_classes_WritableResultStorage as WritableResultStorage;
42
43/**
44 * Implements tao results storage using the configured persistence "taoOutcomeRds"
45 */
46abstract class AbstractRdsResultStorage extends ConfigurableService implements
47    WritableResultStorage,
48    ReadableResultStorage,
49    ResultManagement,
50    LoggerAwareInterface
51{
52    use LoggerAwareTrait;
53    use ResultDeliveryExecutionDelete;
54
55    public const SERVICE_ID = 'taoOutcomeRds/RdsResultStorage';
56    /**
57     * Constants for the database creation and data access
58     */
59    public const RESULTS_TABLENAME = 'results_storage';
60    public const RESULTS_TABLE_ID = 'result_id';
61    public const TEST_TAKER_COLUMN = 'test_taker';
62    public const DELIVERY_COLUMN = 'delivery';
63    public const VARIABLES_TABLENAME = 'variables_storage';
64    public const VARIABLES_TABLE_ID = 'variable_id';
65    public const CALL_ID_ITEM_COLUMN = 'call_id_item';
66    public const CALL_ID_TEST_COLUMN = 'call_id_test';
67    public const TEST_COLUMN = 'test';
68    public const ITEM_COLUMN = 'item';
69    public const VARIABLE_VALUE = 'value';
70    public const VARIABLE_IDENTIFIER = 'identifier';
71    public const VARIABLE_HASH = 'variable_hash';
72    public const CALL_ID_ITEM_INDEX = 'idx_variables_storage_call_id_item';
73    public const CALL_ID_TEST_INDEX = 'idx_variables_storage_call_id_test';
74    public const UNIQUE_VARIABLE_INDEX = 'idx_unique_variables_storage';
75    /** @deprecated */
76    public const VARIABLE_CLASS = 'class';
77    public const VARIABLES_FK_COLUMN = 'results_result_id';
78    public const VARIABLES_FK_NAME = 'fk_variables_results';
79    /** @deprecated */
80    public const RESULT_KEY_VALUE_TABLE_NAME = 'results_kv_storage';
81    /** @deprecated */
82    public const KEY_COLUMN = 'result_key';
83    /** @deprecated */
84    public const VALUE_COLUMN = 'result_value';
85    /** @deprecated */
86    public const RESULTSKV_FK_COLUMN = 'variables_variable_id';
87    /** @deprecated */
88    public const RESULTSKV_FK_NAME = 'fk_resultsKv_variables';
89    /** result storage persistence identifier */
90    public const OPTION_PERSISTENCE = 'persistence';
91    // Fields for results retrieval.
92    public const FIELD_DELIVERY_RESULT = 'deliveryResultIdentifier';
93    public const FIELD_TEST_TAKER = 'testTakerIdentifier';
94    public const FIELD_DELIVERY = 'deliveryIdentifier';
95
96    /** @var */
97    protected $persistence;
98
99    public function storeTestVariable($deliveryResultIdentifier, $test, Variable $testVariable, $callIdTest)
100    {
101        $this->storeTestVariables($deliveryResultIdentifier, $test, [$testVariable], $callIdTest);
102    }
103
104    /**
105     * @inheritdoc
106     * Stores the test variables in table and their values in key/value storage.
107     */
108    public function storeTestVariables($deliveryResultIdentifier, $test, array $testVariables, $callIdTest)
109    {
110        $dataToInsert = [];
111
112        foreach ($testVariables as $testVariable) {
113            $dataToInsert[] = $this->prepareTestVariableData(
114                $deliveryResultIdentifier,
115                $test,
116                $testVariable,
117                $callIdTest
118            );
119        }
120
121        $this->insertMultiple($dataToInsert);
122    }
123
124    /**
125     * @inheritdoc
126     * Stores the item in table and its value in key/value storage.
127     */
128    public function storeItemVariable($deliveryResultIdentifier, $test, $item, Variable $itemVariable, $callIdItem)
129    {
130        $this->storeItemVariables($deliveryResultIdentifier, $test, $item, [$itemVariable], $callIdItem);
131    }
132
133    /**
134     * @inheritdoc
135     * Stores the item variables in table and their values in key/value storage.
136     */
137    public function storeItemVariables($deliveryResultIdentifier, $test, $item, array $itemVariables, $callIdItem)
138    {
139        $dataToInsert = [];
140
141        foreach ($itemVariables as $itemVariable) {
142            $dataToInsert[] = $this->prepareItemVariableData(
143                $deliveryResultIdentifier,
144                $test,
145                $item,
146                $itemVariable,
147                $callIdItem
148            );
149        }
150
151        $this->insertMultiple($dataToInsert);
152    }
153
154    /**
155     * Force update existing variable
156     *
157     * @param array $itemVariables - [
158     *    '{{variable_id}}' => (Object of taoResultServer_models_classes_Variable)
159     * ]
160     * @return void
161     */
162    public function replaceItemVariables(
163        string $deliveryResultIdentifier,
164        string $testUri,
165        string $itemUri,
166        string $callIdItem,
167        array $itemVariables
168    ): void {
169        $update = [];
170
171        foreach ($itemVariables as $itemVariableId => $itemVariable) {
172            $update[] = [
173                'conditions' => [
174                    self::VARIABLES_TABLE_ID => $itemVariableId,
175                ],
176                'updateValues' => $this->prepareItemVariableData(
177                    $deliveryResultIdentifier,
178                    $testUri,
179                    $itemUri,
180                    $itemVariable,
181                    $callIdItem
182                )
183            ];
184        }
185
186        $this->getPersistence()->updateMultiple(self::VARIABLES_TABLENAME, $update);
187    }
188
189    /**
190     * Force update existing variable
191     *
192     * @param array $testVariables - [
193     *    '{{variable_id}}' => (Object of taoResultServer_models_classes_Variable)
194     * ]
195     * @return void
196     */
197    public function replaceTestVariables(
198        string $deliveryResultIdentifier,
199        string $testUri,
200        string $callIdTest,
201        array $testVariables
202    ): void {
203        $update = [];
204
205        foreach ($testVariables as $testVariableId => $testVariable) {
206            $update[] = [
207                'conditions' => [
208                    self::VARIABLES_TABLE_ID => $testVariableId,
209                ],
210                'updateValues' => $this->prepareTestVariableData(
211                    $deliveryResultIdentifier,
212                    $testUri,
213                    $testVariable,
214                    $callIdTest
215                )
216            ];
217        }
218
219        $this->getPersistence()->updateMultiple(self::VARIABLES_TABLENAME, $update);
220    }
221
222    public function storeRelatedTestTaker($deliveryResultIdentifier, $testTakerIdentifier)
223    {
224        $this->storeRelatedData($deliveryResultIdentifier, self::TEST_TAKER_COLUMN, $testTakerIdentifier);
225    }
226
227    public function storeRelatedDelivery($deliveryResultIdentifier, $deliveryIdentifier)
228    {
229        $this->storeRelatedData($deliveryResultIdentifier, self::DELIVERY_COLUMN, $deliveryIdentifier);
230    }
231
232    /**
233     * Store Delivery corresponding to the current test
234     *
235     * @param string $deliveryResultIdentifier
236     * @param string $relatedField
237     * @param string $relatedIdentifier
238     */
239    public function storeRelatedData($deliveryResultIdentifier, $relatedField, $relatedIdentifier)
240    {
241        $qb = $this->getQueryBuilder()
242            ->select('COUNT(*)')
243            ->from(self::RESULTS_TABLENAME)
244            ->andWhere(self::RESULTS_TABLE_ID . ' = :id')
245            ->setParameter('id', $deliveryResultIdentifier);
246        if ((int)$qb->execute()->fetchColumn() === 0) {
247            $this->getPersistence()->insert(
248                self::RESULTS_TABLENAME,
249                [
250                    self::RESULTS_TABLE_ID => $deliveryResultIdentifier,
251                    $relatedField => $relatedIdentifier,
252                ]
253            );
254        } else {
255            $sqlUpdate = 'UPDATE ' . self::RESULTS_TABLENAME . ' SET ' . $relatedField . ' = ? WHERE ' .
256                self::RESULTS_TABLE_ID . ' = ?';
257            $paramsUpdate = [$relatedIdentifier, $deliveryResultIdentifier];
258            $this->getPersistence()->exec($sqlUpdate, $paramsUpdate);
259        }
260    }
261
262    public function getVariables($callId)
263    {
264        if (!is_array($callId)) {
265            $callId = [$callId];
266        }
267
268        $qb = $this->getQueryBuilder()
269            ->select('*')
270            ->from(self::VARIABLES_TABLENAME)
271            ->andWhere(self::CALL_ID_ITEM_COLUMN . ' IN (:ids) OR ' . self::CALL_ID_TEST_COLUMN . ' IN (:ids)')
272            ->orderBy($this->getVariablesSortingField())
273            ->setParameter('ids', $callId, Connection::PARAM_STR_ARRAY);
274
275        $returnValue = [];
276        foreach ($qb->execute()->fetchAll() as $variable) {
277            $returnValue[$variable[self::VARIABLES_TABLE_ID]][] = $this->getResultRow($variable);
278        }
279
280        return $returnValue;
281    }
282
283    public function getDeliveryVariables($deliveryResultIdentifier)
284    {
285        if (!is_array($deliveryResultIdentifier)) {
286            $deliveryResultIdentifier = [$deliveryResultIdentifier];
287        }
288
289        $qb = $this->getQueryBuilder()
290            ->select('*')
291            ->from(self::VARIABLES_TABLENAME)
292            ->andWhere(self::VARIABLES_FK_COLUMN . ' IN (:ids)')
293            ->orderBy($this->getVariablesSortingField())
294            ->setParameter('ids', $deliveryResultIdentifier, Connection::PARAM_STR_ARRAY);
295
296        $returnValue = [];
297        foreach ($qb->execute()->fetchAll() as $variable) {
298            $returnValue[$variable[self::VARIABLES_TABLE_ID]][] = $this->getResultRow($variable);
299        }
300
301        return $returnValue;
302    }
303
304    public function getVariable($callId, $variableIdentifier)
305    {
306        $qb = $this->getQueryBuilder()
307            ->select('*')
308            ->from(self::VARIABLES_TABLENAME)
309            ->andWhere(self::CALL_ID_ITEM_COLUMN . ' = :callId OR ' . self::CALL_ID_TEST_COLUMN . ' = :callId')
310            ->andWhere(self::VARIABLE_IDENTIFIER . ' = :variableId')
311            ->setParameter('callId', $callId)
312            ->setParameter('variableId', $variableIdentifier);
313
314        $returnValue = [];
315        foreach ($qb->execute()->fetchAll() as $variable) {
316            $returnValue[$variable[self::VARIABLES_TABLE_ID]] = $this->getResultRow($variable);
317        }
318
319        return $returnValue;
320    }
321
322    public function getVariableProperty($variableId, $property)
323    {
324        $qb = $this->getQueryBuilder()
325            ->select(self::VARIABLE_VALUE)
326            ->from(self::VARIABLES_TABLENAME)
327            ->andWhere(self::VARIABLES_TABLE_ID . ' = :variableId')
328            ->setParameter('variableId', $variableId);
329
330        $variableValue = $qb->execute()->fetchColumn();
331        $variableValue = $this->unserializeVariableValue($variableValue);
332        $getter = 'get' . ucfirst($property);
333        if (is_callable([$variableValue, $getter])) {
334            return $variableValue->$getter();
335        }
336
337        return null;
338    }
339
340    /**
341     * Returns the field to sort item and test variables.
342     *
343     * @return string
344     */
345    abstract protected function getVariablesSortingField();
346
347    public function getTestTaker($deliveryResultIdentifier)
348    {
349        return $this->getRelatedData($deliveryResultIdentifier, self::TEST_TAKER_COLUMN);
350    }
351
352    public function getDelivery($deliveryResultIdentifier)
353    {
354        return $this->getRelatedData($deliveryResultIdentifier, self::DELIVERY_COLUMN);
355    }
356
357    /**
358     * Retrieves data related to a result.
359     *
360     * @param string $deliveryResultIdentifier
361     * @param string $field
362     *
363     * @return mixed
364     */
365    public function getRelatedData($deliveryResultIdentifier, $field)
366    {
367        $qb = $this->getQueryBuilder()
368            ->select($field)
369            ->from(self::RESULTS_TABLENAME)
370            ->andWhere(self::RESULTS_TABLE_ID . ' = :id')
371            ->setParameter('id', $deliveryResultIdentifier);
372
373        return $qb->execute()->fetchColumn();
374    }
375
376    /**
377     * @inheritdoc
378     * o(n) do not use real time (postprocessing)
379     */
380    public function getAllCallIds()
381    {
382        $qb = $this->getQueryBuilder()
383            ->select(
384                'DISTINCT(' . self::CALL_ID_ITEM_COLUMN . '), ' .
385                self::CALL_ID_TEST_COLUMN . ', ' . self::VARIABLES_FK_COLUMN
386            )
387            ->from(self::VARIABLES_TABLENAME);
388
389        $returnValue = [];
390        foreach ($qb->execute()->fetchAll() as $value) {
391            $returnValue[] = ($value[self::CALL_ID_ITEM_COLUMN] != '')
392                ? $value[self::CALL_ID_ITEM_COLUMN]
393                : $value[self::CALL_ID_TEST_COLUMN];
394        }
395
396        return $returnValue;
397    }
398
399    public function getRelatedItemCallIds($deliveryResultIdentifier)
400    {
401        return $this->getRelatedCallIds($deliveryResultIdentifier, self::CALL_ID_ITEM_COLUMN);
402    }
403
404    public function getRelatedTestCallIds($deliveryResultIdentifier)
405    {
406        return $this->getRelatedCallIds($deliveryResultIdentifier, self::CALL_ID_TEST_COLUMN);
407    }
408
409    public function getRelatedCallIds($deliveryResultIdentifier, $field)
410    {
411        $qb = $this->getQueryBuilder()
412            ->select('DISTINCT(' . $field . ')')
413            ->from(self::VARIABLES_TABLENAME)
414            ->andWhere(self::VARIABLES_FK_COLUMN . ' = :id AND ' . $field . ' <> :field')
415            ->setParameter('id', $deliveryResultIdentifier)
416            ->setParameter('field', '');
417
418        $returnValue = [];
419        foreach ($qb->execute()->fetchAll() as $value) {
420            if (isset($value[$field])) {
421                $returnValue[] = $value[$field];
422            }
423        }
424
425        return $returnValue;
426    }
427
428    public function getAllTestTakerIds()
429    {
430        return $this->getAllIds(self::FIELD_TEST_TAKER, self::TEST_TAKER_COLUMN);
431    }
432
433    public function getAllDeliveryIds()
434    {
435        return $this->getAllIds(self::FIELD_DELIVERY, self::DELIVERY_COLUMN);
436    }
437
438    public function getAllIds($fieldName, $field)
439    {
440        $qb = $this->getQueryBuilder()
441            ->select(self::RESULTS_TABLE_ID . ', ' . $field)
442            ->from(self::RESULTS_TABLENAME);
443
444        $returnValue = [];
445        foreach ($qb->execute()->fetchAll() as $value) {
446            $returnValue[] = [
447                self::FIELD_DELIVERY_RESULT => $value[self::RESULTS_TABLE_ID],
448                $fieldName => $value[$field],
449            ];
450        }
451
452        return $returnValue;
453    }
454
455    public function getResultByDelivery($delivery, $options = [])
456    {
457        if (!is_array($delivery)) {
458            $delivery = [$delivery];
459        }
460        $qb = $this->getQueryBuilder()
461            ->select('*')
462            ->from(self::RESULTS_TABLENAME)
463            ->orderBy($this->getOrderField($options), $this->getOrderDirection($options));
464
465        if (isset($options['offset'])) {
466            $qb->setFirstResult($options['offset']);
467        }
468        if (isset($options['limit'])) {
469            $qb->setMaxResults($options['limit']);
470        }
471
472        if (count($delivery) > 0) {
473            $qb
474                ->andWhere(self::DELIVERY_COLUMN . ' IN (:delivery)')
475                ->setParameter(':delivery', $delivery, Connection::PARAM_STR_ARRAY);
476        }
477
478        $returnValue = [];
479        foreach ($qb->execute()->fetchAll() as $value) {
480            $returnValue[] = [
481                self::FIELD_DELIVERY_RESULT => $value[self::RESULTS_TABLE_ID],
482                self::FIELD_TEST_TAKER => $value[self::TEST_TAKER_COLUMN],
483                self::FIELD_DELIVERY => $value[self::DELIVERY_COLUMN],
484            ];
485        }
486
487        return $returnValue;
488    }
489
490    /**
491     * Generates and sanitize ORDER BY field.
492     *
493     * @param array $options
494     *
495     * @return string
496     */
497    protected function getOrderField(array $options)
498    {
499        $allowedOrderFields = [self::DELIVERY_COLUMN, self::TEST_TAKER_COLUMN, self::RESULTS_TABLE_ID];
500
501        if (isset($options['order']) && in_array($options['order'], $allowedOrderFields)) {
502            return $options['order'];
503        }
504
505        return self::RESULTS_TABLE_ID;
506    }
507
508    /**
509     * Generates and sanitize ORDER BY direction.
510     *
511     * @param array $options
512     *
513     * @return string
514     */
515    protected function getOrderDirection(array $options)
516    {
517        $allowedOrderDirections = ['ASC', 'DESC'];
518
519        if (isset($options['orderdir']) && in_array(strtoupper($options['orderdir']), $allowedOrderDirections)) {
520            return $options['orderdir'];
521        }
522
523        return 'ASC';
524    }
525
526    public function countResultByDelivery($delivery)
527    {
528        if (!is_array($delivery)) {
529            $delivery = [$delivery];
530        }
531        $qb = $this->getQueryBuilder()
532            ->select('COUNT(*)')
533            ->from(self::RESULTS_TABLENAME);
534
535        if (count($delivery) > 0) {
536            $qb
537                ->andWhere(self::DELIVERY_COLUMN . ' IN (:delivery)')
538                ->setParameter('delivery', $delivery, Connection::PARAM_STR_ARRAY);
539        }
540
541        return $qb->execute()->fetchColumn();
542    }
543
544    public function deleteResult($deliveryResultIdentifier)
545    {
546        // remove variables
547        $sql = 'DELETE FROM ' . self::VARIABLES_TABLENAME . '
548            WHERE ' . self::VARIABLES_FK_COLUMN . ' = ?';
549
550        if ($this->getPersistence()->exec($sql, [$deliveryResultIdentifier]) === false) {
551            return false;
552        }
553
554        // remove results
555        $sql = 'DELETE FROM ' . self::RESULTS_TABLENAME . '
556            WHERE ' . self::RESULTS_TABLE_ID . ' = ?';
557
558        if ($this->getPersistence()->exec($sql, [$deliveryResultIdentifier]) === false) {
559            return false;
560        }
561
562        return true;
563    }
564
565    /**
566     * Prepares data to be inserted in database.
567     *
568     * @param string $deliveryResultIdentifier
569     * @param string $test
570     * @param string $item
571     * @param Variable $variable
572     * @param string $callId
573     *
574     * @return array
575     */
576    protected function prepareItemVariableData($deliveryResultIdentifier, $test, $item, Variable $variable, $callId)
577    {
578        $variableData = $this->prepareVariableData($deliveryResultIdentifier, $test, $variable, $callId);
579        $variableData[self::ITEM_COLUMN] = $item;
580        $variableData[self::CALL_ID_ITEM_COLUMN] = $callId;
581
582        return $variableData;
583    }
584
585    /**
586     * Prepares data to be inserted in database.
587     *
588     * @param string $deliveryResultIdentifier
589     * @param string $test
590     * @param Variable $variable
591     * @param string $callId
592     *
593     * @return array
594     */
595    protected function prepareTestVariableData($deliveryResultIdentifier, $test, Variable $variable, $callId)
596    {
597        $variableData = $this->prepareVariableData($deliveryResultIdentifier, $test, $variable, $callId);
598        $variableData[self::CALL_ID_TEST_COLUMN] = $callId;
599
600        return $variableData;
601    }
602
603    /**
604     * Prepares data to be inserted in database.
605     *
606     * @param string $deliveryResultIdentifier
607     * @param string $test
608     * @param Variable $variable
609     * @param string $callId
610     *
611     * @return array
612     */
613    protected function prepareVariableData($deliveryResultIdentifier, $test, Variable $variable, $callId)
614    {
615        // Ensures that variable has epoch.
616        if (!$variable->isSetEpoch()) {
617            $variable->setEpoch(microtime());
618        }
619
620        return $this->prepareVariableDataForSchema($deliveryResultIdentifier, $test, $variable, $callId);
621    }
622
623    /**
624     * Prepares data to be inserted in database according to a given schema.
625     *
626     * @param string $deliveryResultIdentifier
627     * @param string $test
628     * @param Variable $variable
629     * @param string $callId
630     *
631     * @return array
632     */
633    abstract protected function prepareVariableDataForSchema(
634        $deliveryResultIdentifier,
635        $test,
636        Variable $variable,
637        $callId
638    );
639
640    /**
641     * Builds a variable from database row.
642     *
643     * @param array $variable
644     *
645     * @return \stdClass
646     */
647    protected function getResultRow($variable)
648    {
649        $resultVariable = $this->unserializeVariableValue($variable[self::VARIABLE_VALUE]);
650        $object = new \stdClass();
651        $object->uri = $variable[self::VARIABLES_TABLE_ID];
652        $object->class = get_class($resultVariable);
653        $object->deliveryResultIdentifier = $variable[self::VARIABLES_FK_COLUMN];
654        $object->callIdItem = $variable[self::CALL_ID_ITEM_COLUMN];
655        $object->callIdTest = $variable[self::CALL_ID_TEST_COLUMN];
656        $object->test = $variable[self::TEST_COLUMN];
657        $object->item = $variable[self::ITEM_COLUMN];
658        $object->variable = clone $resultVariable;
659
660        return $object;
661    }
662
663    /**
664     * @return QueryBuilder
665     */
666    protected function getQueryBuilder()
667    {
668        return $this->getPersistence()->getPlatform()->getQueryBuilder();
669    }
670
671    /**
672     * @return \common_persistence_SqlPersistence
673     */
674    public function getPersistence()
675    {
676        if ($this->persistence === null) {
677            $persistenceId = $this->hasOption(self::OPTION_PERSISTENCE) ?
678                $this->getOption(self::OPTION_PERSISTENCE)
679                : 'default';
680            $this->persistence = $this->getServiceLocator()
681                ->get(PersistenceManager::SERVICE_ID)
682                ->getPersistenceById($persistenceId);
683        }
684
685        return $this->persistence;
686    }
687
688    /**
689     * @param $value
690     *
691     * @return mixed
692     */
693    protected function unserializeVariableValue($value)
694    {
695        $unserializedValue = json_decode($value, true);
696
697        if (json_last_error() === JSON_ERROR_NONE) {
698            return Variable::fromData($unserializedValue);
699        }
700
701        return unserialize(
702            $value,
703            [
704                'allowed_classes' => [
705                    \taoResultServer_models_classes_ResponseVariable::class,
706                    \taoResultServer_models_classes_OutcomeVariable::class,
707                    \taoResultServer_models_classes_TraceVariable::class,
708                ],
709            ]
710        );
711    }
712
713    /**
714     * @param $value
715     *
716     * @return string
717     */
718    protected function serializeVariableValue($value)
719    {
720        if (!$value instanceof \taoResultServer_models_classes_Variable) {
721            throw new \LogicException(
722                sprintf(
723                    "Value cannot be serialized. Expected instance of '%s', '%s' received.",
724                    \taoResultServer_models_classes_Variable::class,
725                    gettype($value)
726                )
727            );
728        }
729
730        return json_encode($value);
731    }
732
733    /**
734     * @param array $data
735     *
736     * @param array $types
737     * @throws DuplicateVariableException
738     */
739    private function insertMultiple(array $data, array $types = [])
740    {
741        if (empty($types)) {
742            $types = $this->getTypes($data);
743        }
744        $duplicatedData = false;
745        try {
746            $this->getPersistence()->insertMultiple(self::VARIABLES_TABLENAME, $data, $types);
747        } catch (UniqueConstraintViolationException $e) {
748            $duplicatedData = true;
749            foreach ($data as $row) {
750                try {
751                    $this->getPersistence()->insert(self::VARIABLES_TABLENAME, $row, $types);
752                } catch (UniqueConstraintViolationException $e) {
753                    //do nothing, just skip it
754                }
755            }
756        }
757
758        if ($duplicatedData) {
759            throw new DuplicateVariableException(sprintf('An identical result variable already exists.'));
760        }
761    }
762
763    public function spawnResult()
764    {
765        $this->getLogger()->error('Unsupported function');
766    }
767
768    /*
769     * retrieve specific parameters from the resultserver to configure the storage
770     */
771    public function configure($callOptions = [])
772    {
773        $this->getLogger()->info('configure  RdsResultStorage with options : ' . implode(' ', $callOptions));
774    }
775
776    /**
777     *
778     * @param mixed $a
779     * @param mixed $b
780     *
781     * @return number
782     */
783    public static function sortTimeStamps($a, $b)
784    {
785        [$usec, $sec] = explode(' ', $a);
786        $floata = ((float)$usec + (float)$sec);
787        [$usec, $sec] = explode(' ', $b);
788        $floatb = ((float)$usec + (float)$sec);
789        //the callback is expecting an int returned, for the case where the difference is of less than a second
790        if ((floatval($floata) - floatval($floatb)) > 0) {
791            return 1;
792        } elseif ((floatval($floata) - floatval($floatb)) < 0) {
793            return -1;
794        } else {
795            return 0;
796        }
797    }
798
799    /**
800     * Creates the table for results storage.
801     *
802     * @param Schema $schema
803     *
804     * @return Table
805     * @throws SchemaException
806     */
807    public function createResultsTable(Schema $schema)
808    {
809        $table = $schema->createtable(self::RESULTS_TABLENAME);
810        $table->addOption('engine', 'MyISAM');
811
812        $table->addColumn(self::RESULTS_TABLE_ID, 'string', ['length' => 255]);
813        $table->addColumn(self::TEST_TAKER_COLUMN, 'string', ['notnull' => false, 'length' => 255]);
814        $table->addColumn(self::DELIVERY_COLUMN, 'string', ['notnull' => false, 'length' => 255]);
815
816        $table->setPrimaryKey([self::RESULTS_TABLE_ID]);
817
818        return $table;
819    }
820
821    /**
822     * Creates the table for variables storage.
823     *
824     * @param Schema $schema
825     *
826     * @return Table
827     * @throws SchemaException
828     */
829    abstract public function createVariablesTable(Schema $schema);
830
831    /**
832     * Adds constraints for the tables.
833     *
834     * @param Table $variablesTable
835     * @param Table $resultsTable
836     *
837     * @throws SchemaException
838     */
839    public function createTableConstraints(Table $variablesTable, Table $resultsTable)
840    {
841        $variablesTable->addForeignKeyConstraint(
842            $resultsTable,
843            [self::VARIABLES_FK_COLUMN],
844            [self::RESULTS_TABLE_ID],
845            [],
846            self::VARIABLES_FK_NAME
847        );
848    }
849    protected function getTypes(array $data = []): array
850    {
851        return [
852            ParameterType::STRING,
853            ParameterType::STRING,
854            ParameterType::STRING,
855            ParameterType::STRING,
856            ParameterType::STRING,
857            ParameterType::STRING,
858            null,
859            ParameterType::STRING,
860        ];
861    }
862}