org.totschnig.myexpenses.provider.TransactionDatabase.java Source code

Java tutorial

Introduction

Here is the source code for org.totschnig.myexpenses.provider.TransactionDatabase.java

Source

/*   This file is part of My Expenses.
 *   My Expenses is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   My Expenses is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with My Expenses.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.totschnig.myexpenses.provider;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Currency;
import java.util.Locale;

import org.totschnig.myexpenses.MyApplication;
import org.totschnig.myexpenses.R;
import org.totschnig.myexpenses.model.Account;
import org.totschnig.myexpenses.model.AccountType;
import org.totschnig.myexpenses.model.CurrencyEnum;
import org.totschnig.myexpenses.model.Grouping;
import org.totschnig.myexpenses.model.Money;
import org.totschnig.myexpenses.model.PaymentMethod;
import org.totschnig.myexpenses.model.Plan;
import org.totschnig.myexpenses.model.Template;
import org.totschnig.myexpenses.model.Transaction;
import org.totschnig.myexpenses.preference.PrefKey;
import org.totschnig.myexpenses.util.AcraHelper;
import org.totschnig.myexpenses.util.Utils;

import com.android.calendar.CalendarContractCompat.Events;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import static org.totschnig.myexpenses.provider.DatabaseConstants.*;

public class TransactionDatabase extends SQLiteOpenHelper {
    public static final int DATABASE_VERSION = 58;
    public static final String DATABASE_NAME = "data";
    private Context mCtx;

    private static final String TAG = "TransactionDatabase";
    /**
     * SQL statement for expenses TABLE
     * both transactions and transfers are stored in this table
     * for transfers there are two rows (one per account) which
     * are linked by KEY_TRANSFER_PEER
     * for normal transactions KEY_TRANSFER_PEER is set to NULL
     * split parts are linked with their parents through KEY_PARENTID
     * KEY_STATUS has STATUS_EXPORTED if transaction is exported, and
     * STATUS_UNCOMMITTED for transactions that are created during editing of splits
     * KEY_CR_STATUS stores cleared/reconciled
     */
    private static final String DATABASE_CREATE = "CREATE TABLE " + TABLE_TRANSACTIONS + "( " + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_COMMENT + " text, " + KEY_DATE + " datetime not null, "
            + KEY_AMOUNT + " integer not null, " + KEY_CATID + " integer references " + TABLE_CATEGORIES + "("
            + KEY_ROWID + "), " + KEY_ACCOUNTID + " integer not null references " + TABLE_ACCOUNTS + "(" + KEY_ROWID
            + ") ON DELETE CASCADE," + KEY_PAYEEID + " integer references " + TABLE_PAYEES + "(" + KEY_ROWID + "), "
            + KEY_TRANSFER_PEER + " integer references " + TABLE_TRANSACTIONS + "(" + KEY_ROWID + "), "
            + KEY_TRANSFER_ACCOUNT + " integer references " + TABLE_ACCOUNTS + "(" + KEY_ROWID + ")," + KEY_METHODID
            + " integer references " + TABLE_METHODS + "(" + KEY_ROWID + ")," + KEY_PARENTID
            + " integer references " + TABLE_TRANSACTIONS + "(" + KEY_ROWID + ") ON DELETE CASCADE, " + KEY_STATUS
            + " integer default 0, " + KEY_CR_STATUS + " text not null check (" + KEY_CR_STATUS + " in ("
            + Transaction.CrStatus.JOIN + ")) default '" + Transaction.CrStatus.RECONCILED.name() + "',"
            + KEY_REFERENCE_NUMBER + " text, " + KEY_PICTURE_URI + " text);";

    private static String buildViewDefinition(String tableName) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(" AS SELECT ").append(tableName).append(".*, ").append(TABLE_PAYEES).append(".")
                .append(KEY_PAYEE_NAME).append(", ").append(TABLE_METHODS).append(".").append(KEY_LABEL)
                .append(" AS ").append(KEY_METHOD_LABEL);

        if (tableName.equals(TABLE_TRANSACTIONS)) {
            stringBuilder.append(", ").append(TABLE_PLAN_INSTANCE_STATUS).append(".").append(KEY_TEMPLATEID);
        }

        stringBuilder.append(" FROM ").append(tableName).append(" LEFT JOIN ").append(TABLE_PAYEES).append(" ON ")
                .append(KEY_PAYEEID).append(" = ").append(TABLE_PAYEES).append(".").append(KEY_ROWID)
                .append(" LEFT JOIN ").append(TABLE_METHODS).append(" ON ").append(KEY_METHODID).append(" = ")
                .append(TABLE_METHODS).append(".").append(KEY_ROWID);

        if (tableName.equals(TABLE_TRANSACTIONS)) {
            stringBuilder.append(" LEFT JOIN ").append(TABLE_PLAN_INSTANCE_STATUS).append(" ON ").append(tableName)
                    .append(".").append(KEY_ROWID).append(" = ").append(TABLE_PLAN_INSTANCE_STATUS).append(".")
                    .append(KEY_TRANSACTIONID);
        }
        return stringBuilder.toString();
    }

    private static String buildViewDefinitionExtended(String tableName) {
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append(" AS SELECT ").append(tableName).append(".*, ").append(TABLE_PAYEES).append(".")
                .append(KEY_PAYEE_NAME).append(", ").append(KEY_COLOR).append(", ").append(KEY_CURRENCY)
                .append(", ").append(KEY_EXCLUDE_FROM_TOTALS).append(", ").append(TABLE_METHODS).append(".")
                .append(KEY_LABEL).append(" AS ").append(KEY_METHOD_LABEL);

        if (tableName.equals(TABLE_TRANSACTIONS)) {
            stringBuilder.append(", ").append(TABLE_PLAN_INSTANCE_STATUS).append(".").append(KEY_TEMPLATEID);
        }

        stringBuilder.append(" FROM ").append(tableName).append(" LEFT JOIN ").append(TABLE_PAYEES).append(" ON ")
                .append(KEY_PAYEEID).append(" = ").append(TABLE_PAYEES).append(".").append(KEY_ROWID)
                .append(" LEFT JOIN ").append(TABLE_ACCOUNTS).append(" ON ").append(KEY_ACCOUNTID).append(" = ")
                .append(TABLE_ACCOUNTS).append(".").append(KEY_ROWID).append(" LEFT JOIN ").append(TABLE_METHODS)
                .append(" ON ").append(KEY_METHODID).append(" = ").append(TABLE_METHODS).append(".")
                .append(KEY_ROWID);

        if (tableName.equals(TABLE_TRANSACTIONS)) {
            stringBuilder.append(" LEFT JOIN ").append(TABLE_PLAN_INSTANCE_STATUS).append(" ON ").append(tableName)
                    .append(".").append(KEY_ROWID).append(" = ").append(TABLE_PLAN_INSTANCE_STATUS).append(".")
                    .append(KEY_TRANSACTIONID);
        }

        return stringBuilder.toString();
    }

    /**
     * SQL statement for accounts TABLE
     */
    private static final String ACCOUNTS_CREATE = "CREATE TABLE " + TABLE_ACCOUNTS + " (" + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_LABEL + " text not null, " + KEY_OPENING_BALANCE
            + " integer, " + KEY_DESCRIPTION + " text, " + KEY_CURRENCY + " text not null, " + KEY_TYPE
            + " text not null check (" + KEY_TYPE + " in (" + AccountType.JOIN + ")) default '"
            + AccountType.CASH.name() + "', " + KEY_COLOR + " integer default -3355444, " + KEY_GROUPING
            + " text not null check (" + KEY_GROUPING + " in (" + Grouping.JOIN + ")) default '"
            + Grouping.NONE.name() + "', " + KEY_USAGES + " integer default 0," + KEY_LAST_USED + " datetime, "
            + KEY_SORT_KEY + " integer," + KEY_EXCLUDE_FROM_TOTALS + " boolean default 0);";

    /**
     * SQL statement for categories TABLE
     * Table definition reflects format of Grisbis categories
     * Main categories have parent_id 0
     * usages counts how often the cat is selected
     */
    private static final String CATEGORIES_CREATE = "CREATE TABLE " + TABLE_CATEGORIES + " (" + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_LABEL + " text not null, " + KEY_LABEL_NORMALIZED
            + " text," + KEY_PARENTID + " integer references " + TABLE_CATEGORIES + "(" + KEY_ROWID + "), "
            + KEY_USAGES + " integer default 0, " + KEY_LAST_USED + " datetime, " + "unique (" + KEY_LABEL + ","
            + KEY_PARENTID + "));";

    private static final String PAYMENT_METHODS_CREATE = "CREATE TABLE " + TABLE_METHODS + " (" + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_LABEL + " text not null, " + KEY_IS_NUMBERED
            + " boolean default 0, " + KEY_TYPE + " integer " + "check (" + KEY_TYPE + " in ("
            + PaymentMethod.EXPENSE + "," + PaymentMethod.NEUTRAL + "," + PaymentMethod.INCOME + ")) default 0);";

    private static final String ACCOUNTTYE_METHOD_CREATE = "CREATE TABLE " + TABLE_ACCOUNTTYES_METHODS + " ("
            + KEY_TYPE + " text not null check (" + KEY_TYPE + " in (" + AccountType.JOIN + ")), " + KEY_METHODID
            + " integer references " + TABLE_METHODS + "(" + KEY_ROWID + "), " + "primary key (" + KEY_TYPE + ","
            + KEY_METHODID + "));";

    /**
     * {@link DatabaseConstants#KEY_TRANSFER_PEER} does not point to another instance
     * but is a boolean indicating if the template is for a transfer
     * {@link DatabaseConstants#KEY_PLANID} references an event in com.android.providers.calendar
     */
    private static final String TEMPLATE_CREATE = "CREATE TABLE " + TABLE_TEMPLATES + " ( " + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_COMMENT + " text, " + KEY_AMOUNT + " integer not null, "
            + KEY_CATID + " integer references " + TABLE_CATEGORIES + "(" + KEY_ROWID + "), " + KEY_ACCOUNTID
            + " integer not null references " + TABLE_ACCOUNTS + "(" + KEY_ROWID + ") ON DELETE CASCADE,"
            + KEY_PAYEEID + " integer references " + TABLE_PAYEES + "(" + KEY_ROWID + "), " + KEY_TRANSFER_PEER
            + " boolean default 0, " + KEY_TRANSFER_ACCOUNT + " integer references " + TABLE_ACCOUNTS + "("
            + KEY_ROWID + ") ON DELETE CASCADE," + KEY_METHODID + " integer references " + TABLE_METHODS + "("
            + KEY_ROWID + "), " + KEY_TITLE + " text not null, " + KEY_USAGES + " integer default 0, " + KEY_PLANID
            + " integer, " + KEY_PLAN_EXECUTION + " boolean default 0, " + KEY_UUID + " text, " + KEY_LAST_USED
            + " datetime);";

    private static final String EVENT_CACHE_CREATE = "CREATE TABLE " + TABLE_EVENT_CACHE + " ( " + Events.TITLE
            + " TEXT," + Events.DESCRIPTION + " TEXT," + Events.DTSTART + " INTEGER," + Events.DTEND + " INTEGER,"
            + Events.EVENT_TIMEZONE + " TEXT," + Events.DURATION + " TEXT," + Events.ALL_DAY
            + " INTEGER NOT NULL DEFAULT 0," + Events.RRULE + " TEXT," + Events.CUSTOM_APP_PACKAGE + " TEXT,"
            + Events.CUSTOM_APP_URI + " TEXT);";

    /**
     * stores payees and payers
     * this table is used for populating the autocompleting text field,
     */
    private static final String PAYEE_CREATE = "CREATE TABLE " + TABLE_PAYEES + " (" + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_PAYEE_NAME + " text unique not null,"
            + KEY_PAYEE_NAME_NORMALIZED + " text);";

    private static final String CURRENCY_CREATE = "CREATE TABLE " + TABLE_CURRENCIES + " (" + KEY_ROWID
            + " integer primary key autoincrement, " + KEY_CODE + " text unique not null);";

    /**
     * in this table we store links between plan instances and transactions,
     * thus allowing us to track if an instance has been applied, and to allow editing or cancellation of
     * transactions added from plan instances
     */
    private static final String PLAN_INSTANCE_STATUS_CREATE = "CREATE TABLE " + TABLE_PLAN_INSTANCE_STATUS + " ( "
            + KEY_TEMPLATEID + " integer references " + TABLE_TEMPLATES + "(" + KEY_ROWID + ") ON DELETE CASCADE,"
            + KEY_INSTANCEID + " integer," + // references Instances._ID in calendar content provider
            KEY_TRANSACTIONID + " integer references " + TABLE_TRANSACTIONS + "(" + KEY_ROWID
            + ") ON DELETE CASCADE, " + "primary key (" + KEY_INSTANCEID + "," + KEY_TRANSACTIONID + "));";

    private static final String STALE_URIS_CREATE = "CREATE TABLE " + TABLE_STALE_URIS + " ( " + KEY_PICTURE_URI
            + " text);";

    private static final String STALE_URI_TRIGGER_CREATE = "CREATE TRIGGER cache_stale_uri " + "AFTER DELETE ON "
            + TABLE_TRANSACTIONS + " " + "WHEN old." + KEY_PICTURE_URI + " NOT NULL " + "AND NOT EXISTS "
            + "(SELECT 1 FROM " + TABLE_TRANSACTIONS + " " + "WHERE " + KEY_PICTURE_URI + " = old."
            + KEY_PICTURE_URI + ") " + "BEGIN INSERT INTO " + TABLE_STALE_URIS + " VALUES (old." + KEY_PICTURE_URI
            + "); END";

    private static final String ACCOUNTS_TRIGGER_CREATE = "CREATE TRIGGER sort_key_default " + "AFTER INSERT ON "
            + TABLE_ACCOUNTS + " " + "BEGIN UPDATE " + TABLE_ACCOUNTS + " SET " + KEY_SORT_KEY
            + " = (SELECT coalesce(max(" + KEY_SORT_KEY + "),0) FROM " + TABLE_ACCOUNTS + ") + 1 WHERE " + KEY_ROWID
            + " = NEW." + KEY_ROWID + "; END";

    public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
    public static final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);

    TransactionDatabase(Context context) {
        super(context, getDbName(), null, DATABASE_VERSION);
        mCtx = context;
    }

    public static String getDbName() {
        return MyApplication.isInstrumentationTest() ? MyApplication.getTestId() : DATABASE_NAME;
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        //since API 16 we could use onConfigure to enable foreign keys
        //which is run before onUpgrade
        //but this makes upgrades more difficult, since then you have to maintain the constraint in
        //each step of a multi statement upgrade with table rename
        //we stick to doing upgrades with foreign keys disabled which forces us
        //to take care of ensuring consistency during upgrades
        if (!db.isReadOnly()) {
            db.execSQL("PRAGMA foreign_keys=ON;");
        }
        try {
            db.delete(TABLE_TRANSACTIONS, KEY_STATUS + " = " + STATUS_UNCOMMITTED, null);
        } catch (SQLiteException e) {
            AcraHelper.report(e, DbUtils.getTableDetails(db.query("sqlite_master", new String[] { "name", "sql" },
                    "type = 'table'", null, null, null, null)));
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(DATABASE_CREATE);
        db.execSQL(PAYEE_CREATE);
        db.execSQL(PAYMENT_METHODS_CREATE);
        db.execSQL(TEMPLATE_CREATE);
        db.execSQL(PLAN_INSTANCE_STATUS_CREATE);
        String viewTransactions = buildViewDefinition(TABLE_TRANSACTIONS);
        db.execSQL("CREATE VIEW " + VIEW_COMMITTED + viewTransactions + " WHERE " + KEY_STATUS + " != "
                + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW " + VIEW_UNCOMMITTED + viewTransactions + " WHERE " + KEY_STATUS + " = "
                + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW " + VIEW_ALL + viewTransactions);
        db.execSQL("CREATE VIEW " + VIEW_TEMPLATES + buildViewDefinition(TABLE_TEMPLATES));
        db.execSQL(CATEGORIES_CREATE);
        db.execSQL(ACCOUNTS_CREATE);
        db.execSQL("CREATE VIEW " + VIEW_EXTENDED + buildViewDefinitionExtended(TABLE_TRANSACTIONS) + " WHERE "
                + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW " + VIEW_TEMPLATES_EXTENDED + buildViewDefinitionExtended(TABLE_TEMPLATES));
        db.execSQL(ACCOUNTS_TRIGGER_CREATE);
        insertDefaultAccount(db);
        db.execSQL(ACCOUNTTYE_METHOD_CREATE);
        insertDefaultPaymentMethods(db);
        db.execSQL(CURRENCY_CREATE);
        //category for splits needed to honour foreign constraint
        ContentValues initialValues = new ContentValues();
        initialValues.put(KEY_ROWID, SPLIT_CATID);
        initialValues.put(KEY_PARENTID, SPLIT_CATID);
        initialValues.put(KEY_LABEL, "__SPLIT_TRANSACTION__");
        db.insertOrThrow(TABLE_CATEGORIES, null, initialValues);
        insertCurrencies(db);
        db.execSQL(EVENT_CACHE_CREATE);
        db.execSQL(STALE_URIS_CREATE);
        db.execSQL(STALE_URI_TRIGGER_CREATE);
        db.execSQL("CREATE INDEX transactions_cat_id_index on " + TABLE_TRANSACTIONS + "(" + KEY_CATID + ")");
        db.execSQL("CREATE INDEX templates_cat_id_index on " + TABLE_TEMPLATES + "(" + KEY_CATID + ")");
    }

    private void insertCurrencies(SQLiteDatabase db) {
        ContentValues initialValues = new ContentValues();
        for (CurrencyEnum currency : CurrencyEnum.values()) {
            initialValues.put(KEY_CODE, currency.name());
            db.insert(TABLE_CURRENCIES, null, initialValues);
        }
    }

    /**
     * @param db insert the predefined payment methods in the database, all of them are valid only for bank accounts
     */
    private void insertDefaultPaymentMethods(SQLiteDatabase db) {
        ContentValues initialValues;
        long _id;
        for (PaymentMethod.PreDefined pm : PaymentMethod.PreDefined.values()) {
            initialValues = new ContentValues();
            initialValues.put(KEY_LABEL, pm.name());
            initialValues.put(KEY_TYPE, pm.paymentType);
            initialValues.put(KEY_IS_NUMBERED, pm.isNumbered);
            _id = db.insert(TABLE_METHODS, null, initialValues);
            initialValues = new ContentValues();
            initialValues.put(KEY_METHODID, _id);
            initialValues.put(KEY_TYPE, "BANK");
            db.insert(TABLE_ACCOUNTTYES_METHODS, null, initialValues);
        }
    }

    private void insertDefaultAccount(SQLiteDatabase db) {
        ContentValues initialValues = new ContentValues();
        initialValues.put(KEY_LABEL, mCtx.getString(R.string.default_account_name));
        initialValues.put(KEY_OPENING_BALANCE, 0);
        initialValues.put(KEY_DESCRIPTION, mCtx.getString(R.string.default_account_description));
        Currency localCurrency = Utils.getLocalCurrency();
        initialValues.put(KEY_CURRENCY, localCurrency.getCurrencyCode());
        initialValues.put(KEY_TYPE, AccountType.CASH.name());
        initialValues.put(KEY_GROUPING, Grouping.NONE.name());
        initialValues.put(KEY_COLOR, Account.DEFAULT_COLOR);
        db.insert(TABLE_ACCOUNTS, null, initialValues);
        Money.ensureFractionDigitsAreCached(localCurrency);
    }

    /*
    * in onUpgrade, we can not rely on the constants, since we need the statements to be executed as defined
    * as is
    * if we would use the constants, and they change in the future, we would no longer have the same upgrade
    * and this can lead to bugs, if a later upgrade relies on column names as defined earlier,
    * and a user upgrading several versions at once would get a broken upgrade process
    */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        try {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ".");
            if (oldVersion < 17) {
                db.execSQL("drop table accounts");
                db.execSQL("CREATE TABLE accounts (_id integer primary key autoincrement, label text not null, "
                        + "opening_balance integer, description text, currency text not null);");
                //db.execSQL("ALTER TABLE expenses add column account_id integer");
            }

            if (oldVersion < 18) {
                db.execSQL(
                        "CREATE TABLE payee (_id integer primary key autoincrement, name text unique not null);");
                db.execSQL("ALTER TABLE expenses add column payee text");
            }

            if (oldVersion < 19) {
                db.execSQL("ALTER TABLE expenses add column transfer_peer text");
            }

            if (oldVersion < 20) {
                db.execSQL(
                        "CREATE TABLE transactions ( _id integer primary key autoincrement, comment text not null, "
                                + "date datetime not null, amount integer not null, cat_id integer, account_id integer, "
                                + "payee  text, transfer_peer integer default null);");
                db.execSQL("INSERT INTO transactions (comment,date,amount,cat_id,account_id,payee,transfer_peer)"
                        + " SELECT comment,date,CAST(ROUND(amount*100) AS INTEGER),cat_id,account_id,payee,transfer_peer FROM expenses");
                db.execSQL("DROP TABLE expenses");
                db.execSQL("ALTER TABLE accounts RENAME to accounts_old");
                db.execSQL("CREATE TABLE accounts (_id integer primary key autoincrement, label text not null, "
                        + "opening_balance integer, description text, currency text not null);");
                db.execSQL("INSERT INTO accounts (label,opening_balance,description,currency)"
                        + " SELECT label,CAST(ROUND(opening_balance*100) AS INTEGER),description,currency FROM accounts_old");
                db.execSQL("DROP TABLE accounts_old");
            }

            if (oldVersion < 21) {
                db.execSQL(
                        "CREATE TABLE paymentmethods (_id integer primary key autoincrement, label text not null, type integer default 0);");
                db.execSQL(
                        "CREATE TABLE accounttype_paymentmethod (type text, method_id integer, primary key (type,method_id));");
                ContentValues initialValues;
                long _id;
                for (PaymentMethod.PreDefined pm : PaymentMethod.PreDefined.values()) {
                    initialValues = new ContentValues();
                    initialValues.put("label", pm.name());
                    initialValues.put("type", pm.paymentType);
                    _id = db.insert("paymentmethods", null, initialValues);
                    initialValues = new ContentValues();
                    initialValues.put("method_id", _id);
                    initialValues.put("type", "BANK");
                    db.insert("accounttype_paymentmethod", null, initialValues);
                }
                db.execSQL("ALTER TABLE transactions add column payment_method_id integer");
                db.execSQL("ALTER TABLE accounts add column type text default 'CASH'");
            }

            if (oldVersion < 22) {
                db.execSQL("CREATE TABLE templates ( _id integer primary key autoincrement, comment text not null, "
                        + "amount integer not null, cat_id integer, account_id integer, payee text, transfer_peer integer default null, "
                        + "payment_method_id integer, title text not null);");
            }

            if (oldVersion < 23) {
                db.execSQL("ALTER TABLE templates RENAME to templates_old");
                db.execSQL("CREATE TABLE templates ( _id integer primary key autoincrement, comment text not null, "
                        + "amount integer not null, cat_id integer, account_id integer, payee text, transfer_peer integer default null, "
                        + "payment_method_id integer, title text not null, unique(account_id, title));");
                try {
                    db.execSQL(
                            "INSERT INTO templates(comment,amount,cat_id,account_id,payee,transfer_peer,payment_method_id,title)"
                                    + " SELECT comment,amount,cat_id,account_id,payee,transfer_peer,payment_method_id,title FROM templates_old");
                } catch (SQLiteConstraintException e) {
                    Log.e(TAG, e.getLocalizedMessage());
                    //theoretically we could have entered duplicate titles for one account
                    //we silently give up in that case (since this concerns only a narrowly distributed alpha version)
                }
                db.execSQL("DROP TABLE templates_old");
            }

            if (oldVersion < 24) {
                db.execSQL("ALTER TABLE templates add column usages integer default 0");
            }

            if (oldVersion < 25) {
                //for transactions that were not transfers, transfer_peer was set to null in transactions, but to 0 in templates
                db.execSQL("update transactions set transfer_peer=0 WHERE transfer_peer is null;");
            }

            if (oldVersion < 26) {
                db.execSQL("alter table accounts add column color integer default -6697984");
            }

            if (oldVersion < 27) {
                db.execSQL("CREATE TABLE feature_used (feature text not null);");
            }

            if (oldVersion < 28) {
                db.execSQL("ALTER TABLE transactions RENAME to transactions_old");
                db.execSQL(
                        "CREATE TABLE transactions(_id integer primary key autoincrement, comment text, date datetime not null, amount integer not null, "
                                + "cat_id integer references categories(_id), account_id integer not null references accounts(_id),payee text, "
                                + "transfer_peer integer references transactions(_id), transfer_account integer references accounts(_id), "
                                + "method_id integer references paymentmethods(_id));");
                db.execSQL(
                        "INSERT INTO transactions (_id,comment,date,amount,cat_id,account_id,payee,transfer_peer,transfer_account,method_id) "
                                + "SELECT _id,comment,date,amount, "
                                + "CASE WHEN transfer_peer THEN null ELSE CASE WHEN cat_id THEN cat_id ELSE null END END, "
                                + "account_id,payee, "
                                + "CASE WHEN transfer_peer THEN transfer_peer ELSE null END, "
                                + "CASE WHEN transfer_peer THEN cat_id ELSE null END, "
                                + "CASE WHEN payment_method_id THEN payment_method_id ELSE null END "
                                + "FROM transactions_old");
                db.execSQL("ALTER TABLE accounts RENAME to accounts_old");
                db.execSQL(
                        "CREATE TABLE accounts (_id integer primary key autoincrement, label text not null, opening_balance integer, description text, "
                                + "currency text not null, type text not null check (type in ('CASH','BANK','CCARD','ASSET','LIABILITY')) default 'CASH', color integer default -3355444);");
                db.execSQL("INSERT INTO accounts (_id,label,opening_balance,description,currency,type,color) "
                        + "SELECT _id,label,opening_balance,description,currency,type,color FROM accounts_old");
                //previously templates where not deleted if referred to accounts were deleted
                db.execSQL(
                        "DELETE FROM templates where account_id not in (SELECT _id FROM accounts) or (cat_id != 0 and transfer_peer = 1 and cat_id not in (SELECT _id from accounts))");
                db.execSQL("ALTER TABLE templates RENAME to templates_old");
                db.execSQL(
                        "CREATE TABLE templates ( _id integer primary key autoincrement, comment text not null, amount integer not null, "
                                + "cat_id integer references categories(_id), account_id integer not null references accounts(_id),payee text, "
                                + "transfer_peer boolean default false, transfer_account integer references accounts(_id),method_id integer references paymentmethods(_id), "
                                + "title text not null, usages integer default 0, unique(account_id,title));");
                db.execSQL(
                        "INSERT INTO templates (_id,comment,amount,cat_id,account_id,payee,transfer_peer,transfer_account,method_id,title,usages) "
                                + "SELECT _id,comment,amount,"
                                + "CASE WHEN transfer_peer THEN null ELSE CASE WHEN cat_id THEN cat_id ELSE null END END, "
                                + "account_id,payee, " + "CASE WHEN transfer_peer THEN 1 ELSE 0 END, "
                                + "CASE WHEN transfer_peer THEN cat_id ELSE null END, "
                                + "CASE WHEN payment_method_id THEN payment_method_id ELSE null END, "
                                + "title,usages FROM templates_old");
                db.execSQL("ALTER TABLE categories RENAME to categories_old");
                db.execSQL(
                        "CREATE TABLE categories (_id integer primary key autoincrement, label text not null, parent_id integer references categories(_id), "
                                + "usages integer default 0, unique (label,parent_id));");
                db.execSQL("INSERT INTO categories (_id,label,parent_id,usages) "
                        + "SELECT _id,label,CASE WHEN parent_id THEN parent_id ELSE null END,usages FROM categories_old");
                db.execSQL("ALTER TABLE paymentmethods RENAME to paymentmethods_old");
                db.execSQL(
                        "CREATE TABLE paymentmethods (_id integer primary key autoincrement, label text not null, type integer check (type in (-1,0,1)) default 0);");
                db.execSQL(
                        "INSERT INTO paymentmethods (_id,label,type) SELECT _id,label,type FROM paymentmethods_old");
                db.execSQL("ALTER TABLE accounttype_paymentmethod RENAME to accounttype_paymentmethod_old");
                db.execSQL(
                        "CREATE TABLE accounttype_paymentmethod (type text not null check (type in ('CASH','BANK','CCARD','ASSET','LIABILITY')), method_id integer references paymentmethods (_id), primary key (type,method_id));");
                db.execSQL(
                        "INSERT INTO accounttype_paymentmethod (type,method_id) SELECT type,method_id FROM accounttype_paymentmethod_old");
                db.execSQL("DROP TABLE transactions_old");
                db.execSQL("DROP TABLE accounts_old");
                db.execSQL("DROP TABLE templates_old");
                db.execSQL("DROP TABLE categories_old");
                db.execSQL("DROP TABLE paymentmethods_old");
                db.execSQL("DROP TABLE accounttype_paymentmethod_old");
                //Changes to handle
                //1) Transfer account no longer stored as cat_id but in transfer_account (in transactions and templates)
                //2) parent_id for categories uses foreign key on itself, hence root categories have null instead of 0 as parent_id
                //3) catId etc now need to be null instead of 0
                //4) transactions payment_method_id renamed to method_id
            }

            if (oldVersion < 29) {
                db.execSQL("ALTER TABLE transactions add column status integer default 0");
            }

            if (oldVersion < 30) {
                db.execSQL("ALTER TABLE transactions add column parent_id integer references transactions (_id)");
                //      db.execSQL("CREATE VIEW committed AS SELECT * FROM transactions WHERE status != 2;");
                //      db.execSQL("CREATE VIEW uncommitted AS SELECT * FROM transactions WHERE status = 2;");
                ContentValues initialValues = new ContentValues();
                initialValues.put("_id", 0);
                initialValues.put("parent_id", 0);
                initialValues.put("label", "__SPLIT_TRANSACTION__");
                db.insert("categories", null, initialValues);
            }

            if (oldVersion < 31) {
                //in an alpha version distributed on Google Play, we had SPLIT_CATID as -1
                ContentValues initialValues = new ContentValues();
                initialValues.put("_id", 0);
                initialValues.put("parent_id", 0);
                db.update("categories", initialValues, "_id=-1", null);
            }

            if (oldVersion < 32) {
                db.execSQL("ALTER TABLE accounts add column grouping text not null check (grouping in "
                        + "('NONE','DAY','WEEK','MONTH','YEAR')) default 'NONE'");
            }

            if (oldVersion < 33) {
                db.execSQL("ALTER TABLE accounts add column usages integer default 0");
                db.execSQL(
                        "UPDATE accounts SET usages = (SELECT count(*) FROM transactions WHERE account_id = accounts._id AND parent_id IS null)");
            }

            if (oldVersion < 34) {
                //fix for https://github.com/mtotschnig/MyExpenses/issues/69
                db.execSQL(
                        "UPDATE transactions set date = (SELECT date from transactions parent WHERE parent._id = transactions.parent_id) WHERE parent_id IS NOT null");
            }

            if (oldVersion < 35) {
                db.execSQL(
                        "ALTER TABLE transactions add column cr_status text not null check (cr_status in ('UNRECONCILED','CLEARED','RECONCILED')) default 'UNRECONCILED'");
            }

            if (oldVersion < 36) {
                //move payee field in transactions from text to foreign key
                db.execSQL("ALTER TABLE transactions RENAME to transactions_old");
                db.execSQL("CREATE TABLE transactions (" + " _id integer primary key autoincrement,"
                        + " comment text, date datetime not null," + " amount integer not null,"
                        + " cat_id integer references categories(_id),"
                        + " account_id integer not null references accounts(_id),"
                        + " payee_id integer references payee(_id),"
                        + " transfer_peer integer references transactions(_id),"
                        + " transfer_account integer references accounts(_id),"
                        + " method_id integer references paymentmethods(_id),"
                        + " parent_id integer references transactions(_id)," + " status integer default 0,"
                        + " cr_status text not null check (cr_status in ('UNRECONCILED','CLEARED','RECONCILED')) default 'RECONCILED')");
                //insert all payees that are stored in transactions, but are not in payee
                db.execSQL(
                        "INSERT INTO payee (name) SELECT DISTINCT payee FROM transactions_old WHERE payee != '' AND NOT exists (SELECT 1 FROM payee WHERE name=transactions_old.payee)");
                db.execSQL("INSERT INTO transactions "
                        + "(_id,comment,date,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,parent_id,status,cr_status) "
                        + "SELECT " + "_id, " + "comment, " + "date, " + "amount, " + "cat_id, " + "account_id, "
                        + "(SELECT _id from payee WHERE name = payee), " + "transfer_peer, " + "transfer_account, "
                        + "method_id," + "parent_id," + "status," + "cr_status " + "FROM transactions_old");
                db.execSQL("DROP TABLE transactions_old");

                //move payee field in templates from text to foreign key
                db.execSQL("ALTER TABLE templates RENAME to templates_old");
                db.execSQL("CREATE TABLE templates (" + " _id integer primary key autoincrement," + " comment text,"
                        + " amount integer not null," + " cat_id integer references categories(_id),"
                        + " account_id integer not null references accounts(_id),"
                        + " payee_id integer references payee(_id)," + " transfer_peer boolean default false,"
                        + " transfer_account integer references accounts(_id),"
                        + " method_id integer references paymentmethods(_id)," + " title text not null,"
                        + " usages integer default 0," + " unique(account_id,title));");
                //insert all payees that are stored in templates, but are not in payee
                db.execSQL(
                        "INSERT INTO payee (name) SELECT DISTINCT payee FROM templates_old WHERE payee != '' AND NOT exists (SELECT 1 FROM payee WHERE name=templates_old.payee)");
                db.execSQL("INSERT INTO templates "
                        + "(_id,comment,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,title,usages) "
                        + "SELECT " + "_id, " + "comment, " + "amount, " + "cat_id, " + "account_id, "
                        + "(SELECT _id from payee WHERE name = payee), " + "transfer_peer, " + "transfer_account, "
                        + "method_id," + "title," + "usages " + "FROM templates_old");
                db.execSQL("DROP TABLE templates_old");

                db.execSQL("DROP VIEW IF EXISTS committed");
                db.execSQL("DROP VIEW IF EXISTS uncommitted");
                //for the definition of the view, it is safe to rely on the constants,
                //since we will not alter the view, but drop it, and recreate it, if needed
                //      String viewTransactions = VIEW_DEFINITION(TABLE_TRANSACTIONS);
                //      db.execSQL("CREATE VIEW transactions_committed "  + viewTransactions + " WHERE " + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
                //      db.execSQL("CREATE VIEW transactions_uncommitted" + viewTransactions + " WHERE " + KEY_STATUS +  " = " + STATUS_UNCOMMITTED + ";");
                //      db.execSQL("CREATE VIEW transactions_all" + viewTransactions);
                //      db.execSQL("CREATE VIEW templates_all" +  VIEW_DEFINITION(TABLE_TEMPLATES));
            }

            if (oldVersion < 37) {
                db.execSQL("ALTER TABLE transactions add column number text");
                db.execSQL("ALTER TABLE paymentmethods add column is_numbered boolean default 0");
                ContentValues initialValues = new ContentValues();
                initialValues.put("is_numbered", true);
                db.update("paymentmethods", initialValues, "label = ?", new String[] { "CHEQUE" });
            }

            if (oldVersion < 38) {
                db.execSQL("ALTER TABLE templates add column plan_id integer");
                db.execSQL("ALTER TABLE templates add column plan_execution boolean default 0");
            }

            if (oldVersion < 39) {
                //      db.execSQL("CREATE VIEW transactions_extended" + VIEW_DEFINITION_EXTENDED(TABLE_TRANSACTIONS) + " WHERE " + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
                //      db.execSQL("CREATE VIEW templates_extended" +  VIEW_DEFINITION_EXTENDED(TABLE_TEMPLATES));
                db.execSQL(
                        "CREATE TABLE currency (_id integer primary key autoincrement, code text unique not null);");
                insertCurrencies(db);
            }

            if (oldVersion < 40) {
                //added currency to extended view
                db.execSQL("DROP VIEW IF EXISTS transactions_extended");
                db.execSQL("DROP VIEW IF EXISTS templates_extended");
                //      db.execSQL("CREATE VIEW transactions_extended" + VIEW_DEFINITION_EXTENDED(TABLE_TRANSACTIONS) + " WHERE " + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
                //      db.execSQL("CREATE VIEW templates_extended" +  VIEW_DEFINITION_EXTENDED(TABLE_TEMPLATES));
            }

            if (oldVersion < 41) {
                db.execSQL("CREATE TABLE planinstance_transaction "
                        + "(template_id integer references templates(_id), " + "instance_id integer, "
                        + "transaction_id integer references transactions(_id), "
                        + "primary key (instance_id,transaction_id));");
            }

            if (oldVersion < 42) {
                //migrate date field to unix time stamp (UTC)
                db.execSQL("ALTER TABLE transactions RENAME to transactions_old");
                db.execSQL("CREATE TABLE transactions (" + " _id integer primary key autoincrement,"
                        + " comment text, date datetime not null," + " amount integer not null,"
                        + " cat_id integer references categories(_id),"
                        + " account_id integer not null references accounts(_id),"
                        + " payee_id integer references payee(_id),"
                        + " transfer_peer integer references transactions(_id),"
                        + " transfer_account integer references accounts(_id),"
                        + " method_id integer references paymentmethods(_id),"
                        + " parent_id integer references transactions(_id)," + " status integer default 0,"
                        + " cr_status text not null check (cr_status in ('UNRECONCILED','CLEARED','RECONCILED')) default 'RECONCILED',"
                        + " number text)");
                db.execSQL("INSERT INTO transactions "
                        + "(_id,comment,date,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,parent_id,status,cr_status,number) "
                        + "SELECT " + "_id, " + "comment, " + "strftime('%s',date,'utc'), " + "amount, "
                        + "cat_id, " + "account_id, " + "payee_id, " + "transfer_peer, " + "transfer_account, "
                        + "method_id," + "parent_id," + "status," + "cr_status, " + "number "
                        + "FROM transactions_old");
                db.execSQL("DROP TABLE transactions_old");
            }

            if (oldVersion < 43) {
                db.execSQL("UPDATE accounts set currency = 'ZMW' WHERE currency = 'ZMK'");
                db.execSQL("UPDATE currency set code = 'ZMW' WHERE code = 'ZMK'");
            }

            if (oldVersion < 44) {
                //add ON DELETE CASCADE
                //accounts table sort_key column
                db.execSQL("ALTER TABLE planinstance_transaction RENAME to planinstance_transaction_old");
                db.execSQL("CREATE TABLE planinstance_transaction "
                        + "(template_id integer references templates(_id) ON DELETE CASCADE, "
                        + "instance_id integer, "
                        + "transaction_id integer references transactions(_id) ON DELETE CASCADE, "
                        + "primary key (instance_id,transaction_id));");
                db.execSQL("INSERT INTO planinstance_transaction " + "(template_id,instance_id,transaction_id)"
                        + "SELECT " + "template_id,instance_id,transaction_id FROM planinstance_transaction_old");
                db.execSQL("DROP TABLE planinstance_transaction_old");
                db.execSQL("ALTER TABLE transactions RENAME to transactions_old");
                db.execSQL("CREATE TABLE transactions (" + " _id integer primary key autoincrement,"
                        + " comment text, date datetime not null," + " amount integer not null,"
                        + " cat_id integer references categories(_id),"
                        + " account_id integer not null references accounts(_id) ON DELETE CASCADE,"
                        + " payee_id integer references payee(_id),"
                        + " transfer_peer integer references transactions(_id),"
                        + " transfer_account integer references accounts(_id),"
                        + " method_id integer references paymentmethods(_id),"
                        + " parent_id integer references transactions(_id) ON DELETE CASCADE,"
                        + " status integer default 0,"
                        + " cr_status text not null check (cr_status in ('UNRECONCILED','CLEARED','RECONCILED')) default 'RECONCILED',"
                        + " number text)");
                db.execSQL("INSERT INTO transactions "
                        + "(_id,comment,date,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,parent_id,status,cr_status,number) "
                        + "SELECT " + "_id, " + "comment, " + "date, " + "amount, " + "cat_id, " + "account_id, "
                        + "payee_id, " + "transfer_peer, " + "transfer_account, " + "method_id," + "parent_id,"
                        + "status," + "cr_status, " + "number " + "FROM transactions_old");
                db.execSQL("DROP TABLE transactions_old");
                db.execSQL("ALTER TABLE templates RENAME to templates_old");
                db.execSQL("CREATE TABLE templates (" + " _id integer primary key autoincrement," + " comment text,"
                        + " amount integer not null," + " cat_id integer references categories(_id),"
                        + " account_id integer not null references accounts(_id) ON DELETE CASCADE,"
                        + " payee_id integer references payee(_id)," + " transfer_peer boolean default 0,"
                        + " transfer_account integer references accounts(_id) ON DELETE CASCADE,"
                        + " method_id integer references paymentmethods(_id)," + " title text not null,"
                        + " usages integer default 0," + " plan_id integer, "
                        + " plan_execution boolean default 0, " + " unique(account_id,title));");
                db.execSQL("INSERT INTO templates "
                        + "(_id,comment,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,title,usages,plan_id,plan_execution) "
                        + "SELECT " + "_id, " + "comment, " + "amount, " + "cat_id, " + "account_id, "
                        + "payee_id, " + "transfer_peer, " + "transfer_account, " + "method_id," + "title,"
                        + "usages, " + "plan_id, " + "plan_execution " + "FROM templates_old");
                db.execSQL("ALTER TABLE accounts add column sort_key integer");
            }

            if (oldVersion < 45) {
                db.execSQL("ALTER TABLE accounts add column exclude_from_totals boolean default 0");
                //added  to extended view
                db.execSQL("DROP VIEW IF EXISTS transactions_extended");
                db.execSQL("DROP VIEW IF EXISTS templates_extended");
                //      db.execSQL("CREATE VIEW transactions_extended" + VIEW_DEFINITION_EXTENDED(TABLE_TRANSACTIONS) + " WHERE " + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
                //      db.execSQL("CREATE VIEW templates_extended" +  VIEW_DEFINITION_EXTENDED(TABLE_TEMPLATES));
            }

            if (oldVersion < 46) {
                db.execSQL("ALTER TABLE payee add column name_normalized text");
                Cursor c = db.query("payee", new String[] { "_id", "name" }, null, null, null, null, null);
                if (c != null) {
                    if (c.moveToFirst()) {
                        ContentValues v = new ContentValues();
                        while (c.getPosition() < c.getCount()) {
                            v.put("name_normalized", Utils.normalize(c.getString(1)));
                            db.update("payee", v, "_id = " + c.getLong(0), null);
                            c.moveToNext();
                        }
                    }
                    c.close();
                }
            }

            if (oldVersion < 47) {
                db.execSQL("ALTER TABLE templates add column uuid text");
                db.execSQL(EVENT_CACHE_CREATE);
            }

            if (oldVersion < 48) {
                //added method_label to extended view
                //do not comment out, since it is needed by the uuid update
                refreshViews(db);
                //need to inline to protect against later renames

                if (oldVersion < 47) {
                    String[] projection = new String[] { "templates._id", "amount", "comment", "cat_id",
                            "CASE WHEN " + "  " + "transfer_peer" + " " + " THEN " + "  (SELECT " + "label"
                                    + " FROM " + "accounts" + " WHERE " + "_id" + " = " + "transfer_account" + ") "
                                    + " ELSE " + " CASE WHEN " + " (SELECT " + "parent_id" + " FROM " + "categories"
                                    + " WHERE " + "_id" + " = " + "cat_id" + ") " + " THEN " + " (SELECT " + "label"
                                    + " FROM " + "categories" + " WHERE " + "_id" + " = " + " (SELECT "
                                    + "parent_id" + " FROM " + "categories" + " WHERE " + "_id" + " = " + "cat_id"
                                    + ")) " + "  || ' : ' || " + " (SELECT " + "label" + " FROM " + "categories"
                                    + " WHERE " + "_id" + " = " + "cat_id" + ") " + " ELSE" + " (SELECT " + "label"
                                    + " FROM " + "categories" + " WHERE " + "_id" + " = " + "cat_id" + ") "
                                    + " END " + " END AS  " + "label",
                            "name", "transfer_peer", "transfer_account", "account_id", "method_id",
                            "paymentmethods.label AS method_label", "title", "plan_id", "plan_execution", "uuid",
                            "currency" };
                    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
                    qb.setTables("templates LEFT JOIN payee ON payee_id = payee._id"
                            + " LEFT JOIN accounts ON account_id = accounts._id"
                            + " LEFT JOIN paymentmethods ON method_id = paymentmethods._id");
                    Cursor c = qb.query(db, projection, null, null, null, null, null);
                    if (c != null) {
                        if (c.moveToFirst()) {
                            ContentValues templateValues = new ContentValues(), eventValues = new ContentValues();
                            String planCalendarId = MyApplication.getInstance().checkPlanner();
                            while (c.getPosition() < c.getCount()) {
                                Template t = new Template(c);
                                templateValues.put(DatabaseConstants.KEY_UUID, t.getUuid());
                                long templateId = c.getLong(c.getColumnIndex("_id"));
                                long planId = c.getLong(c.getColumnIndex("plan_id"));
                                eventValues.put(Events.DESCRIPTION, t.compileDescription(mCtx));
                                db.update("templates", templateValues, "_id = " + templateId, null);
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                                    try {
                                        mCtx.getContentResolver().update(Events.CONTENT_URI, eventValues,
                                                Events._ID + "= ? AND " + Events.CALENDAR_ID + " = ?",
                                                new String[] { String.valueOf(planId), planCalendarId });
                                    } catch (Exception e) {
                                        //fails with IllegalArgumentException on 2.x devices,
                                        //since the same uri works for inserting and querying
                                        //but also on HUAWEI Y530-U00 with 4.3
                                        //probably SecurityException could arise here
                                    }
                                }
                                c.moveToNext();
                            }
                        }
                        c.close();
                    }
                }
            }

            if (oldVersion < 49) {
                //forgotten to drop in previous upgrade
                db.execSQL("DROP TABLE IF EXISTS templates_old");
            }

            if (oldVersion < 50) {
                db.execSQL("ALTER TABLE transactions add column picture_id text");
                db.execSQL("DROP TABLE IF EXISTS feature_used");
            }

            if (oldVersion < 51) {
                File pictureDir = Utils.getPictureDir(false);
                //fallback if not mounted
                if (pictureDir == null) {
                    pictureDir = new File(
                            Environment.getExternalStorageDirectory().getPath() + "/Android/data/"
                                    + MyApplication.getInstance().getPackageName() + "/files",
                            Environment.DIRECTORY_PICTURES);
                }
                if (!pictureDir.exists()) {
                    AcraHelper.report(new Exception("Unable to calculate pictureDir during upgrade"));
                }
                //if pictureDir does not exist, we use its URI nonetheless, in order to have the data around
                //for potential trouble handling
                String prefix = Uri.fromFile(pictureDir).toString() + "/";
                String postfix = ".jpg";
                //if picture_id concat expression will also be null
                db.execSQL("UPDATE transactions set picture_id = '" + prefix + "'||picture_id||'" + postfix + "'");

                db.execSQL("CREATE TABLE stale_uris ( picture_id text);");
                db.execSQL(
                        "CREATE TRIGGER cache_stale_uri BEFORE DELETE ON transactions WHEN old.picture_id NOT NULL "
                                + " BEGIN INSERT INTO stale_uris VALUES (old.picture_id); END");
            }

            if (oldVersion < 52) {
                db.execSQL("CREATE INDEX transactions_cat_id_index on transactions(cat_id)");
                db.execSQL("CREATE INDEX templates_cat_id_index on templates(cat_id)");
            }

            if (oldVersion < 53) {
                //add VOID status
                db.execSQL("ALTER TABLE transactions RENAME to transactions_old");
                db.execSQL("CREATE TABLE " + "transactions" + "( " + "_id" + " integer primary key autoincrement, "
                        + "comment" + " text, " + "date" + " datetime not null, " + "amount" + " integer not null, "
                        + "cat_id" + " integer references " + "categories" + "(" + "_id" + "), " + "account_id"
                        + " integer not null references " + "accounts" + "(" + "_id" + ") ON DELETE CASCADE,"
                        + "payee_id" + " integer references " + "payee" + "(" + "_id" + "), " + "transfer_peer"
                        + " integer references " + "transactions" + "(" + "_id" + "), " + "transfer_account"
                        + " integer references " + "accounts" + "(" + "_id" + ")," + "method_id"
                        + " integer references " + "paymentmethods" + "(" + "_id" + ")," + "parent_id"
                        + " integer references " + "transactions" + "(" + "_id" + ") ON DELETE CASCADE, " + "status"
                        + " integer default 0, " + "cr_status" + " text not null check (" + "cr_status"
                        + " in ('UNRECONCILED','CLEARED','RECONCILED','VOID')) default 'RECONCILED', " + "number"
                        + " text, " + "picture_id" + " text);");
                db.execSQL("INSERT INTO transactions "
                        + "(_id,comment,date,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,parent_id,status,cr_status,number,picture_id) "
                        + "SELECT " + "_id, " + "comment, " + "date, " + "amount, " + "cat_id, " + "account_id, "
                        + "payee_id, " + "transfer_peer, " + "transfer_account, " + "method_id," + "parent_id,"
                        + "status," + "cr_status, " + "number, " + "picture_id " + "FROM transactions_old");
                db.execSQL("DROP TABLE transactions_old");
                db.execSQL(
                        "CREATE TRIGGER cache_stale_uri BEFORE DELETE ON transactions WHEN old.picture_id NOT NULL "
                                + " BEGIN INSERT INTO stale_uris VALUES (old.picture_id); END");
                db.execSQL("CREATE INDEX transactions_cat_id_index on transactions(cat_id)");
            }

            if (oldVersion < 54) {
                db.execSQL("DROP TRIGGER cache_stale_uri");
                db.execSQL("CREATE TRIGGER cache_stale_uri " + "AFTER DELETE ON " + "transactions" + " "
                        + "WHEN old." + "picture_id" + " NOT NULL " + "AND NOT EXISTS " + "(SELECT 1 FROM "
                        + "transactions" + " " + "WHERE " + "picture_id" + " = old." + "picture_id" + ") "
                        + "BEGIN INSERT INTO " + "stale_uris" + " VALUES (old." + "picture_id" + "); END");
                //all Accounts with old default color are updated to the new one
                db.execSQL(String.format(Locale.US, "UPDATE accounts set color = %d WHERE color = %d", 0xff009688,
                        0xff99CC00));
            }

            if (oldVersion < 55) {
                db.execSQL("ALTER TABLE categories add column label_normalized text");
                Cursor c = db.query("categories", new String[] { "_id", "label" }, null, null, null, null, null);
                if (c != null) {
                    if (c.moveToFirst()) {
                        ContentValues v = new ContentValues();
                        while (c.getPosition() < c.getCount()) {
                            v.put("label_normalized", Utils.normalize(c.getString(1)));
                            db.update("categories", v, "_id = " + c.getLong(0), null);
                            c.moveToNext();
                        }
                    }
                    c.close();
                }
            }

            if (oldVersion < 56) {
                db.execSQL("ALTER TABLE templates add column last_used datetime");
                db.execSQL("ALTER TABLE categories add column last_used datetime");
                db.execSQL("ALTER TABLE accounts add column last_used datetime");
                db.execSQL("CREATE TRIGGER sort_key_default AFTER INSERT ON accounts "
                        + "BEGIN UPDATE accounts SET sort_key = (SELECT coalesce(max(sort_key),0) FROM accounts) + 1 "
                        + "WHERE _id = NEW._id; END");
                //The sort key could be set by user in previous versions, now it is handled internally
                Cursor c = db.query("accounts", new String[] { "_id", "sort_key" }, null, null, null, null,
                        "sort_key ASC");
                boolean hasAccountSortKeySet = false;
                if (c != null) {
                    if (c.moveToFirst()) {
                        ContentValues v = new ContentValues();
                        while (c.getPosition() < c.getCount()) {
                            v.put("sort_key", c.getPosition() + 1);
                            db.update("accounts", v, "_id = ?", new String[] { c.getString(0) });
                            if (c.getInt(1) != 0)
                                hasAccountSortKeySet = true;
                            c.moveToNext();
                        }
                    }
                    c.close();
                }
                String legacy = PrefKey.SORT_ORDER_LEGACY.getString("USAGES");
                PrefKey.SORT_ORDER_TEMPLATES.putString(legacy);
                PrefKey.SORT_ORDER_CATEGORIES.putString(legacy);
                PrefKey.SORT_ORDER_ACCOUNTS.putString(hasAccountSortKeySet ? "CUSTOM" : legacy);
                PrefKey.SORT_ORDER_LEGACY.remove();
            }
        } catch (SQLException e) {
            throw Utils.hasApiLevel(Build.VERSION_CODES.JELLY_BEAN)
                    ? new SQLiteUpgradeFailedException("Database upgrade failed", e)
                    : e;
        }

        if (oldVersion < 57) {
            //fix custom app uris
            if (ContextCompat.checkSelfPermission(mCtx,
                    Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
                Cursor c = db.query("templates", new String[] { "_id", "plan_id" }, "plan_id IS NOT null", null,
                        null, null, null);
                if (c != null) {
                    if (c.moveToFirst()) {
                        while (!c.isAfterLast()) {
                            Plan.updateCustomAppUri(c.getLong(1), Template.buildCustomAppUri(c.getLong(0)));
                            c.moveToNext();
                        }
                    }
                    c.close();
                }
            }

            //Drop unique constraint on templates

            db.execSQL("ALTER TABLE templates RENAME to templates_old");
            db.execSQL("CREATE TABLE templates (" + " _id integer primary key autoincrement," + " comment text,"
                    + " amount integer not null," + " cat_id integer references categories(_id),"
                    + " account_id integer not null references accounts(_id) ON DELETE CASCADE,"
                    + " payee_id integer references payee(_id)," + " transfer_peer boolean default 0,"
                    + " transfer_account integer references accounts(_id) ON DELETE CASCADE,"
                    + " method_id integer references paymentmethods(_id)," + " title text not null,"
                    + " usages integer default 0," + " plan_id integer, " + " plan_execution boolean default 0, "
                    + " uuid text, " + " last_used datetime);");
            db.execSQL("INSERT INTO templates "
                    + "(_id,comment,amount,cat_id,account_id,payee_id,transfer_peer,transfer_account,method_id,title,usages,plan_id,plan_execution,uuid,last_used) "
                    + "SELECT " + "_id, " + "comment, " + "amount, " + "cat_id, " + "account_id, " + "payee_id, "
                    + "transfer_peer, " + "transfer_account, " + "method_id," + "title," + "usages, " + "plan_id, "
                    + "plan_execution, uuid, last_used " + "FROM templates_old");
            db.execSQL("DROP TABLE templates_old");
            //Recreate changed views
            refreshViews(db);
        }

        if (oldVersion < 58) {
            //cache fraction digits
            Cursor c = db.rawQuery("SELECT distinct currency from accounts", null);
            if (c != null) {
                if (c.moveToFirst()) {
                    while (!c.isAfterLast()) {
                        Money.ensureFractionDigitsAreCached(Utils.getSaveInstance(c.getString(0)));
                        c.moveToNext();
                    }
                }
                c.close();
            }
        }
    }

    private void refreshViews(SQLiteDatabase db) {
        db.execSQL("DROP VIEW IF EXISTS transactions_extended");
        db.execSQL("DROP VIEW IF EXISTS templates_extended");
        db.execSQL("DROP VIEW IF EXISTS transactions_committed");
        db.execSQL("DROP VIEW IF EXISTS transactions_uncommitted");
        db.execSQL("DROP VIEW IF EXISTS transactions_all");
        db.execSQL("DROP VIEW IF EXISTS templates_all");
        db.execSQL("CREATE VIEW transactions_extended" + buildViewDefinitionExtended(TABLE_TRANSACTIONS) + " WHERE "
                + KEY_STATUS + " != " + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW templates_extended" + buildViewDefinitionExtended(TABLE_TEMPLATES));
        String viewTransactions = buildViewDefinition(TABLE_TRANSACTIONS);
        db.execSQL("CREATE VIEW transactions_committed " + viewTransactions + " WHERE " + KEY_STATUS + " != "
                + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW transactions_uncommitted" + viewTransactions + " WHERE " + KEY_STATUS + " = "
                + STATUS_UNCOMMITTED + ";");
        db.execSQL("CREATE VIEW transactions_all" + viewTransactions);
        db.execSQL("CREATE VIEW templates_all" + buildViewDefinition(TABLE_TEMPLATES));
    }

    @Override
    public final void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        throw new SQLiteDowngradeFailedException();
    }

    public static class SQLiteDowngradeFailedException extends SQLiteException {
    }

    public static class SQLiteUpgradeFailedException extends SQLiteException {
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        public SQLiteUpgradeFailedException(String error, Throwable cause) {
            super(error, cause);
        }
    }

}