1<?php
2
3
4namespace ComboStrap\Meta\Store;
5
6
7use ComboStrap\DatabasePageRow;
8use ComboStrap\ExceptionBadArgument;
9use ComboStrap\ExceptionCompile;
10use ComboStrap\ExceptionNotExists;
11use ComboStrap\ExceptionNotFound;
12use ComboStrap\ExceptionRuntime;
13use ComboStrap\ExceptionRuntimeInternal;
14use ComboStrap\ExceptionSqliteNotAvailable;
15use ComboStrap\LogUtility;
16use ComboStrap\MarkupPath;
17use ComboStrap\Meta\Api\Metadata;
18use ComboStrap\Meta\Store\MetadataDokuWikiStore;
19use ComboStrap\Meta\Api\MetadataStore;
20use ComboStrap\Meta\Api\MetadataStoreAbs;
21use ComboStrap\Meta\Api\MetadataTabular;
22use ComboStrap\ResourceCombo;
23use ComboStrap\Sqlite;
24
25/**
26 * Class MetadataDbStore
27 * @package ComboStrap
28 * The database store
29 * TODO: {@link DatabasePageRow} should be integrated into MetadataDbStore
30 *   A tabular metadata should be created to get all {@link DatabasePageRow::getMetaRecord()}
31 */
32class MetadataDbStore extends MetadataStoreAbs implements MetadataStore
33{
34    const CANONICAL = "database";
35
36    /**
37     * @var DatabasePageRow[]
38     */
39    private static array $dbRows = [];
40    private Sqlite $sqlite;
41
42    /**
43     * @var Metadata - the uid metadata
44     * They are here to throw at construct time
45     */
46    private Metadata $resourceUidMeta;
47    /**
48     * @var mixed - the uid metadata value
49     * They are here to throw at construct time
50     */
51    private $resourceUidMetaValue;
52
53    /**
54     * @throws ExceptionSqliteNotAvailable
55     * @throws ExceptionNotExists - if the resource does not exist in the database
56     */
57    public function __construct(ResourceCombo $resource)
58    {
59        // sqlite in the constructor to handle only one sqlite exception
60        $this->sqlite = Sqlite::createOrGetSqlite();
61
62        // uid of the resoure (the old page id)
63        $this->resourceUidMeta = $resource->getUid();
64        $persistentName = $this->resourceUidMeta::getPersistentName();
65        /**
66         * If  uid is null, it's not yet in the database
67         * and returns the default or null, or empty array
68         */
69        $this->resourceUidMetaValue = MetadataDokuWikiStore::getOrCreateFromResource($resource)
70            ->getFromName($persistentName);
71
72        parent::__construct($resource);
73    }
74
75
76    /**
77     * @throws ExceptionNotExists
78     * @throws ExceptionSqliteNotAvailable
79     */
80    static function getOrCreateFromResource(ResourceCombo $resourceCombo): MetadataStore
81    {
82        return new MetadataDbStore($resourceCombo);
83    }
84
85    public static function resetAll()
86    {
87        self::$dbRows = [];
88    }
89
90    public function set(Metadata $metadata)
91    {
92        if ($metadata instanceof MetadataTabular) {
93
94            $this->syncTabular($metadata);
95            return;
96        }
97
98        throw new ExceptionRuntime("The metadata ($metadata) is not yet supported on set", self::CANONICAL);
99
100    }
101
102    public function get(Metadata $metadata, $default = null)
103    {
104
105        $resource = $metadata->getResource();
106        if (!($resource instanceof MarkupPath)) {
107            throw new ExceptionRuntime("The resource type ({$resource->getType()}) is not yet supported for the database metadata store", self::CANONICAL);
108        }
109
110        if ($metadata instanceof MetadataTabular) {
111
112            return $this->getDbTabularData($metadata);
113
114        } else {
115
116            $pageMetaFromFileSystem = MarkupPath::createPageFromAbsoluteId($resource->getPathObject()->toAbsoluteId());
117            $fsStore = MetadataDokuWikiStore::getOrCreateFromResource($pageMetaFromFileSystem);
118            $pageMetaFromFileSystem->setReadStore($fsStore);
119
120            $database = DatabasePageRow::getOrCreateFromPageObject($pageMetaFromFileSystem);
121            if (!$database->exists()) {
122                return null;
123            }
124            return $database->getFromRow($metadata->getName());
125
126        }
127    }
128
129    /**
130     *
131     */
132    private function syncTabular(MetadataTabular $metadata)
133    {
134
135        try {
136            $uid = $metadata->getUidObject();
137        } catch (ExceptionBadArgument $e) {
138            throw new ExceptionRuntimeInternal("The uid class should be defined for the metadata ($metadata)");
139        }
140
141        $sourceRows = $metadata->toStoreValue();
142        if ($sourceRows === null) {
143            return;
144        }
145
146        $targetRows = $this->getDbTabularData($metadata);
147        foreach ($targetRows as $targetRow) {
148            $targetRowId = $targetRow[$uid::getPersistentName()];
149            if (isset($sourceRows[$targetRowId])) {
150                unset($sourceRows[$targetRowId]);
151            } else {
152                $this->deleteRow($targetRow, $metadata);
153            }
154        }
155
156        foreach ($sourceRows as $sourceRow) {
157            $this->addRow($sourceRow, $metadata);
158        }
159
160    }
161
162
163    /**
164     * @param array $row
165     * @param Metadata $metadata
166     * @return void
167     */
168    private function addRow(array $row, Metadata $metadata): void
169    {
170
171        /**
172         * Add the id
173         */
174        $resourceCombo = $metadata->getResource();
175        $resourceUidObject = $resourceCombo->getUidObject();
176        try {
177            $uidValue = $resourceUidObject->getValue();
178        } catch (ExceptionNotFound $e) {
179            // not yet in db
180            return;
181        }
182
183        $row[$resourceUidObject::getPersistentName()] = $uidValue;
184
185        $tableName = $this->getTableName($metadata);
186        $request = $this->sqlite
187            ->createRequest()
188            ->setTableRow($tableName, $row);
189        try {
190            $request->execute();
191        } catch (ExceptionCompile $e) {
192            LogUtility::msg("There was a problem during rows insertion for the table ($tableName)" . $e->getMessage());
193        } finally {
194            $request->close();
195        }
196
197
198    }
199
200    /**
201     * @param array $row
202     * @param Metadata $metadata
203     */
204    private function deleteRow(array $row, Metadata $metadata): void
205    {
206        $tableName = $this->getTableName($metadata);
207        $resourceIdAttribute = $metadata->getResource()->getUidObject()::getPersistentName();
208        $metadataIdAttribute = $metadata->getUidObject()::getPersistentName();
209        $delete = <<<EOF
210delete from $tableName where $resourceIdAttribute = ? and $metadataIdAttribute = ?
211EOF;
212
213        $row = [
214            $row[$resourceIdAttribute] ?? null,
215            $row[$metadataIdAttribute] ?? null
216        ];
217        $request = Sqlite::createOrGetSqlite()
218            ->createRequest()
219            ->setQueryParametrized($delete, $row);
220        try {
221            $request->execute();
222        } catch (ExceptionCompile $e) {
223            LogUtility::msg("There was a problem during the row delete of $tableName. Message: {$e->getMessage()}");
224            return;
225        } finally {
226            $request->close();
227        }
228
229
230    }
231
232
233    /**
234     * @return array - the rows
235     * @var Metadata $metadata
236     */
237    private function getDbTabularData(Metadata $metadata): array
238    {
239
240        $uid = $this->resourceUidMeta;
241        $uidValue = $this->resourceUidMetaValue;
242        if ($uidValue === null) {
243            // no yet in the db
244            return [];
245        }
246
247        $uidAttribute = $uid::getPersistentName();
248        $children = $metadata->getChildrenObject();
249        if ($children === null) {
250            throw new ExceptionRuntimeInternal("The children of the tabular metadata ($metadata) should be set to synchronize into the database");
251        }
252        $attributes = [];
253        foreach ($children as $child) {
254            $attributes[] = $child::getPersistentName();
255        }
256        $tableName = $this->getTableName($metadata);
257        $query = Sqlite::createSelectFromTableAndColumns($tableName, $attributes);
258        $query = "$query where $uidAttribute = ? ";
259        $res = $this->sqlite
260            ->createRequest()
261            ->setQueryParametrized($query, [$uidValue]);
262        $rows = [];
263        try {
264            $rows = $res
265                ->execute()
266                ->getRows();
267        } catch (ExceptionCompile $e) {
268            throw new ExceptionRuntimeInternal("An exception has occurred with the $tableName ({$metadata->getResource()}) selection query. Message: {$e->getMessage()}, Query: ($query", self::CANONICAL, 1, $e);
269        } finally {
270            $res->close();
271        }
272        return $rows;
273
274    }
275
276    public function persist()
277    {
278        // there is no notion of commit in the sqlite plugin
279    }
280
281    public function isHierarchicalTextBased(): bool
282    {
283        return false;
284    }
285
286    public function reset()
287    {
288        throw new ExceptionRuntime("To implement");
289    }
290
291    public function getFromName(string $name, $default = null)
292    {
293
294        if ($this->resourceUidMetaValue === null) {
295            // not yet in the db
296            return $default;
297        }
298
299        $row = $this->getDatabaseRow();
300        $value = $row->getFromRow($name);
301        if ($value !== null) {
302            return $value;
303        }
304        return $default;
305    }
306
307    public function setFromPersistentName(string $name, $value, $default = null)
308    {
309        throw new ExceptionRuntime("Not implemented");
310    }
311
312
313    private function getTableName(Metadata $metadata): string
314    {
315        return $metadata->getResource()->getType() . "_" . $metadata::getPersistentName();
316
317    }
318
319    public function getCanonical(): string
320    {
321        return self::CANONICAL;
322    }
323
324    private function getDatabaseRow(): DatabasePageRow
325    {
326        $mapKey = $this->getResource()->getPathObject()->toAbsoluteId();
327        $row = self::$dbRows[$mapKey];
328        if ($row === null) {
329            $page = $this->getResource();
330            $row = DatabasePageRow::getFromPageObject($page);
331            self::$dbRows[$mapKey] = $row;
332        }
333        return $row;
334    }
335
336
337}
338