001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.model.AssociationModel;
004    import de.deepamehta.core.model.IndexMode;
005    import de.deepamehta.core.model.RelatedAssociationModel;
006    import de.deepamehta.core.model.RelatedTopicModel;
007    import de.deepamehta.core.model.SimpleValue;
008    import de.deepamehta.core.model.TopicModel;
009    import de.deepamehta.core.service.ResultList;
010    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
011    import de.deepamehta.core.storage.spi.DeepaMehtaStorage;
012    
013    import static java.util.Arrays.asList;
014    import java.util.Iterator;
015    import java.util.List;
016    import java.util.logging.Logger;
017    
018    
019    
020    public class StorageDecorator {
021    
022        // ---------------------------------------------------------------------------------------------- Instance Variables
023    
024        private DeepaMehtaStorage storage;
025    
026        private final Logger logger = Logger.getLogger(getClass().getName());
027    
028        // ---------------------------------------------------------------------------------------------------- Constructors
029    
030        public StorageDecorator(DeepaMehtaStorage storage) {
031            this.storage = storage;
032        }
033    
034        // -------------------------------------------------------------------------------------------------- Public Methods
035    
036    
037    
038        // === Topics ===
039    
040        /**
041         * @return  The fetched topic.
042         *          Note: its composite value is not initialized.
043         */
044        TopicModel fetchTopic(long topicId) {
045            return storage.fetchTopic(topicId);
046        }
047    
048        /**
049         * Looks up a single topic by exact property value.
050         * If no such topic exists <code>null</code> is returned.
051         * If more than one topic were found a runtime exception is thrown.
052         * <p>
053         * IMPORTANT: Looking up a topic this way requires the property to be indexed with indexing mode <code>KEY</code>.
054         * This is achieved by declaring the respective data field with <code>indexing_mode: "KEY"</code>
055         * (for statically declared data field, typically in <code>types.json</code>) or
056         * by calling DataField's {@link DataField#setIndexingMode} method with <code>"KEY"</code> as argument
057         * (for dynamically created data fields, typically in migration classes).
058         *
059         * @return  The fetched topic.
060         *          Note: its composite value is not initialized.
061         */
062        TopicModel fetchTopic(String key, SimpleValue value) {
063            return storage.fetchTopic(key, value.value());
064        }
065    
066        List<TopicModel> fetchTopics(String key, SimpleValue value) {
067            return storage.fetchTopics(key, value.value());
068        }
069    
070        // ---
071    
072        /**
073         * @return  The fetched topics.
074         *          Note: their composite values are not initialized.
075         */
076        List<TopicModel> queryTopics(String searchTerm, String fieldUri) {
077            return storage.queryTopics(fieldUri, searchTerm);
078        }
079    
080        // ---
081    
082        Iterator<TopicModel> fetchAllTopics() {
083            return storage.fetchAllTopics();
084        }
085    
086        // ---
087    
088        /**
089         * Creates a topic.
090         * <p>
091         * The topic's URI is stored and indexed.
092         *
093         * @return  FIXDOC ### the created topic. Note:
094         *          - the topic URI   is initialzed and     persisted.
095         *          - the topic value is initialzed but not persisted.
096         *          - the type URI    is initialzed but not persisted.
097         */
098        void storeTopic(TopicModel model) {
099            storage.storeTopic(model);
100        }
101    
102        /**
103         * Stores and indexes the topic's URI.
104         */
105        void storeTopicUri(long topicId, String uri) {
106            storage.storeTopicUri(topicId, uri);
107        }
108    
109        void storeTopicTypeUri(long topicId, String topicTypeUri) {
110            storage.storeTopicTypeUri(topicId, topicTypeUri);
111        }
112    
113        // ---
114    
115        /**
116         * Convenience method (no indexing).
117         */
118        void storeTopicValue(long topicId, SimpleValue value) {
119            storeTopicValue(topicId, value, asList(IndexMode.OFF), null, null);
120        }
121    
122        /**
123         * Stores the topic's value.
124         * <p>
125         * Note: the value is not indexed automatically. Use the {@link indexTopicValue} method. ### FIXDOC
126         *
127         * @return  The previous value, or <code>null</code> if no value was stored before. ### FIXDOC
128         */
129        void storeTopicValue(long topicId, SimpleValue value, List<IndexMode> indexModes, String indexKey,
130                                                                                          SimpleValue indexValue) {
131            storage.storeTopicValue(topicId, value, indexModes, indexKey, indexValue);
132        }
133    
134        // ---
135    
136        /**
137         * Deletes the topic.
138         * <p>
139         * Prerequisite: the topic has no relations.
140         */
141        void deleteTopic(long topicId) {
142            storage.deleteTopic(topicId);
143        }
144    
145    
146    
147        // === Associations ===
148    
149        AssociationModel fetchAssociation(long assocId) {
150            return storage.fetchAssociation(assocId);
151        }
152    
153        // ---
154    
155        /**
156         * Convenience method (checks singularity).
157         *
158         * Returns the association between two topics, qualified by association type and both role types.
159         * If no such association exists <code>null</code> is returned.
160         * If more than one association exist, a runtime exception is thrown.
161         *
162         * @param   assocTypeUri    Association type filter. Pass <code>null</code> to switch filter off.
163         *                          ### FIXME: for methods with a singular return value all filters should be mandatory
164         */
165        AssociationModel fetchAssociation(String assocTypeUri, long topicId1, long topicId2, String roleTypeUri1,
166                                                                                             String roleTypeUri2) {
167            List<AssociationModel> assocs = fetchAssociations(assocTypeUri, topicId1, topicId2, roleTypeUri1, roleTypeUri2);
168            switch (assocs.size()) {
169            case 0:
170                return null;
171            case 1:
172                return assocs.get(0);
173            default:
174                throw new RuntimeException("Ambiguity: there are " + assocs.size() + " \"" + assocTypeUri +
175                    "\" associations (topicId1=" + topicId1 + ", topicId2=" + topicId2 + ", " +
176                    "roleTypeUri1=\"" + roleTypeUri1 + "\", roleTypeUri2=\"" + roleTypeUri2 + "\")");
177            }
178        }
179    
180        /**
181         * Returns the associations between two topics. If no such association exists an empty set is returned.
182         *
183         * @param   assocTypeUri    Association type filter. Pass <code>null</code> to switch filter off.
184         */
185        List<AssociationModel> fetchAssociations(String assocTypeUri, long topicId1, long topicId2,
186                                                                            String roleTypeUri1, String roleTypeUri2) {
187            return storage.fetchAssociations(assocTypeUri, topicId1, topicId2, roleTypeUri1, roleTypeUri2);
188        }
189    
190        // ---
191    
192        /**
193         * Convenience method (checks singularity).
194         */
195        AssociationModel fetchAssociationBetweenTopicAndAssociation(String assocTypeUri, long topicId, long assocId,
196                                                                         String topicRoleTypeUri, String assocRoleTypeUri) {
197            List<AssociationModel> assocs = fetchAssociationsBetweenTopicAndAssociation(assocTypeUri, topicId, assocId,
198                topicRoleTypeUri, assocRoleTypeUri);
199            switch (assocs.size()) {
200            case 0:
201                return null;
202            case 1:
203                return assocs.get(0);
204            default:
205                throw new RuntimeException("Ambiguity: there are " + assocs.size() + " \"" + assocTypeUri +
206                    "\" associations (topicId=" + topicId + ", assocId=" + assocId + ", " +
207                    "topicRoleTypeUri=\"" + topicRoleTypeUri + "\", assocRoleTypeUri=\"" + assocRoleTypeUri + "\")");
208            }
209        }
210    
211        List<AssociationModel> fetchAssociationsBetweenTopicAndAssociation(String assocTypeUri, long topicId,
212                                                           long assocId, String topicRoleTypeUri, String assocRoleTypeUri) {
213            return storage.fetchAssociationsBetweenTopicAndAssociation(assocTypeUri, topicId, assocId, topicRoleTypeUri,
214                assocRoleTypeUri);
215        }
216    
217        // ---
218    
219        Iterator<AssociationModel> fetchAllAssociations() {
220            return storage.fetchAllAssociations();
221        }
222    
223        // ---
224    
225        /**
226         * Stores and indexes the association's URI.
227         */
228        void storeAssociationUri(long assocId, String uri) {
229            storage.storeAssociationUri(assocId, uri);
230        }
231    
232        void storeAssociationTypeUri(long assocId, String assocTypeUri) {
233            storage.storeAssociationTypeUri(assocId, assocTypeUri);
234        }
235    
236        void storeRoleTypeUri(long assocId, long playerId, String roleTypeUri) {
237            storage.storeRoleTypeUri(assocId, playerId, roleTypeUri);
238        }
239    
240        // ---
241    
242        /**
243         * Convenience method (no indexing).
244         */
245        void storeAssociationValue(long assocId, SimpleValue value) {
246            storeAssociationValue(assocId, value, asList(IndexMode.OFF), null, null);
247        }
248    
249        /**
250         * Stores the association's value.
251         * <p>
252         * Note: the value is not indexed automatically. Use the {@link indexAssociationValue} method. ### FIXDOC
253         *
254         * @return  The previous value, or <code>null</code> if no value was stored before. ### FIXDOC
255         */
256        void storeAssociationValue(long assocId, SimpleValue value, List<IndexMode> indexModes, String indexKey,
257                                                                                                SimpleValue indexValue) {
258            storage.storeAssociationValue(assocId, value, indexModes, indexKey, indexValue);
259        }
260    
261        // ---
262    
263        void storeAssociation(AssociationModel model) {
264            storage.storeAssociation(model);
265        }
266    
267        void deleteAssociation(long assocId) {
268            storage.deleteAssociation(assocId);
269        }
270    
271    
272    
273        // === Traversal ===
274    
275        /**
276         * @return  The fetched associations.
277         *          Note: their composite values are not initialized.
278         */
279        List<AssociationModel> fetchTopicAssociations(long topicId) {
280            return storage.fetchTopicAssociations(topicId);
281        }
282    
283        List<AssociationModel> fetchAssociationAssociations(long assocId) {
284            return storage.fetchAssociationAssociations(assocId);
285        }
286    
287        // ---
288    
289        /**
290         * Convenience method (checks singularity).
291         *
292         * @param   assocTypeUri        may be null
293         * @param   myRoleTypeUri       may be null
294         * @param   othersRoleTypeUri   may be null
295         * @param   othersTopicTypeUri  may be null
296         *
297         * @return  The fetched topics.
298         *          Note: their composite values are not initialized.
299         */
300        RelatedTopicModel fetchTopicRelatedTopic(long topicId, String assocTypeUri, String myRoleTypeUri,
301                                                               String othersRoleTypeUri, String othersTopicTypeUri) {
302            ResultList<RelatedTopicModel> topics = fetchTopicRelatedTopics(topicId, assocTypeUri, myRoleTypeUri,
303                othersRoleTypeUri, othersTopicTypeUri, 0);
304            switch (topics.getSize()) {
305            case 0:
306                return null;
307            case 1:
308                return topics.iterator().next();
309            default:
310                throw new RuntimeException("Ambiguity: there are " + topics.getSize() + " related topics (topicId=" +
311                    topicId + ", assocTypeUri=\"" + assocTypeUri + "\", myRoleTypeUri=\"" + myRoleTypeUri + "\", " +
312                    "othersRoleTypeUri=\"" + othersRoleTypeUri + "\", othersTopicTypeUri=\"" + othersTopicTypeUri + "\")");
313            }
314        }
315    
316        /**
317         * @param   assocTypeUri        may be null
318         * @param   myRoleTypeUri       may be null
319         * @param   othersRoleTypeUri   may be null
320         * @param   othersTopicTypeUri  may be null
321         *
322         * @return  The fetched topics.
323         *          Note: their composite values are not initialized.
324         */
325        ResultList<RelatedTopicModel> fetchTopicRelatedTopics(long topicId, String assocTypeUri,
326                                                                    String myRoleTypeUri, String othersRoleTypeUri,
327                                                                    String othersTopicTypeUri, int maxResultSize) {
328            List<RelatedTopicModel> relTopics = storage.fetchTopicRelatedTopics(topicId, assocTypeUri, myRoleTypeUri,
329                othersRoleTypeUri, othersTopicTypeUri);
330            // ### TODO: respect maxResultSize
331            return new ResultList(relTopics.size(), relTopics);
332        }
333    
334        /**
335         * Convenience method (receives *list* of association types).
336         *
337         * @param   assocTypeUris       may *not* be null
338         * @param   myRoleTypeUri       may be null
339         * @param   othersRoleTypeUri   may be null
340         * @param   othersTopicTypeUri  may be null
341         *
342         * @return  The fetched topics.
343         *          Note: their composite values are not initialized.
344         */
345        ResultList<RelatedTopicModel> fetchTopicRelatedTopics(long topicId, List<String> assocTypeUris,
346                                                                    String myRoleTypeUri, String othersRoleTypeUri,
347                                                                    String othersTopicTypeUri, int maxResultSize) {
348            ResultList<RelatedTopicModel> result = new ResultList();
349            for (String assocTypeUri : assocTypeUris) {
350                ResultList<RelatedTopicModel> res = fetchTopicRelatedTopics(topicId, assocTypeUri, myRoleTypeUri,
351                    othersRoleTypeUri, othersTopicTypeUri, maxResultSize);
352                result.addAll(res);
353            }
354            return result;
355        }
356    
357        // ---
358    
359        /**
360         * Convenience method (checks singularity).
361         *
362         * @return  The fetched association.
363         *          Note: its composite value is not initialized.
364         */
365        RelatedAssociationModel fetchTopicRelatedAssociation(long topicId, String assocTypeUri, String myRoleTypeUri,
366                                                                    String othersRoleTypeUri, String othersAssocTypeUri) {
367            List<RelatedAssociationModel> assocs = fetchTopicRelatedAssociations(topicId, assocTypeUri, myRoleTypeUri,
368                othersRoleTypeUri, othersAssocTypeUri);
369            switch (assocs.size()) {
370            case 0:
371                return null;
372            case 1:
373                return assocs.get(0);
374            default:
375                throw new RuntimeException("Ambiguity: there are " + assocs.size() + " related associations (topicId=" +
376                    topicId + ", assocTypeUri=\"" + assocTypeUri + "\", myRoleTypeUri=\"" + myRoleTypeUri + "\", " +
377                    "othersRoleTypeUri=\"" + othersRoleTypeUri + "\", othersAssocTypeUri=\"" + othersAssocTypeUri + "\")");
378            }
379        }
380    
381        /**
382         * @param   assocTypeUri        may be null
383         * @param   myRoleTypeUri       may be null
384         * @param   othersRoleTypeUri   may be null
385         * @param   othersAssocTypeUri  may be null
386         *
387         * @return  The fetched associations.
388         *          Note: their composite values are not initialized.
389         */
390        List<RelatedAssociationModel> fetchTopicRelatedAssociations(long topicId, String assocTypeUri,
391                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
392            return storage.fetchTopicRelatedAssociations(topicId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
393                othersAssocTypeUri);
394        }
395    
396        // ---
397    
398        /**
399         * Convenience method (checks singularity).
400         *
401         * @return  The fetched topics.
402         *          Note: their composite values are not initialized.
403         */
404        RelatedTopicModel fetchAssociationRelatedTopic(long assocId, String assocTypeUri, String myRoleTypeUri,
405                                                              String othersRoleTypeUri, String othersTopicTypeUri) {
406            ResultList<RelatedTopicModel> topics = fetchAssociationRelatedTopics(assocId, assocTypeUri, myRoleTypeUri,
407                othersRoleTypeUri, othersTopicTypeUri, 0);
408            switch (topics.getSize()) {
409            case 0:
410                return null;
411            case 1:
412                return topics.iterator().next();
413            default:
414                throw new RuntimeException("Ambiguity: there are " + topics.getSize() + " related topics (assocId=" +
415                    assocId + ", assocTypeUri=\"" + assocTypeUri + "\", myRoleTypeUri=\"" + myRoleTypeUri + "\", " +
416                    "othersRoleTypeUri=\"" + othersRoleTypeUri + "\", othersTopicTypeUri=\"" + othersTopicTypeUri + "\")");
417            }
418        }
419    
420        /**
421         * @return  The fetched topics.
422         *          Note: their composite values are not initialized.
423         */
424        ResultList<RelatedTopicModel> fetchAssociationRelatedTopics(long assocId, String assocTypeUri,
425                                                                          String myRoleTypeUri, String othersRoleTypeUri,
426                                                                          String othersTopicTypeUri, int maxResultSize) {
427            List<RelatedTopicModel> relTopics = storage.fetchAssociationRelatedTopics(assocId, assocTypeUri, myRoleTypeUri,
428                othersRoleTypeUri, othersTopicTypeUri);
429            // ### TODO: respect maxResultSize
430            return new ResultList(relTopics.size(), relTopics);
431        }
432    
433        /**
434         * Convenience method (receives *list* of association types).
435         *
436         * @param   assocTypeUris       may be null
437         * @param   myRoleTypeUri       may be null
438         * @param   othersRoleTypeUri   may be null
439         * @param   othersTopicTypeUri  may be null
440         *
441         * @return  The fetched topics.
442         *          Note: their composite values are not initialized.
443         */
444        ResultList<RelatedTopicModel> fetchAssociationRelatedTopics(long assocId, List<String> assocTypeUris,
445                                                                          String myRoleTypeUri, String othersRoleTypeUri,
446                                                                          String othersTopicTypeUri, int maxResultSize) {
447            ResultList<RelatedTopicModel> result = new ResultList();
448            for (String assocTypeUri : assocTypeUris) {
449                ResultList<RelatedTopicModel> res = fetchAssociationRelatedTopics(assocId, assocTypeUri, myRoleTypeUri,
450                    othersRoleTypeUri, othersTopicTypeUri, maxResultSize);
451                result.addAll(res);
452            }
453            return result;
454        }
455    
456        // ---
457    
458        /**
459         * Convenience method (checks singularity).
460         *
461         * @return  The fetched association.
462         *          Note: its composite value is not initialized.
463         */
464        RelatedAssociationModel fetchAssociationRelatedAssociation(long assocId, String assocTypeUri,
465                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
466            List<RelatedAssociationModel> assocs = fetchAssociationRelatedAssociations(assocId, assocTypeUri, myRoleTypeUri,
467                othersRoleTypeUri, othersAssocTypeUri);
468            switch (assocs.size()) {
469            case 0:
470                return null;
471            case 1:
472                return assocs.get(0);
473            default:
474                throw new RuntimeException("Ambiguity: there are " + assocs.size() + " related associations (assocId=" +
475                    assocId + ", assocTypeUri=\"" + assocTypeUri + "\", myRoleTypeUri=\"" + myRoleTypeUri + "\", " +
476                    "othersRoleTypeUri=\"" + othersRoleTypeUri + "\", othersAssocTypeUri=\"" + othersAssocTypeUri +
477                    "\"),\nresult=" + assocs);
478            }
479        }
480    
481        /**
482         * @param   assocTypeUri        may be null
483         * @param   myRoleTypeUri       may be null
484         * @param   othersRoleTypeUri   may be null
485         * @param   othersAssocTypeUri  may be null
486         *
487         * @return  The fetched associations.
488         *          Note: their composite values are not initialized.
489         */
490        List<RelatedAssociationModel> fetchAssociationRelatedAssociations(long assocId, String assocTypeUri,
491                                                String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
492            return storage.fetchAssociationRelatedAssociations(assocId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
493                othersAssocTypeUri);
494        }
495    
496        // ---
497    
498        /**
499         * Convenience method (checks singularity).
500         *
501         * @param   id                  id of a topic or an association
502         * @param   assocTypeUri        may be null
503         * @param   myRoleTypeUri       may be null
504         * @param   othersRoleTypeUri   may be null
505         * @param   othersTopicTypeUri  may be null
506         *
507         * @return  The fetched topic.
508         *          Note: its composite value is not initialized.
509         */
510        RelatedTopicModel fetchRelatedTopic(long id, String assocTypeUri, String myRoleTypeUri,
511                                                   String othersRoleTypeUri, String othersTopicTypeUri) {
512            ResultList<RelatedTopicModel> topics = fetchRelatedTopics(id, assocTypeUri, myRoleTypeUri,
513                othersRoleTypeUri, othersTopicTypeUri);
514            switch (topics.getSize()) {
515            case 0:
516                return null;
517            case 1:
518                return topics.iterator().next();
519            default:
520                throw new RuntimeException("Ambiguity: there are " + topics.getSize() + " related topics (id=" + id +
521                    ", assocTypeUri=\"" + assocTypeUri + "\", myRoleTypeUri=\"" + myRoleTypeUri + "\", " +
522                    "othersRoleTypeUri=\"" + othersRoleTypeUri + "\", othersTopicTypeUri=\"" + othersTopicTypeUri + "\")");
523            }
524        }
525    
526        /**
527         * @param   id                  id of a topic or an association
528         * @param   assocTypeUri        may be null
529         * @param   myRoleTypeUri       may be null
530         * @param   othersRoleTypeUri   may be null
531         * @param   othersTopicTypeUri  may be null
532         *
533         * @return  The fetched topics.
534         *          Note: their composite values are not initialized.
535         */
536        ResultList<RelatedTopicModel> fetchRelatedTopics(long id, String assocTypeUri, String myRoleTypeUri,
537                                                               String othersRoleTypeUri, String othersTopicTypeUri) {
538            List<RelatedTopicModel> relTopics = storage.fetchRelatedTopics(id, assocTypeUri, myRoleTypeUri,
539                othersRoleTypeUri, othersTopicTypeUri);
540            return new ResultList(relTopics.size(), relTopics);
541        }
542    
543        // ### TODO: decorator for fetchRelatedAssociations()
544    
545    
546    
547        // === Properties ===
548    
549        Object fetchTopicProperty(long topicId, String propUri) {
550            return storage.fetchTopicProperty(topicId, propUri);
551        }
552    
553        Object fetchAssociationProperty(long assocId, String propUri) {
554            return storage.fetchAssociationProperty(assocId, propUri);
555        }
556    
557        // ---
558    
559        List<TopicModel> fetchTopicsByProperty(String propUri, Object propValue) {
560            return storage.fetchTopicsByProperty(propUri, propValue);
561        }
562    
563        List<TopicModel> fetchTopicsByPropertyRange(String propUri, Number from, Number to) {
564            return storage.fetchTopicsByPropertyRange(propUri, from, to);
565        }
566    
567        List<AssociationModel> fetchAssociationsByProperty(String propUri, Object propValue) {
568            return storage.fetchAssociationsByProperty(propUri, propValue);
569        }
570    
571        List<AssociationModel> fetchAssociationsByPropertyRange(String propUri, Number from, Number to) {
572            return storage.fetchAssociationsByPropertyRange(propUri, from, to);
573        }
574    
575        // ---
576    
577        void storeTopicProperty(long topicId, String propUri, Object propValue, boolean addToIndex) {
578            storage.storeTopicProperty(topicId, propUri, propValue, addToIndex);
579        }
580    
581        void storeAssociationProperty(long assocId, String propUri, Object propValue, boolean addToIndex) {
582            storage.storeAssociationProperty(assocId, propUri, propValue, addToIndex);
583        }
584    
585        // ---
586    
587        boolean hasTopicProperty(long topicId, String propUri) {
588            return storage.hasTopicProperty(topicId, propUri);
589        }
590    
591        boolean hasAssociationProperty(long assocId, String propUri) {
592            return storage.hasAssociationProperty(assocId, propUri);
593        }
594    
595        // ---
596    
597        void removeTopicProperty(long topicId, String propUri) {
598            storage.deleteTopicProperty(topicId, propUri);
599        }
600    
601        void removeAssociationProperty(long assocId, String propUri) {
602            storage.deleteAssociationProperty(assocId, propUri);
603        }
604    
605    
606    
607        // === DB ===
608    
609        DeepaMehtaTransaction beginTx() {
610            return storage.beginTx();
611        }
612    
613        /**
614         * Initializes the database.
615         * Prerequisite: there is an open transaction.
616         *
617         * @return  <code>true</code> if a clean install is detected, <code>false</code> otherwise.
618         */
619        boolean init() {
620            boolean isCleanInstall = storage.setupRootNode();
621            if (isCleanInstall) {
622                logger.info("Starting with a fresh DB -- Setting migration number to 0");
623                storeMigrationNr(0);
624            }
625            return isCleanInstall;
626        }
627    
628        void shutdown() {
629            storage.shutdown();
630        }
631    
632        int fetchMigrationNr() {
633            return (Integer) storage.fetchTopicProperty(0, "core_migration_nr");
634        }
635    
636        void storeMigrationNr(int migrationNr) {
637            storage.storeTopicProperty(0, "core_migration_nr", migrationNr, false);     // addToIndex=false
638        }
639    }