diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.java deleted file mode 100644 index 5a54a2d5..00000000 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.java +++ /dev/null @@ -1,107 +0,0 @@ -package eu.pretix.libpretixsync.sync; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import eu.pretix.libpretixsync.api.PretixApi; -import eu.pretix.libpretixsync.db.Item; -import eu.pretix.libpretixsync.db.Migrations; -import eu.pretix.libpretixsync.db.Question; -import eu.pretix.libpretixsync.utils.JSONUtils; -import io.requery.BlockingEntityStore; -import io.requery.Persistable; -import io.requery.query.Tuple; -import io.requery.util.CloseableIterator; - -public class QuestionSyncAdapter extends BaseConditionalSyncAdapter { - - public QuestionSyncAdapter(BlockingEntityStore store, FileStorage fileStorage, String eventSlug, PretixApi api, String syncCycleId, SyncManager.ProgressFeedback feedback) { - super(store, fileStorage, eventSlug, api, syncCycleId, feedback); - } - - @Override - public void updateObject(Question obj, JSONObject jsonobj) throws JSONException { - obj.setEvent_slug(eventSlug); - obj.setServer_id(jsonobj.getLong("id")); - obj.setPosition(jsonobj.getLong("position")); - obj.setRequired(jsonobj.optBoolean("required", false)); - obj.setJson_data(jsonobj.toString()); - JSONArray itemsarr = jsonobj.getJSONArray("items"); - List itemids = new ArrayList<>(); - for (int i = 0; i < itemsarr.length(); i++) { - itemids.add(itemsarr.getLong(i)); - } - List items = store.select(Item.class).where( - Item.SERVER_ID.in(itemids) - ).get().toList(); - for (Item item : items) { - if (!obj.getItems().contains(item)) { - obj.getItems().add(item); - } - } - obj.getItems().retainAll(items); - } - - @Override - public CloseableIterator runBatch(List ids) { - return store.select(Question.class) - .where(Question.EVENT_SLUG.eq(eventSlug)) - .and(Question.SERVER_ID.in(ids)) - .get().iterator(); - } - - @Override - CloseableIterator getKnownIDsIterator() { - return store.select(Question.SERVER_ID) - .where(Question.EVENT_SLUG.eq(eventSlug)) - .get().iterator(); - } - - @Override - String getResourceName() { - return "questions"; - } - - @Override - Long getId(JSONObject obj) throws JSONException { - return obj.getLong("id"); - } - - @Override - Long getId(Question obj) { - return obj.getServer_id(); - } - - @Override - Question newEmptyObject() { - return new Question(); - } - - public void standaloneRefreshFromJSON(JSONObject data) throws JSONException { - Question obj = store.select(Question.class) - .where(Question.SERVER_ID.eq(data.getLong("id"))) - .get().firstOr(newEmptyObject()); - JSONObject old = null; - if (obj.getId() != null) { - old = obj.getJSON(); - } - - // Store object - data.put("__libpretixsync_dbversion", Migrations.CURRENT_VERSION); - data.put("__libpretixsync_syncCycleId", syncCycleId); - if (old == null) { - updateObject(obj, data); - store.insert(obj); - } else { - if (!JSONUtils.similar(data, old)) { - updateObject(obj, data); - store.update(obj); - } - } - } -} diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.kt new file mode 100644 index 00000000..3e941f1b --- /dev/null +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/QuestionSyncAdapter.kt @@ -0,0 +1,141 @@ +package eu.pretix.libpretixsync.sync + +import app.cash.sqldelight.TransactionWithoutReturn +import app.cash.sqldelight.db.QueryResult +import eu.pretix.libpretixsync.api.PretixApi +import eu.pretix.libpretixsync.db.Migrations +import eu.pretix.libpretixsync.sqldelight.Question +import eu.pretix.libpretixsync.sqldelight.SyncDatabase +import eu.pretix.libpretixsync.sync.SyncManager.ProgressFeedback +import eu.pretix.libpretixsync.utils.JSONUtils +import org.json.JSONException +import org.json.JSONObject + +class QuestionSyncAdapter( + db: SyncDatabase, + fileStorage: FileStorage, + eventSlug: String, + api: PretixApi, + syncCycleId: String, + feedback: ProgressFeedback?, +) : SqBaseConditionalSyncAdapter( + db = db, + fileStorage = fileStorage, + eventSlug = eventSlug, + api = api, + syncCycleId = syncCycleId, + feedback = feedback, +) { + + override fun getResourceName(): String = "questions" + + override fun getId(obj: Question): Long = obj.server_id!! + + override fun getId(obj: JSONObject): Long = obj.getLong("id") + + override fun getJSON(obj: Question): JSONObject = JSONObject(obj.json_data!!) + + override fun queryKnownIDs(): MutableSet? { + val res = mutableSetOf() + db.questionQueries.selectServerIdsByEventSlug(event_slug = eventSlug).execute { cursor -> + while (cursor.next().value) { + val id = cursor.getLong(0) ?: throw RuntimeException("id column not available") + res.add(id) + } + + QueryResult.Unit + } + + return res + } + + override fun insert(jsonobj: JSONObject) { + val questionId = db.questionQueries.transactionWithResult { + db.questionQueries.insert( + event_slug = eventSlug, + json_data = jsonobj.toString(), + position = jsonobj.getLong("position"), + required = jsonobj.optBoolean("required", false), + server_id = jsonobj.getLong("id"), + ) + db.compatQueries.getLastInsertedQuestionId().executeAsOne() + } + + upsertItemRelations(questionId, emptySet(), jsonobj) + } + + override fun update(obj: Question, jsonobj: JSONObject) { + val existingRelations = db.questionQueries.selectRelationsForQuestion(obj.id) + .executeAsList() + .map { it.ItemId } + .toSet() + + db.questionQueries.updateFromJson( + event_slug = eventSlug, + json_data = jsonobj.toString(), + position = jsonobj.getLong("position"), + required = jsonobj.optBoolean("required", false), + id = obj.id, + ) + + upsertItemRelations(obj.id, existingRelations, jsonobj) + } + + private fun upsertItemRelations(questionId: Long, existingIds: Set, jsonobj: JSONObject) { + val itemsarr = jsonobj.getJSONArray("items") + val itemids = ArrayList(itemsarr.length()) + for (i in 0 until itemsarr.length()) { + itemids.add(itemsarr.getLong(i)) + } + val newIds = db.itemQueries.selectByServerIdListAndEventSlug( + server_id = itemids, + event_slug = eventSlug, + ).executeAsList().map { it.id }.toSet() + + for (newId in newIds - existingIds) { + db.questionQueries.insertItemRelation( + item_id = newId, + question_id = questionId, + ) + } + for (oldId in existingIds - newIds) { + db.questionQueries.deleteItemRelation( + item_id = oldId, + question_id = questionId, + ) + } + } + + override fun delete(key: Long) { + db.questionQueries.deleteItemRelationsForQuestion(key) + db.questionQueries.deleteByServerId(key) + } + + override fun runInTransaction(body: TransactionWithoutReturn.() -> Unit) { + db.questionQueries.transaction(false, body) + } + + override fun runBatch(parameterBatch: List): List = + db.questionQueries.selectByServerIdListAndEventSlug( + server_id = parameterBatch, + event_slug = eventSlug, + ).executeAsList() + + @Throws(JSONException::class) + fun standaloneRefreshFromJSON(data: JSONObject) { + val known = db.questionQueries.selectByServerId(data.getLong("id")).executeAsOneOrNull() + + // Store object + data.put("__libpretixsync_dbversion", Migrations.CURRENT_VERSION) + data.put("__libpretixsync_syncCycleId", syncCycleId) + if (known == null) { + insert(data) + } else { + val old = JSONObject(known.json_data!!) + if (!JSONUtils.similar(data, old)) { + update(known, data) + } + } + } + +} diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java index a2db05a9..c9b5a80a 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/sync/SyncManager.java @@ -423,7 +423,7 @@ protected void downloadData(ProgressFeedback feedback, Boolean skip_orders, Stri } download(new ItemCategorySyncAdapter(db, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback)); download(new ItemSyncAdapter(db, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback)); - download(new QuestionSyncAdapter(dataStore, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback)); + download(new QuestionSyncAdapter(db, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback)); if (profile == Profile.PRETIXPOS) { download(new QuotaSyncAdapter(dataStore, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback, subEvent)); download(new TaxRuleSyncAdapter(db, fileStorage, eventSlug, api, configStore.getSyncCycleId(), feedback)); diff --git a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/Question.sq b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/Question.sq index 7bffa5c4..c928a515 100644 --- a/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/Question.sq +++ b/libpretixsync/src/main/sqldelight/common/eu/pretix/libpretixsync/sqldelight/Question.sq @@ -1,3 +1,8 @@ +selectByServerId: +SELECT * +FROM Question +WHERE server_id = ?; + selectForItem: SELECT Question.* FROM Question @@ -6,3 +11,63 @@ WHERE Question.id IN ( FROM Question_Item WHERE Question_Item.ItemId = :item_id ); + +selectByServerIdListAndEventSlug: +SELECT * +FROM Question +WHERE server_id IN ? AND event_slug = ?; + +selectServerIdsByEventSlug: +SELECT server_id +FROM Question +WHERE event_slug = ?; + +deleteByServerId: +DELETE FROM Question +WHERE server_id = ?; + +insert: +INSERT INTO Question( + event_slug, + json_data, + "position", + required, + server_id +) VALUES( + ?, + ?, + ?, + ?, + ? +); + +selectRelationsForQuestion: +SELECT * +FROM Question_Item +WHERE QuestionId = :question_id; + +insertItemRelation: +INSERT INTO Question_Item( + ItemId, + QuestionId +) VALUES( + :item_id, + :question_id +); + +deleteItemRelation: +DELETE FROM Question_Item +WHERE ItemId = :item_id AND QuestionId = :question_id; + +deleteItemRelationsForQuestion: +DELETE FROM Question_Item +WHERE QuestionId = :question_id; + +updateFromJson: +UPDATE Question +SET + event_slug = ?, + json_data = ?, + "position" = ?, + required = ? +WHERE id = ?; diff --git a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/compat.sq b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/compat.sq index 6e874477..afb63be1 100644 --- a/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/compat.sq +++ b/libpretixsync/src/main/sqldelight/postgres/eu/pretix/libpretixsync/sqldelight/compat.sq @@ -13,6 +13,10 @@ SELECT currval('checkin_id_seq') AS Long; getLastInsertedOrderPositionId: SELECT currval('orderposition_id_seq') AS Long; +-- Switch to RETURNING once it is also supported by SQLite +getLastInsertedQuestionId: +SELECT currval('question_id_seq') AS Long; + truncateAllTables: TRUNCATE BadgeLayout, diff --git a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/compat.sq b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/compat.sq index 2ee9d818..f17c75d1 100644 --- a/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/compat.sq +++ b/libpretixsync/src/main/sqldelight/sqlite/eu/pretix/libpretixsync/sqldelight/compat.sq @@ -13,6 +13,10 @@ SELECT last_insert_rowid(); getLastInsertedOrderPositionId: SELECT last_insert_rowid(); +-- Switch to RETURNING once it is also supported by SQLite +getLastInsertedQuestionId: +SELECT last_insert_rowid(); + truncateAllTables { DELETE FROM BadgeLayout; DELETE FROM BadgeLayoutItem; diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt index 755b7f6e..8f1e83ee 100644 --- a/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/check/AsyncCheckProviderTest.kt @@ -872,7 +872,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionsForOtherItem() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question1.json") ) @@ -882,7 +882,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionNotDuringCheckin() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question3.json") ) @@ -892,7 +892,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionsFilled() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question1.json") ) @@ -902,7 +902,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionsIgnored() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question1.json") ) @@ -912,7 +912,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionsRequired() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question1.json") ) @@ -936,7 +936,7 @@ class AsyncCheckProviderTest : BaseDatabaseTest() { @Test fun testQuestionsInvalidInput() { - QuestionSyncAdapter(dataStore, FakeFileStorage(), "demo", fakeApi, "", null).standaloneRefreshFromJSON( + QuestionSyncAdapter(db, FakeFileStorage(), "demo", fakeApi!!, "", null).standaloneRefreshFromJSON( jsonResource("questions/question2.json") )