Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee559389 authored by Marten Gajda's avatar Marten Gajda
Browse files

Add VERSION column, implements #778

This commit adds a read-only task VERSION column which is incremented upon any update or a task (including sync-adapter updates).

The column serves two purposes:

* safe transactions by asserting a specific version before updating a task
* quick detection if a specific task has been modified

The former could be used by sync-adapters to ensure a task has not been updated while being synced.
Notifications will make use of the latter to avoid unnecessary updates of task notifications for unchanged tasks.
parent 8cd309ec
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -471,6 +471,18 @@ public final class TaskContract
         */
        String _ID = "_id";

        /**
         * The local version number of this task. The only guarantee about the value is, it's incremented whenever the task changes (this includes any
         * changes applied by sync adapters).
         * <p>
         * Note, there is no guarantee about how much it's incremented other than by at least 1.
         * <p>
         * Value: Integer
         * <p>
         * read-only
         */
        String VERSION = "version";

        /**
         * The id of the list this task belongs to. This value is <strong>write-once</strong> and must not be <code>null</code>.
         * <p>
+133 −13
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import org.dmfs.opentaskspal.tasks.StatusData;
import org.dmfs.opentaskspal.tasks.SyncIdData;
import org.dmfs.opentaskspal.tasks.TimeData;
import org.dmfs.opentaskspal.tasks.TitleData;
import org.dmfs.opentaskspal.tasks.VersionData;
import org.dmfs.opentaskstestpal.InstanceTestData;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.Duration;
@@ -147,7 +148,9 @@ public class TaskProviderTest

        ), resultsIn(mClient,
                new Assert<>(taskList, new NameData("list1")),
                new Assert<>(task, new TitleData("task1")),
                new Assert<>(task, new Composite<>(
                        new TitleData("task1"),
                        new VersionData(0))),
                new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
                                new InstanceTestData(0),
@@ -173,7 +176,9 @@ public class TaskProviderTest

        ), resultsIn(mClient,
                new Assert<>(taskList, new NameData("list1")),
                new Assert<>(task, new TitleData("task updated")),
                new Assert<>(task, new Composite<>(
                        new TitleData("task updated"),
                        new VersionData(1))),
                new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
                                new InstanceTestData(0),
@@ -205,9 +210,71 @@ public class TaskProviderTest
        ), resultsIn(mClient,
                new Assert<>(taskList1, new NameData("list1")),
                new Assert<>(taskList2, new NameData("list2")),
                new Assert<>(task1, new TitleData("task1")),
                new Assert<>(task2, new TitleData("task2")),
                new Assert<>(task3, new TitleData("task3")),
                new Assert<>(task1, new Composite<>(
                        new TitleData("task1"),
                        new VersionData(0))),
                new Assert<>(task2, new Composite<>(
                        new TitleData("task2"),
                        new VersionData(0))),
                new Assert<>(task3, new Composite<>(
                        new TitleData("task3"),
                        new VersionData(0))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task1,
                        new Composite<Instances>(
                                new InstanceTestData(0),
                                new CharSequenceRowData<>(Tasks.TZ, null))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task2,
                        new Composite<Instances>(
                                new InstanceTestData(0),
                                new CharSequenceRowData<>(Tasks.TZ, null))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task3,
                        new Composite<Instances>(
                                new InstanceTestData(0),
                                new CharSequenceRowData<>(Tasks.TZ, null))
                )));
    }


    /**
     * Create 2 task list and 3 tasks with updates, check values.
     */
    @Test
    public void testMultipleInsertsAndUpdates()
    {
        Table<TaskLists> taskListsTable = new LocalTaskListsTable(mAuthority);
        RowSnapshot<TaskLists> taskList1 = new VirtualRowSnapshot<>(taskListsTable);
        RowSnapshot<TaskLists> taskList2 = new VirtualRowSnapshot<>(taskListsTable);
        RowSnapshot<Tasks> task1 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority)));
        RowSnapshot<Tasks> task2 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority)));
        RowSnapshot<Tasks> task3 = new VirtualRowSnapshot<>(new TaskListScoped(taskList2, new TasksTable(mAuthority)));

        assertThat(new Seq<>(
                new Put<>(taskList1, new NameData("list1")),
                new Put<>(taskList2, new NameData("list2")),
                new Put<>(task1, new TitleData("task1a")),
                new Put<>(task2, new TitleData("task2a")),
                new Put<>(task3, new TitleData("task3a")),
                // update task 1 and 2
                new Put<>(task1, new TitleData("task1b")),
                new Put<>(task2, new TitleData("task2b")),
                // update task 1 once more
                new Put<>(task1, new TitleData("task1c"))

        ), resultsIn(mClient,
                new Assert<>(taskList1, new NameData("list1")),
                new Assert<>(taskList2, new NameData("list2")),
                new Assert<>(task1, new Composite<>(
                        new TitleData("task1c"),
                        new VersionData(2))),
                new Assert<>(task2, new Composite<>(
                        new TitleData("task2b"),
                        new VersionData(1))),
                new Assert<>(task3, new Composite<>(
                        new TitleData("task3a"),
                        new VersionData(0))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task1,
                        new Composite<Instances>(
@@ -244,7 +311,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(start, due))

        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(start, due)),
                new Assert<>(task, new Composite<>(
                        new TimeData(start, due),
                        new VersionData(0))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -276,7 +345,46 @@ public class TaskProviderTest
                // update the status of the new task
                new Put<>(task, new StatusData(Tasks.STATUS_COMPLETED))
        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(start, due)),
                new Assert<>(task, new Composite<>(
                        new TimeData(start, due),
                        new VersionData(1))), // task has been updated once
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
                                new InstanceTestData(
                                        start.shiftTimeZone(TimeZone.getDefault()),
                                        due.shiftTimeZone(TimeZone.getDefault()),
                                        absent(),
                                        -1),
                                new CharSequenceRowData<>(Tasks.TZ, "UTC"))
                )));
    }


    /**
     * Create task with start and due, check datetime and INSTANCE_STATUS values after updating the task twice.
     */
    @Test
    public void testInsertTaskWithStartAndDueUpdateTwice()
    {
        RowSnapshot<TaskLists> taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
        RowSnapshot<Tasks> task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));

        DateTime start = DateTime.now();
        DateTime due = start.addDuration(new Duration(1, 1, 0));

        assertThat(new Seq<>(
                new Put<>(taskList, new EmptyRowData<>()),
                new Put<>(task, new TimeData(start, due)),
                // update the status of the new task
                new Put<>(task, new StatusData(Tasks.STATUS_COMPLETED)),
                // update the title of the new task
                new Put<>(task, new TitleData("Task Title"))
        ), resultsIn(mClient,
                new Assert<>(task, new Composite<>(
                        new TimeData(start, due),
                        new TitleData("Task Title"),
                        new VersionData(2))), // task has been updated twice
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -311,7 +419,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(start, due)),
                new Put<>(task, new TimeData(startNew, dueNew))
        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(startNew, dueNew)),
                new Assert<>(task, new Composite<>(
                        new TimeData(startNew, dueNew),
                        new VersionData(1))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -346,7 +456,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(start, due)),
                new Put<>(task, new TimeData(startNew, dueNew))
        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(startNew, dueNew)),
                new Assert<>(task, new Composite<>(
                        new TimeData(startNew, dueNew),
                        new VersionData(1))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -377,7 +489,9 @@ public class TaskProviderTest
                new Put<>(task, new TitleData("Test")),
                new Put<>(task, new TimeData(start, due))
        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(start, due)),
                new Assert<>(task, new Composite<>(
                        new TimeData(start, due),
                        new VersionData(1))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -409,7 +523,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(start, duration))

        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(start, duration)),
                new Assert<>(task, new Composite<>(
                        new TimeData(start, duration),
                        new VersionData(0))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
@@ -444,7 +560,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(startNew, duration))

        ), resultsIn(mClient,
                new Assert<>(task, new TimeData(startNew, duration)),
                new Assert<>(task, new Composite<>(
                        new TimeData(startNew, duration),
                        new VersionData(1))),
                // note that, apart from the time zone, all values stay the same
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
@@ -485,7 +603,9 @@ public class TaskProviderTest
                new Put<>(task, new TimeData(start, due2))

        ), resultsIn(queue,
                new Assert<>(task, new TimeData(start, due2)),
                new Assert<>(task, new Composite<>(
                        new TimeData(start, due2),
                        new VersionData(1))),
                new AssertRelated<>(
                        new InstanceTable(mAuthority), Instances.TASK_ID, task,
                        new Composite<Instances>(
+20 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
    /**
     * The database version.
     */
    private static final int DATABASE_VERSION = 19;
    private static final int DATABASE_VERSION = 20;


    /**
@@ -364,6 +364,14 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
                    + " DELETE FROM " + Tables.PROPERTIES + " WHERE " + Properties.TASK_ID + "= OLD." + Tasks._ID + ";"
                    + " END;";

    /**
     * SQL command to create a trigger to increment task version number on every update.
     */
    private final static String SQL_CREATE_TASK_VERSION_TRIGGER =
            "CREATE TRIGGER task_version_trigger BEFORE UPDATE ON " + Tables.TASKS + " BEGIN "
                    + " UPDATE " + Tables.TASKS + " SET " + Tasks.VERSION + " = OLD." + Tasks.VERSION + " + 1 where " + Tasks._ID + " = NEW." + Tasks._ID + ";"
                    + " END;";

    /**
     * SQL command to create the task list table.
     */
@@ -396,6 +404,7 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
    private final static String SQL_CREATE_TASKS_TABLE =
            "CREATE TABLE " + Tables.TASKS + " ( "
                    + TaskContract.Tasks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                    + TaskContract.Tasks.VERSION + " INTEGER DEFAULT 0,"
                    + TaskContract.Tasks.LIST_ID + " INTEGER NOT NULL, "
                    + TaskContract.Tasks.TITLE + " TEXT,"
                    + TaskContract.Tasks.LOCATION + " TEXT,"
@@ -596,6 +605,9 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
                + TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new."
                + TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END");

        // create task version update trigger
        db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER);

        // create instances table and view
        db.execSQL(SQL_CREATE_INSTANCES_TABLE);

@@ -811,6 +823,13 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
            db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
        }

        if (oldVersion < 20)
        {
            // create task version column and update trigger
            db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.VERSION + " Integer default 0;");
            db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER);
        }

        // upgrade FTS
        FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion);

+6 −1
Original line number Diff line number Diff line
@@ -21,8 +21,8 @@ import android.database.Cursor;

import org.dmfs.provider.tasks.model.adapters.BinaryFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.DateTimeIterableFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.DateTimeFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.DateTimeIterableFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.DurationFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
@@ -46,6 +46,11 @@ public interface TaskAdapter extends EntityAdapter<TaskAdapter>
     */
    LongFieldAdapter<TaskAdapter> _ID = new LongFieldAdapter<TaskAdapter>(Tasks._ID);

    /**
     * Adapter for the version of a task.
     */
    LongFieldAdapter<TaskAdapter> VERSION = new LongFieldAdapter<>(Tasks.VERSION);

    /**
     * Adapter for the task row id of as instance.
     */
+5 −0
Original line number Diff line number Diff line
@@ -115,6 +115,11 @@ public final class Validating implements EntityProcessor<TaskAdapter>
            throw new IllegalArgumentException("_ID can not be set manually");
        }

        if (task.isUpdated(TaskAdapter.VERSION))
        {
            throw new IllegalArgumentException("VERSION can not be set manually");
        }

        // account name can not be set on a tasks
        if (task.isUpdated(TaskAdapter.ACCOUNT_NAME))
        {
Loading