ru.orangesoftware.financisto2.db.DatabaseAdapter.java Source code

Java tutorial

Introduction

Here is the source code for ru.orangesoftware.financisto2.db.DatabaseAdapter.java

Source

/*******************************************************************************
 * Copyright (c) 2010 Denis Solonenko.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 * Contributors:
 *     Denis Solonenko - initial API and implementation
 *     Abdsandryk - implement getAllExpenses method for bill filtering
 ******************************************************************************/
package ru.orangesoftware.financisto2.db;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.support.v4.util.LongSparseArray;
import android.util.Log;

import org.androidannotations.annotations.EBean;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import ru.orangesoftware.financisto2.R;
import ru.orangesoftware.financisto2.blotter.BlotterFilter;
import ru.orangesoftware.financisto2.datetime.DateUtils;
import ru.orangesoftware.financisto2.db.DatabaseHelper.CategoryColumns;
import ru.orangesoftware.financisto2.db.DatabaseHelper.TransactionColumns;
import ru.orangesoftware.financisto2.db.DatabaseHelper.deleteLogColumns;
import ru.orangesoftware.financisto2.filter.Criteria;
import ru.orangesoftware.financisto2.filter.WhereFilter;
import ru.orangesoftware.financisto2.model.Account;
import ru.orangesoftware.financisto2.model.Attribute;
import ru.orangesoftware.financisto2.model.Budget;
import ru.orangesoftware.financisto2.model.Category;
import ru.orangesoftware.financisto2.model.CategoryTree;
import ru.orangesoftware.financisto2.model.Currency;
import ru.orangesoftware.financisto2.model.Payee;
import ru.orangesoftware.financisto2.model.Project;
import ru.orangesoftware.financisto2.model.RestoredTransaction;
import ru.orangesoftware.financisto2.model.SystemAttribute;
import ru.orangesoftware.financisto2.model.Total;
import ru.orangesoftware.financisto2.model.TotalError;
import ru.orangesoftware.financisto2.model.Transaction;
import ru.orangesoftware.financisto2.model.TransactionAttribute;
import ru.orangesoftware.financisto2.model.TransactionStatus;
import ru.orangesoftware.financisto2.rates.ExchangeRate;
import ru.orangesoftware.financisto2.rates.ExchangeRateProvider;
import ru.orangesoftware.financisto2.rates.ExchangeRatesCollection;
import ru.orangesoftware.financisto2.rates.HistoryExchangeRates;
import ru.orangesoftware.financisto2.rates.LatestExchangeRates;
import ru.orangesoftware.orb.Expressions;

import static ru.orangesoftware.financisto2.db.DatabaseHelper.ACCOUNT_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.AccountColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.AttributeViewColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.BlotterColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CATEGORY_ATTRIBUTE_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CATEGORY_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CCARD_CLOSING_DATE_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CategoryAttributeColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CategoryViewColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.CreditCardClosingDateColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.DELETE_LOG_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.EXCHANGE_RATES_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.ExchangeRateColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.PAYEE_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.TRANSACTION_ATTRIBUTE_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.TRANSACTION_TABLE;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.TransactionAttributeColumns;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.V_ALL_TRANSACTIONS;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.V_ATTRIBUTES;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.V_BLOTTER;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS;
import static ru.orangesoftware.financisto2.db.DatabaseHelper.V_CATEGORY;

@EBean(scope = EBean.Scope.Singleton)
public class DatabaseAdapter extends MyEntityManager {

    private final Context context;

    private boolean updateAccountBalance = true;

    public DatabaseAdapter(Context context) {
        super(context);
        this.context = context;
    }

    // ===================================================================
    // ACCOUNT
    // ===================================================================

    private static final String UPDATE_ORPHAN_TRANSACTIONS_1 = "UPDATE " + TRANSACTION_TABLE + " SET "
            + TransactionColumns.to_account_id + "=0, " + TransactionColumns.to_amount + "=0 " + "WHERE "
            + TransactionColumns.to_account_id + "=?";
    private static final String UPDATE_ORPHAN_TRANSACTIONS_2 = "UPDATE " + TRANSACTION_TABLE + " SET "
            + TransactionColumns.from_account_id + "=" + TransactionColumns.to_account_id + ", "
            + TransactionColumns.from_amount + "=" + TransactionColumns.to_amount + ", "
            + TransactionColumns.to_account_id + "=0, " + TransactionColumns.to_amount + "=0, "
            + TransactionColumns.parent_id + "=0 " + "WHERE " + TransactionColumns.from_account_id + "=? AND "
            + TransactionColumns.to_account_id + ">0";

    public int deleteAccount(long id) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            String[] sid = new String[] { String.valueOf(id) };
            Account a = load(Account.class, id);
            writeDeleteLog(TRANSACTION_TABLE, a.remoteKey);
            db.execSQL(UPDATE_ORPHAN_TRANSACTIONS_1, sid);
            db.execSQL(UPDATE_ORPHAN_TRANSACTIONS_2, sid);
            db.delete(TRANSACTION_ATTRIBUTE_TABLE,
                    TransactionAttributeColumns.TRANSACTION_ID + " in (SELECT _id from " + TRANSACTION_TABLE
                            + " where " + TransactionColumns.from_account_id + "=?)",
                    sid);
            db.delete(TRANSACTION_TABLE, TransactionColumns.from_account_id + "=?", sid);
            int count = db.delete(ACCOUNT_TABLE, "_id=?", sid);
            db.setTransactionSuccessful();
            return count;
        } finally {
            db.endTransaction();
        }

    }

    // ===================================================================
    // TRANSACTION
    // ===================================================================

    public Transaction getTransaction(long id) {
        Transaction t = get(Transaction.class, id);
        if (t != null) {
            t.systemAttributes = getSystemAttributesForTransaction(id);
            if (t.isSplitParent()) {
                t.splits = getSplitsForTransaction(t.id);
            }
            return t;
        }
        return new Transaction();
    }

    public Cursor getBlotter(WhereFilter filter) {
        return getBlotter(V_BLOTTER, filter);
    }

    public Cursor getBlotterForAccount(WhereFilter filter) {
        WhereFilter accountFilter = enhanceFilterForAccountBlotter(filter);
        return getBlotter(V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS, accountFilter);
    }

    public static WhereFilter enhanceFilterForAccountBlotter(WhereFilter filter) {
        WhereFilter accountFilter = WhereFilter.copyOf(filter);
        accountFilter.put(Criteria.raw(BlotterColumns.parent_id + "=0 OR " + BlotterColumns.is_transfer + "=-1"));
        return accountFilter;
    }

    public Cursor getBlotterForAccountWithSplits(WhereFilter filter) {
        return getBlotter(V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS, filter);
    }

    private Cursor getBlotter(String view, WhereFilter filter) {
        long t0 = System.currentTimeMillis();
        try {
            String sortOrder = getBlotterSortOrder(filter);
            return db().query(view, BlotterColumns.NORMAL_PROJECTION, filter.getSelection(),
                    filter.getSelectionArgs(), null, null, sortOrder);
        } finally {
            long t1 = System.currentTimeMillis();
            Log.i("DB", "getBlotter " + (t1 - t0) + "ms");
        }
    }

    private String getBlotterSortOrder(WhereFilter filter) {
        String sortOrder = filter.getSortOrder();
        if (sortOrder == null || sortOrder.length() == 0) {
            sortOrder = BlotterFilter.SORT_NEWER_TO_OLDER + "," + BlotterFilter.SORT_NEWER_TO_OLDER_BY_ID;
        } else {
            if (sortOrder.contains(BlotterFilter.SORT_NEWER_TO_OLDER)) {
                sortOrder += "," + BlotterFilter.SORT_NEWER_TO_OLDER_BY_ID;
            } else {
                sortOrder += "," + BlotterFilter.SORT_OLDER_TO_NEWER_BY_ID;
            }
        }
        return sortOrder;
    }

    public Cursor getAllTemplates(WhereFilter filter) {
        long t0 = System.currentTimeMillis();
        try {
            return db().query(V_ALL_TRANSACTIONS, BlotterColumns.NORMAL_PROJECTION, filter.getSelection(),
                    filter.getSelectionArgs(), null, null, BlotterFilter.SORT_NEWER_TO_OLDER);
        } finally {
            long t1 = System.currentTimeMillis();
            Log.i("DB", "getBlotter " + (t1 - t0) + "ms");
        }
    }

    public Cursor getBlotterWithSplits(String where) {
        return db().query(V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS, BlotterColumns.NORMAL_PROJECTION, where, null, null,
                null, BlotterColumns.datetime + " DESC");
    }

    private static final String ACCOUNT_LAST_CATEGORY_UPDATE = "UPDATE " + ACCOUNT_TABLE + " SET "
            + AccountColumns.LAST_CATEGORY_ID + "=? " + " WHERE " + AccountColumns.ID + "=?";

    private static final String ACCOUNT_LAST_ACCOUNT_UPDATE = "UPDATE " + ACCOUNT_TABLE + " SET "
            + AccountColumns.LAST_ACCOUNT_ID + "=? " + " WHERE " + AccountColumns.ID + "=?";

    private static final String PAYEE_LAST_CATEGORY_UPDATE = "UPDATE " + PAYEE_TABLE
            + " SET last_category_id=(?) WHERE _id=?";

    private static final String CATEGORY_LAST_PROJECT_UPDATE = "UPDATE " + CATEGORY_TABLE
            + " SET last_project_id=(?) WHERE _id=?";

    private void updateLastUsed(Transaction t) {
        SQLiteDatabase db = db();
        if (t.isTransfer()) {
            db.execSQL(ACCOUNT_LAST_ACCOUNT_UPDATE, new Object[] { t.toAccountId, t.fromAccountId });
        }
        db.execSQL(ACCOUNT_LAST_CATEGORY_UPDATE, new Object[] { t.categoryId, t.fromAccountId });
        db.execSQL(PAYEE_LAST_CATEGORY_UPDATE, new Object[] { t.categoryId, t.payeeId });
        db.execSQL(CATEGORY_LAST_PROJECT_UPDATE, new Object[] { t.projectId, t.categoryId });
    }

    public long duplicateTransaction(long id) {
        return duplicateTransaction(id, 0, 1);
    }

    public long duplicateTransactionWithMultiplier(long id, int multiplier) {
        return duplicateTransaction(id, 0, multiplier);
    }

    public long duplicateTransactionAsTemplate(long id) {
        return duplicateTransaction(id, 1, 1);
    }

    private long duplicateTransaction(long id, int isTemplate, int multiplier) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            long now = System.currentTimeMillis();
            Transaction transaction = getTransaction(id);
            if (transaction.isSplitChild()) {
                id = transaction.parentId;
                transaction = getTransaction(id);
            }
            transaction.lastRecurrence = now;
            updateTransaction(transaction);
            transaction.id = -1;
            transaction.isTemplate = isTemplate;
            transaction.dateTime = now;
            transaction.remoteKey = null;
            if (isTemplate == 0) {
                transaction.recurrence = null;
                transaction.notificationOptions = null;
            }
            if (multiplier > 1) {
                transaction.fromAmount *= multiplier;
                transaction.toAmount *= multiplier;
            }
            long transactionId = insertTransaction(transaction);
            Map<Long, String> attributesMap = getAllAttributesForTransaction(id);
            LinkedList<TransactionAttribute> attributes = new LinkedList<TransactionAttribute>();
            for (long attributeId : attributesMap.keySet()) {
                TransactionAttribute ta = new TransactionAttribute();
                ta.attributeId = attributeId;
                ta.value = attributesMap.get(attributeId);
                attributes.add(ta);
            }
            if (attributes.size() > 0) {
                insertAttributes(transactionId, attributes);
            }
            List<Transaction> splits = getSplitsForTransaction(id);
            if (multiplier > 1) {
                for (Transaction split : splits) {
                    split.fromAmount *= multiplier;
                    split.remoteKey = null;
                }
            }
            transaction.id = transactionId;
            transaction.splits = splits;
            insertSplits(transaction);
            db.setTransactionSuccessful();
            return transactionId;
        } finally {
            db.endTransaction();
        }
    }

    public long insertOrUpdate(Transaction transaction) {
        return insertOrUpdate(transaction, Collections.<TransactionAttribute>emptyList());
    }

    public long insertOrUpdate(Transaction transaction, List<TransactionAttribute> attributes) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            long id = insertOrUpdateInTransaction(transaction, attributes);
            db.setTransactionSuccessful();
            return id;
        } finally {
            db.endTransaction();
        }
    }

    public long insertOrUpdateInTransaction(Transaction transaction, List<TransactionAttribute> attributes) {
        long transactionId;
        transaction.lastRecurrence = System.currentTimeMillis();
        if (transaction.id == -1) {
            transactionId = insertTransaction(transaction);
        } else {
            updateTransaction(transaction);
            transactionId = transaction.id;
            db().delete(TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.TRANSACTION_ID + "=?",
                    new String[] { String.valueOf(transactionId) });
            deleteSplitsForParentTransaction(transactionId);
        }
        if (attributes != null) {
            insertAttributes(transactionId, attributes);
        }
        transaction.id = transactionId;
        insertSplits(transaction);
        updateAccountLastTransactionDate(transaction.fromAccountId);
        updateAccountLastTransactionDate(transaction.toAccountId);
        return transactionId;
    }

    public void insertWithoutUpdatingBalance(Transaction transaction) {
        updateAccountBalance = false;
        try {
            transaction.id = insertTransaction(transaction);
            insertSplits(transaction);
        } finally {
            updateAccountBalance = true;
        }
    }

    private void insertAttributes(long transactionId, List<TransactionAttribute> attributes) {
        for (TransactionAttribute a : attributes) {
            a.transactionId = transactionId;
            ContentValues values = a.toValues();
            db().insert(TRANSACTION_ATTRIBUTE_TABLE, null, values);
        }
    }

    private void insertAttributes(long transactionId, Map<Long, String> categoryAttributes) {
        if (categoryAttributes != null && categoryAttributes.size() > 0) {
            List<TransactionAttribute> attributes = new LinkedList<TransactionAttribute>();
            for (Map.Entry<Long, String> e : categoryAttributes.entrySet()) {
                TransactionAttribute a = new TransactionAttribute();
                a.attributeId = e.getKey();
                a.value = e.getValue();
                attributes.add(a);
            }
            insertAttributes(transactionId, attributes);
        }
    }

    private void insertSplits(Transaction parent) {
        List<Transaction> splits = parent.splits;
        if (splits != null) {
            for (Transaction split : splits) {
                split.id = -1;
                split.parentId = parent.id;
                split.dateTime = parent.dateTime;
                split.fromAccountId = parent.fromAccountId;
                split.payeeId = parent.payeeId;
                split.isTemplate = parent.isTemplate;
                split.status = parent.status;
                updateSplitOriginalAmount(parent, split);
                long splitId = insertTransaction(split);
                insertAttributes(splitId, split.categoryAttributes);
            }
        }
    }

    private void updateSplitOriginalAmount(Transaction parent, Transaction split) {
        if (parent.originalCurrencyId > 0) {
            split.originalCurrencyId = parent.originalCurrencyId;
            split.originalFromAmount = split.fromAmount;
            split.fromAmount = calculateAmountInAccountCurrency(parent, split.fromAmount);
        }
    }

    private long calculateAmountInAccountCurrency(Transaction parent, long amount) {
        double rate = getRateFromParent(parent);
        return (long) (rate * amount);
    }

    private double getRateFromParent(Transaction parent) {
        if (parent.originalFromAmount != 0) {
            return Math.abs(1.0 * parent.fromAmount / parent.originalFromAmount);
        }
        return 0;
    }

    private long insertTransaction(Transaction t) {
        t.updatedOn = System.currentTimeMillis();
        long id = db().insert(TRANSACTION_TABLE, null, t.toValues());
        if (updateAccountBalance) {
            if (!t.isTemplateLike()) {
                if (t.isSplitChild()) {
                    if (t.isTransfer()) {
                        updateToAccountBalance(t, id);
                    }
                } else {
                    updateFromAccountBalance(t, id);
                    updateToAccountBalance(t, id);
                    updateLastUsed(t);
                }
            }
        }
        return id;
    }

    private void updateFromAccountBalance(Transaction t, long id) {
        updateAccountBalance(t.fromAccountId, t.fromAmount);
        insertRunningBalance(t.fromAccountId, id, t.dateTime, t.fromAmount, t.fromAmount);
    }

    private void updateToAccountBalance(Transaction t, long id) {
        updateAccountBalance(t.toAccountId, t.toAmount);
        insertRunningBalance(t.toAccountId, id, t.dateTime, t.toAmount, t.toAmount);
    }

    private void updateTransaction(Transaction t) {
        Transaction oldT = null;
        if (t.isNotTemplateLike()) {
            oldT = getTransaction(t.id);
            updateAccountBalance(oldT.fromAccountId, oldT.fromAmount, t.fromAccountId, t.fromAmount);
            updateAccountBalance(oldT.toAccountId, oldT.toAmount, t.toAccountId, t.toAmount);
            updateRunningBalance(oldT, t);
        }
        t.updatedOn = System.currentTimeMillis();
        db().update(TRANSACTION_TABLE, t.toValues(), TransactionColumns._id + "=?",
                new String[] { String.valueOf(t.id) });
        if (oldT != null) {
            updateAccountLastTransactionDate(oldT.fromAccountId);
            updateAccountLastTransactionDate(oldT.toAccountId);
        }
    }

    public void updateTransactionStatus(long id, TransactionStatus status) {
        Transaction t = getTransaction(id);
        t.status = status;
        updateTransaction(t);
    }

    public void deleteTransaction(long id) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            deleteTransactionNoDbTransaction(id);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    public void deleteTransactionNoDbTransaction(long id) {
        Transaction t = getTransaction(id);
        if (t.isNotTemplateLike()) {
            revertFromAccountBalance(t);
            revertToAccountBalance(t);
            updateAccountLastTransactionDate(t.fromAccountId);
            updateAccountLastTransactionDate(t.toAccountId);
        }
        String[] sid = new String[] { String.valueOf(id) };
        SQLiteDatabase db = db();
        db.delete(TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.TRANSACTION_ID + "=?", sid);
        db.delete(TRANSACTION_TABLE, TransactionColumns._id + "=?", sid);
        writeDeleteLog(TRANSACTION_TABLE, t.remoteKey);
        deleteSplitsForParentTransaction(id);
    }

    private void deleteSplitsForParentTransaction(long parentId) {
        List<Transaction> splits = getSplitsForTransaction(parentId);
        SQLiteDatabase db = db();
        for (Transaction split : splits) {
            if (split.isTransfer()) {
                revertToAccountBalance(split);
            }
            db.delete(TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.TRANSACTION_ID + "=?",
                    new String[] { String.valueOf(split.id) });
            writeDeleteLog(TRANSACTION_TABLE, split.remoteKey);
        }

        db.delete(TRANSACTION_TABLE, TransactionColumns.parent_id + "=?",
                new String[] { String.valueOf(parentId) });

    }

    private void revertFromAccountBalance(Transaction t) {
        updateAccountBalance(t.fromAccountId, -t.fromAmount);
        deleteRunningBalance(t.fromAccountId, t.id, t.fromAmount, t.dateTime);
    }

    private void revertToAccountBalance(Transaction t) {
        updateAccountBalance(t.toAccountId, -t.toAmount);
        deleteRunningBalance(t.toAccountId, t.id, t.toAmount, t.dateTime);
    }

    private void updateAccountBalance(long oldAccountId, long oldAmount, long newAccountId, long newAmount) {
        if (oldAccountId == newAccountId) {
            updateAccountBalance(newAccountId, newAmount - oldAmount);
        } else {
            updateAccountBalance(oldAccountId, -oldAmount);
            updateAccountBalance(newAccountId, newAmount);
        }
    }

    private static final String ACCOUNT_TOTAL_AMOUNT_UPDATE = "UPDATE " + ACCOUNT_TABLE + " SET "
            + AccountColumns.TOTAL_AMOUNT + "=" + AccountColumns.TOTAL_AMOUNT + "+(?) " + " WHERE "
            + AccountColumns.ID + "=?";

    private void updateAccountBalance(long accountId, long deltaAmount) {
        if (accountId <= 0) {
            return;
        }
        db().execSQL(ACCOUNT_TOTAL_AMOUNT_UPDATE, new Object[] { deltaAmount, accountId });
    }

    private static final String INSERT_RUNNING_BALANCE = "insert or replace into running_balance(account_id,transaction_id,datetime,balance) values (?,?,?,?)";

    private static final String UPDATE_RUNNING_BALANCE = "update running_balance set balance = balance+(?) where account_id = ? and datetime > ?";

    private static final String DELETE_RUNNING_BALANCE = "delete from running_balance where account_id = ? and transaction_id = ?";

    private void insertRunningBalance(long accountId, long transactionId, long datetime, long amount,
            long deltaAmount) {
        if (accountId <= 0) {
            return;
        }
        long previousTransactionBalance = fetchAccountBalanceAtTheTime(accountId, datetime);
        SQLiteDatabase db = db();
        db.execSQL(INSERT_RUNNING_BALANCE,
                new Object[] { accountId, transactionId, datetime, previousTransactionBalance + amount });
        db.execSQL(UPDATE_RUNNING_BALANCE, new Object[] { deltaAmount, accountId, datetime });
    }

    private void updateRunningBalance(Transaction oldTransaction, Transaction newTransaction) {
        deleteRunningBalance(oldTransaction.fromAccountId, oldTransaction.id, oldTransaction.fromAmount,
                oldTransaction.dateTime);
        insertRunningBalance(newTransaction.fromAccountId, newTransaction.id, newTransaction.dateTime,
                newTransaction.fromAmount, newTransaction.fromAmount);
        deleteRunningBalance(oldTransaction.toAccountId, oldTransaction.id, oldTransaction.toAmount,
                oldTransaction.dateTime);
        insertRunningBalance(newTransaction.toAccountId, newTransaction.id, newTransaction.dateTime,
                newTransaction.toAmount, newTransaction.toAmount);
    }

    private void deleteRunningBalance(long accountId, long transactionId, long amount, long dateTime) {
        if (accountId <= 0) {
            return;
        }
        SQLiteDatabase db = db();
        db.execSQL(DELETE_RUNNING_BALANCE, new Object[] { accountId, transactionId });
        db.execSQL(UPDATE_RUNNING_BALANCE, new Object[] { -amount, accountId, dateTime });
    }

    private long fetchAccountBalanceAtTheTime(long accountId, long datetime) {
        return DatabaseUtils.rawFetchLongValue(this,
                "select balance from running_balance where account_id = ? and datetime <= ? order by datetime desc, transaction_id desc limit 1",
                new String[] { String.valueOf(accountId), String.valueOf(datetime) });
    }

    // ===================================================================
    // CATEGORY
    // ===================================================================

    //    public long insertOrUpdate(Category category, List<Attribute> attributes) {
    //        SQLiteDatabase db = db();
    //        db.beginTransaction();
    //        try {
    //            long id;
    //            if (category.id == -1) {
    //                id = insertCategory(category);
    //            } else {
    //                updateCategory(category);
    //                id = category.id;
    //            }
    //            addAttributes(id, attributes);
    //            category.id = id;
    //            db.setTransactionSuccessful();
    //            return id;
    //        } finally {
    //            db.endTransaction();
    //        }
    //    }

    public void addAttributes(long categoryId, List<Attribute> attributes) {
        SQLiteDatabase db = db();
        db.delete(CATEGORY_ATTRIBUTE_TABLE, CategoryAttributeColumns.CATEGORY_ID + "=?",
                new String[] { String.valueOf(categoryId) });
        if (attributes != null && attributes.size() > 0) {
            ContentValues values = new ContentValues();
            values.put(CategoryAttributeColumns.CATEGORY_ID, categoryId);
            for (Attribute a : attributes) {
                values.put(CategoryAttributeColumns.ATTRIBUTE_ID, a.id);
                db.insert(CATEGORY_ATTRIBUTE_TABLE, null, values);
            }
        }
    }

    //    private long insertCategory(Category category) {
    //        CategoryTree tree = getCategoriesTree(false);
    //        long parentId = category.getParentId();
    //        if (parentId == Category.NO_CATEGORY_ID) {
    ////            if (!tree.isEmpty()) {
    ////                return insertAsLast(category, tree);
    ////            }
    //        } else {
    //            LongSparseArray<Category> map = tree.asIdMap();
    //            Category parent = map.get(parentId);
    //            if (parent != null && parent.hasChildren()) {
    //                //CategoryTree children = parent.children;
    //                //return insertAsLast(category, children);
    //            }
    //        }
    //        return insertChildCategory(parentId, category);
    //    }

    //    private long insertAsLast(Category category, CategoryTree tree) {
    ////        long mateId = tree.getAt(tree.size() - 1).id;
    ////        return insertMateCategory(mateId, category);
    //        return 0;
    //    }

    //    private long updateCategory(Category category) {
    //        Category oldCategory = getCategory(category.id);
    //        if (oldCategory.getParentId() == category.getParentId()) {
    //            updateCategory(category.id, category.title, category.type);
    //            updateChildCategoriesType(category.type, category.left, category.right);
    //        } else {
    //            moveCategory(category);
    //        }
    //        return category.id;
    //    }

    //    private void moveCategory(Category category) {
    //        CategoryTree tree = getCategoriesTree(false);
    //        LongSparseArray<Category> map = tree.asIdMap();
    //        Category oldCategory = map.get(category.id);
    //        if (oldCategory != null) {
    //            Category oldParent = map.get(oldCategory.getParentId());
    //            if (oldParent != null) {
    //                oldParent.removeChild(oldCategory);
    //            } else {
    //                //tree.remove(oldCategory);
    //            }
    //            Category newParent = map.get(category.getParentId());
    //            int newCategoryType = category.type;
    //            if (newParent != null) {
    //                newParent.addChild(oldCategory);
    //                newCategoryType = newParent.type;
    //            } else {
    //                //tree.add(oldCategory);
    //            }
    //            tree.reIndex();
    //            updateCategoryTreeInTransaction(tree);
    //            updateCategory(category.id, category.title, newCategoryType);
    //            updateChildCategoriesType(newCategoryType, oldCategory.left, oldCategory.right);
    //        }
    //    }

    //    public Category getCategory(long id) {
    //        if (id == Category.NO_CATEGORY_ID) return Category.noCategory(context);
    //        if (id == Category.SPLIT_CATEGORY_ID) return Category.splitCategory(context);
    //        Category category = getCategoryNoParent(id);
    //        if (category != null) {
    //            if (category.parentId > 0)  {
    //                Category parent = getCategoryNoParent(category.parentId);
    //                if (parent != null) {
    //                    category.parent = parent;
    //                } else {
    //                    category.parent = new Category(category.parentId);
    //                }
    //            };
    //            return category;
    //        }
    //        return new Category(-1);
    //    }
    //
    //    protected Category getCategoryNoParent(long id) {
    //        return get(Category.class, id);
    //    }
    //
    //    public Category getCategoryByLeft(long left) {
    //        Category category = createQuery(Category.class).where(Expressions.eq("left", left)).uniqueResult();
    //        if (category != null) {
    //            return category;
    //        } else {
    //            return new Category(-1);
    //        }
    //    }
    //
    //    public CategoryTree getCategoriesTree(boolean includeNoCategory) {
    //        return null;
    ////        Cursor c = getCategories(includeNoCategory);
    ////        try {
    ////            return CategoryTree.createFromCursor(c, new NodeCreator<Category>() {
    ////                @Override
    ////                public Category createNode(Cursor c) {
    ////                    return Category.formCursor(c);
    ////                }
    ////            });
    ////        } finally {
    ////            c.close();
    ////        }
    //    }

    //    public CategoryTree getAllCategoriesTree() {
    //        return null;
    ////        Cursor c = getAllCategories();
    ////        try {
    ////            return CategoryTree.createFromCursor(c, new NodeCreator<Category>() {
    ////                @Override
    ////                public Category createNode(Cursor c) {
    ////                    return Category.formCursor(c);
    ////                }
    ////            });
    ////        } finally {
    ////            c.close();
    ////        }
    //    }

    //    public LongSparseArray<Category> getAllCategoriesMap() {
    //        return getAllCategoriesTree().asIdMap();
    //    }
    //
    //    public List<Category> getCategoriesList(boolean includeNoCategory) {
    //        Cursor c = getCategories(includeNoCategory);
    //        return categoriesAsList(c);
    //    }
    //
    //    public Cursor getAllCategories() {
    //        return db().query(V_CATEGORY, CategoryViewColumns.NORMAL_PROJECTION,
    //                null, null, null, null, null);
    //    }
    //
    //    public List<Category> getAllCategoriesList() {
    //        Cursor c = getAllCategories();
    //        return categoriesAsList(c);
    //    }
    //
    //    private List<Category> categoriesAsList(Cursor c) {
    //        ArrayList<Category> list = new ArrayList<Category>();
    //        try {
    //            while (c.moveToNext()) {
    //                Category category = Category.formCursor(c);
    //                list.add(category);
    //            }
    //        } finally {
    //            c.close();
    //        }
    //        return list;
    //    }
    //
    //    public Cursor getCategories(boolean includeNoCategory) {
    //        return db().query(V_CATEGORY, CategoryViewColumns.NORMAL_PROJECTION,
    //                includeNoCategory ? CategoryViewColumns._id + ">=0" : CategoryViewColumns._id + ">0", null, null, null, null);
    //    }
    //
    //    public Cursor getCategoriesWithoutSubtree(long id) {
    //        SQLiteDatabase db = db();
    //        long left = 0, right = 0;
    //        Cursor c = db.query(CATEGORY_TABLE, new String[]{CategoryColumns.left.name(), CategoryColumns.right.name()},
    //                CategoryColumns._id + "=?", new String[]{String.valueOf(id)}, null, null, null);
    //        try {
    //            if (c.moveToFirst()) {
    //                left = c.getLong(0);
    //                right = c.getLong(1);
    //            }
    //        } finally {
    //            c.close();
    //        }
    //        return db.query(V_CATEGORY, CategoryViewColumns.NORMAL_PROJECTION,
    //                "(NOT (" + CategoryViewColumns.left + ">=? AND " + CategoryColumns.right + "<=?)) AND " + CategoryViewColumns._id + ">=0",
    //                new String[]{String.valueOf(left), String.valueOf(right)}, null, null, null);
    //    }
    //
    //    public List<Category> getCategoriesWithoutSubtreeAsList(long categoryId) {
    //        List<Category> list = new ArrayList<Category>();
    //        Cursor c = getCategoriesWithoutSubtree(categoryId);
    //        try {
    //            while (c.moveToNext()) {
    //                Category category = Category.formCursor(c);
    //                list.add(category);
    //            }
    //            return list;
    //        } finally {
    //            c.close();
    //        }
    //    }
    //
    //    private static final String INSERT_CATEGORY_UPDATE_RIGHT = "UPDATE " + CATEGORY_TABLE + " SET " + CategoryColumns.right + "=" + CategoryColumns.right + "+2 WHERE " + CategoryColumns.right + ">?";
    //    private static final String INSERT_CATEGORY_UPDATE_LEFT = "UPDATE " + CATEGORY_TABLE + " SET " + CategoryColumns.left + "=" + CategoryColumns.left + "+2 WHERE " + CategoryColumns.left + ">?";
    //
    //    public long insertChildCategory(long parentId, Category category) {
    //        //DECLARE v_leftkey INT UNSIGNED DEFAULT 0;
    //        //SELECT l INTO v_leftkey FROM `nset` WHERE `id` = ParentID;
    //        //UPDATE `nset` SET `r` = `r` + 2 WHERE `r` > v_leftkey;
    //        //UPDATE `nset` SET `l` = `l` + 2 WHERE `l` > v_leftkey;
    //        //INSERT INTO `nset` (`name`, `l`, `r`) VALUES (NodeName, v_leftkey + 1, v_leftkey + 2);
    //        int type = getActualCategoryType(parentId, category);
    //        return insertCategory(CategoryColumns.left.name(), parentId, category.title, type);
    //    }
    //
    //    public long insertMateCategory(long categoryId, Category category) {
    //        //DECLARE v_rightkey INT UNSIGNED DEFAULT 0;
    //        //SELECT `r` INTO v_rightkey FROM `nset` WHERE `id` = MateID;
    //        //UPDATE `   nset` SET `r` = `r` + 2 WHERE `r` > v_rightkey;
    //        //UPDATE `nset` SET `l` = `l` + 2 WHERE `l` > v_rightkey;
    //        //INSERT `nset` (`name`, `l`, `r`) VALUES (NodeName, v_rightkey + 1, v_rightkey + 2);
    //        Category mate = getCategory(categoryId);
    //        long parentId = mate.getParentId();
    //        int type = getActualCategoryType(parentId, category);
    //        return insertCategory(CategoryColumns.right.name(), categoryId, category.title, type);
    //    }
    //
    //    private int getActualCategoryType(long parentId, Category category) {
    //        int type = category.type;
    //        if (parentId > 0) {
    //            Category parent = getCategory(parentId);
    //            type = parent.type;
    //        }
    //        return type;
    //    }
    //
    //    private long insertCategory(String field, long categoryId, String title, int type) {
    //        int num = 0;
    //        SQLiteDatabase db = db();
    //        Cursor c = db.query(CATEGORY_TABLE, new String[]{field},
    //                CategoryColumns._id + "=?", new String[]{String.valueOf(categoryId)}, null, null, null);
    //        try {
    //            if (c.moveToFirst()) {
    //                num = c.getInt(0);
    //            }
    //        } finally {
    //            c.close();
    //        }
    //        String[] args = new String[]{String.valueOf(num)};
    //        db.execSQL(INSERT_CATEGORY_UPDATE_RIGHT, args);
    //        db.execSQL(INSERT_CATEGORY_UPDATE_LEFT, args);
    //        ContentValues values = new ContentValues();
    //        values.put(CategoryColumns.title.name(), title);
    //        int left = num + 1;
    //        int right = num + 2;
    //        values.put(CategoryColumns.left.name(), left);
    //        values.put(CategoryColumns.right.name(), right);
    //        values.put(CategoryColumns.type.name(), type);
    //        long id = db.insert(CATEGORY_TABLE, null, values);
    //        updateChildCategoriesType(type, left, right);
    //        return id;
    //    }
    //
    //    private static final String CATEGORY_UPDATE_CHILDREN_TYPES = "UPDATE " + CATEGORY_TABLE + " SET " + CategoryColumns.type + "=? WHERE " + CategoryColumns.left + ">? AND " + CategoryColumns.right + "<?";
    //
    //    private void updateChildCategoriesType(int type, int left, int right) {
    //        db().execSQL(CATEGORY_UPDATE_CHILDREN_TYPES, new Object[]{type, left, right});
    //    }
    //
    //    private static final String DELETE_CATEGORY_UPDATE1 = "UPDATE " + TRANSACTION_TABLE
    //            + " SET " + TransactionColumns.category_id + "=0 WHERE "
    //            + TransactionColumns.category_id + " IN ("
    //            + "SELECT " + CategoryColumns._id + " FROM " + CATEGORY_TABLE + " WHERE "
    //            + CategoryColumns.left + " BETWEEN ? AND ?)";
    //    private static final String DELETE_CATEGORY_UPDATE2 = "UPDATE " + CATEGORY_TABLE
    //            + " SET " + CategoryColumns.left + "=(CASE WHEN " + CategoryColumns.left + ">%s THEN "
    //            + CategoryColumns.left + "-%s ELSE " + CategoryColumns.left + " END),"
    //            + CategoryColumns.right + "=" + CategoryColumns.right + "-%s"
    //            + " WHERE " + CategoryColumns.right + ">%s";
    //
    //    public void deleteCategory(long categoryId) {
    //        //DECLARE v_leftkey, v_rightkey, v_width INT DEFAULT 0;
    //        //
    //        //SELECT
    //        //   `l`, `r`, `r` - `l` + 1 INTO v_leftkey, v_rightkey, v_width
    //        //FROM `nset`
    //        //WHERE
    //        //   `id` = NodeID;
    //        //
    //        //DELETE FROM `nset` WHERE `l` BETWEEN v_leftkey AND v_rightkey;
    //        //
    //        //UPDATE `nset`
    //        //SET
    //        //   `l` = IF(`l` > v_leftkey, `l` - v_width, `l`),
    //        //   `r` = `r` - v_width
    //        //WHERE
    //        //   `r` > v_rightkey;
    //        SQLiteDatabase db = db();
    //        int left = 0, right = 0;
    //        Cursor c = db.query(CATEGORY_TABLE, new String[]{CategoryColumns.left.name(), CategoryColumns.right.name()},
    //                CategoryColumns._id + "=?", new String[]{String.valueOf(categoryId)}, null, null, null);
    //        try {
    //            if (c.moveToFirst()) {
    //                left = c.getInt(0);
    //                right = c.getInt(1);
    //            }
    //        } finally {
    //            c.close();
    //        }
    //        db.beginTransaction();
    //        try {
    //            Category category = load(Category.class, categoryId);
    //            writeDeleteLog(CATEGORY_TABLE, category.remoteKey);
    //            int width = right - left + 1;
    //            String[] args = new String[]{String.valueOf(left), String.valueOf(right)};
    //            db.execSQL(DELETE_CATEGORY_UPDATE1, args);
    //            db.delete(CATEGORY_TABLE, CategoryColumns.left + " BETWEEN ? AND ?", args);
    //            db.execSQL(String.format(DELETE_CATEGORY_UPDATE2, left, width, width, right));
    //            db.setTransactionSuccessful();
    //        } finally {
    //            db.endTransaction();
    //        }
    //    }
    //
    //    private void updateCategory(long id, String title, int type) {
    //        ContentValues values = new ContentValues();
    //        values.put(CategoryColumns.title.name(), title);
    //        values.put(CategoryColumns.type.name(), type);
    //        values.remove("updated_on");
    //        values.put(CategoryColumns.updated_on.name(), System.currentTimeMillis());
    //        db().update(CATEGORY_TABLE, values, CategoryColumns._id + "=?", new String[]{String.valueOf(id)});
    //    }
    //
    //    public void insertCategoryTreeInTransaction(CategoryTree tree) {
    //        db().delete("category", "_id > 0", null);
    //        insertCategoryInTransaction(tree);
    //        updateCategoryTreeInTransaction(tree);
    //    }
    //
    //    private void insertCategoryInTransaction(CategoryTree tree) {
    ////        for (Category category : tree) {
    ////            reInsertCategory(category);
    ////            if (category.hasChildren()) {
    ////                insertCategoryInTransaction(category.children);
    ////            }
    ////        }
    //    }
    //
    //    public void updateCategoryTree(CategoryTree tree) {
    //        SQLiteDatabase db = db();
    //        db.beginTransaction();
    //        try {
    //            updateCategoryTreeInTransaction(tree);
    //            db.setTransactionSuccessful();
    //        } finally {
    //            db.endTransaction();
    //        }
    //    }
    //
    //    private static final String WHERE_CATEGORY_ID = CategoryColumns._id + "=?";
    //
    //    private void updateCategoryTreeInTransaction(CategoryTree tree) {
    //        int left = 1;
    //        int right = 2;
    //        ContentValues values = new ContentValues();
    //        String[] sid = new String[1];
    ////        for (Category c : tree) {
    ////            values.put(CategoryColumns.left.name(), c.left);
    ////            values.put(CategoryColumns.right.name(), c.right);
    ////            sid[0] = String.valueOf(c.id);
    ////            db().update(CATEGORY_TABLE, values, WHERE_CATEGORY_ID, sid);
    ////            if (c.hasChildren()) {
    ////                updateCategoryTreeInTransaction(c.children);
    ////            }
    ////            if (c.left < left) {
    ////                left = c.left;
    ////            }
    ////            if (c.right > right) {
    ////                right = c.right;
    ////            }
    ////        }
    //        values.put(CategoryColumns.left.name(), left - 1);
    //        values.put(CategoryColumns.right.name(), right + 1);
    //        sid[0] = String.valueOf(Category.NO_CATEGORY_ID);
    //        db().update(CATEGORY_TABLE, values, WHERE_CATEGORY_ID, sid);
    //    }

    // ===================================================================
    // ATTRIBUTES
    // ===================================================================

    public List<Attribute> getAttributesForCategory(long categoryId) {
        LongSparseArray<Attribute> attributesMap = getAllAttributesMap();
        Cursor c = db().query(V_ATTRIBUTES, AttributeViewColumns.NORMAL_PROJECTION,
                AttributeViewColumns.CATEGORY_ID + "=?", new String[] { String.valueOf(categoryId) }, null, null,
                AttributeViewColumns.NAME);
        return collectAttributesFromCursor(attributesMap, c);
    }

    public List<Attribute> getAllAttributesForCategory(Category category) {
        LongSparseArray<Attribute> attributesMap = getAllAttributesMap();
        Cursor c = db().query(V_ATTRIBUTES, AttributeViewColumns.NORMAL_PROJECTION,
                AttributeViewColumns.CATEGORY_LEFT + "<= ? AND " + AttributeViewColumns.CATEGORY_RIGHT + " >= ?",
                new String[] { String.valueOf(category.left), String.valueOf(category.right) }, null, null,
                AttributeViewColumns.NAME);
        return collectAttributesFromCursor(attributesMap, c);
    }

    protected List<Attribute> collectAttributesFromCursor(LongSparseArray<Attribute> attributesMap, Cursor c) {
        try {
            ArrayList<Attribute> list = new ArrayList<Attribute>(c.getCount());
            while (c.moveToNext()) {
                long attributeId = c.getLong(AttributeViewColumns.Indicies.ID);
                Attribute a = attributesMap.get(attributeId);
                if (a != null) {
                    list.add(a);
                }
            }
            return list;
        } finally {
            c.close();
        }
    }

    public Map<Long, String> getAttributesMapping() {
        Cursor c = db().query(V_ATTRIBUTES, AttributeViewColumns.NORMAL_PROJECTION, null, null, null, null,
                AttributeViewColumns.CATEGORY_ID + ", " + AttributeViewColumns.NAME);
        try {
            HashMap<Long, String> attributes = new HashMap<Long, String>();
            StringBuilder sb = null;
            long prevCategoryId = -1;
            while (c.moveToNext()) {
                long categoryId = c.getLong(AttributeViewColumns.Indicies.CATEGORY_ID);
                String name = c.getString(AttributeViewColumns.Indicies.NAME);
                if (prevCategoryId != categoryId) {
                    if (sb == null) {
                        sb = new StringBuilder();
                        sb.append("[");
                    } else {
                        attributes.put(prevCategoryId, sb.append("]").toString());
                        sb.setLength(1);
                    }
                    prevCategoryId = categoryId;
                }
                if (sb.length() > 1) {
                    sb.append(", ");
                }
                sb.append(name);
            }
            if (sb != null) {
                attributes.put(prevCategoryId, sb.append("]").toString());
            }
            return attributes;
        } finally {
            c.close();
        }
    }

    public Map<Long, String> getAllAttributesForTransaction(long transactionId) {
        Cursor c = db()
                .query(TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.NORMAL_PROJECTION,
                        TransactionAttributeColumns.TRANSACTION_ID + "=? AND "
                                + TransactionAttributeColumns.ATTRIBUTE_ID + ">=0",
                        new String[] { String.valueOf(transactionId) }, null, null, null);
        try {
            HashMap<Long, String> attributes = new HashMap<Long, String>();
            while (c.moveToNext()) {
                long attributeId = c.getLong(TransactionAttributeColumns.Indicies.ATTRIBUTE_ID);
                String value = c.getString(TransactionAttributeColumns.Indicies.VALUE);
                attributes.put(attributeId, value);
            }
            return attributes;
        } finally {
            c.close();
        }
    }

    public EnumMap<SystemAttribute, String> getSystemAttributesForTransaction(long transactionId) {
        Cursor c = db()
                .query(TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.NORMAL_PROJECTION,
                        TransactionAttributeColumns.TRANSACTION_ID + "=? AND "
                                + TransactionAttributeColumns.ATTRIBUTE_ID + "<0",
                        new String[] { String.valueOf(transactionId) }, null, null, null);
        try {
            EnumMap<SystemAttribute, String> attributes = new EnumMap<SystemAttribute, String>(
                    SystemAttribute.class);
            while (c.moveToNext()) {
                long attributeId = c.getLong(TransactionAttributeColumns.Indicies.ATTRIBUTE_ID);
                String value = c.getString(TransactionAttributeColumns.Indicies.VALUE);
                attributes.put(SystemAttribute.forId(attributeId), value);
            }
            return attributes;
        } finally {
            c.close();
        }
    }

    /**
     * Sets status=CL (Cleared) for the selected transactions
     *
     * @param ids selected transactions' ids
     */
    public void clearSelectedTransactions(long[] ids) {
        String sql = "UPDATE " + TRANSACTION_TABLE + " SET " + TransactionColumns.status + "='"
                + TransactionStatus.CL + "'," + TransactionColumns.updated_on + "='" + System.currentTimeMillis()
                + "' ";
        runInTransaction(sql, ids);
    }

    /**
     * Sets status=RC (Reconciled) for the selected transactions
     *
     * @param ids selected transactions' ids
     */
    public void reconcileSelectedTransactions(long[] ids) {
        String sql = "UPDATE " + TRANSACTION_TABLE + " SET " + TransactionColumns.status + "='"
                + TransactionStatus.RC + "'," + TransactionColumns.updated_on + "='" + System.currentTimeMillis()
                + "' ";
        runInTransaction(sql, ids);
    }

    /**
     * Deletes the selected transactions
     *
     * @param ids selected transactions' ids
     */
    public void deleteSelectedTransactions(long[] ids) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            for (long id : ids) {
                deleteTransactionNoDbTransaction(id);
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private void runInTransaction(String sql, long[] ids) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            int count = ids.length;
            int bucket = 100;
            int num = 1 + count / bucket;
            for (int i = 0; i < num; i++) {
                int x = bucket * i;
                int y = Math.min(count, bucket * (i + 1));
                String script = createSql(sql, ids, x, y);
                db.execSQL(script);
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private String createSql(String updateSql, long[] ids, int x, int y) {
        StringBuilder sb = new StringBuilder(updateSql).append(" WHERE ").append(TransactionColumns.is_template)
                .append("=0 AND ").append(TransactionColumns.parent_id).append("=0 AND ")
                .append(TransactionColumns._id).append(" IN (");
        for (int i = x; i < y; i++) {
            if (i > x) {
                sb.append(",");
            }
            sb.append(ids[i]);
        }
        sb.append(")");
        return sb.toString();
    }

    private static final String UPDATE_LAST_RECURRENCE = "UPDATE " + TRANSACTION_TABLE + " SET "
            + TransactionColumns.last_recurrence + "=? WHERE " + TransactionColumns._id + "=?";

    public long[] storeMissedSchedules(List<RestoredTransaction> restored, long now) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            int count = restored.size();
            long[] restoredIds = new long[count];
            HashMap<Long, Transaction> transactions = new HashMap<Long, Transaction>();
            for (int i = 0; i < count; i++) {
                RestoredTransaction rt = restored.get(i);
                long transactionId = rt.transactionId;
                Transaction t = transactions.get(transactionId);
                if (t == null) {
                    t = getTransaction(transactionId);
                    transactions.put(transactionId, t);
                }
                t.id = -1;
                t.dateTime = rt.dateTime.getTime();
                t.status = TransactionStatus.RS;
                t.isTemplate = 0;
                restoredIds[i] = insertOrUpdate(t);
                t.id = transactionId;
            }
            for (Transaction t : transactions.values()) {
                db.execSQL(UPDATE_LAST_RECURRENCE, new Object[] { now, t.id });
            }
            db.setTransactionSuccessful();
            return restoredIds;
        } finally {
            db.endTransaction();
        }
    }

    /**
     * @param accountId
     * @param period
     * @return
     */
    public int getCustomClosingDay(long accountId, int period) {
        String where = CreditCardClosingDateColumns.ACCOUNT_ID + "=? AND " + CreditCardClosingDateColumns.PERIOD
                + "=?";

        Cursor c = db().query(CCARD_CLOSING_DATE_TABLE, new String[] { CreditCardClosingDateColumns.CLOSING_DAY },
                where, new String[] { Long.toString(accountId), Integer.toString(period) }, null, null, null);

        int res = 0;
        try {
            if (c != null) {
                if (c.getCount() > 0) {
                    c.moveToFirst();
                    res = c.getInt(0);
                } else {
                    res = 0;
                }
            } else {
                // there is no custom closing day in database for the given account id an period
                res = 0;
            }
        } catch (SQLiteException e) {
            res = 0;
        } finally {
            if (c != null)
                c.close();
        }
        return res;
    }

    public void setCustomClosingDay(long accountId, int period, int closingDay) {
        ContentValues values = new ContentValues();
        values.put(CreditCardClosingDateColumns.ACCOUNT_ID, Long.toString(accountId));
        values.put(CreditCardClosingDateColumns.PERIOD, Integer.toString(period));
        values.put(CreditCardClosingDateColumns.CLOSING_DAY, Integer.toString(closingDay));
        db().insert(CCARD_CLOSING_DATE_TABLE, null, values);
    }

    public void deleteCustomClosingDay(long accountId, int period) {
        String where = CreditCardClosingDateColumns.ACCOUNT_ID + "=? AND " + CreditCardClosingDateColumns.PERIOD
                + "=?";
        String[] args = new String[] { Long.toString(accountId), Integer.toString(period) };
        db().delete(CCARD_CLOSING_DATE_TABLE, where, args);
    }

    public void updateCustomClosingDay(long accountId, int period, int closingDay) {
        // delete previous content
        deleteCustomClosingDay(accountId, period);

        // save new value
        setCustomClosingDay(accountId, period, closingDay);
    }

    /**
     * Re-populates running_balance table for all accounts
     */
    public void rebuildRunningBalances() {
        List<Account> accounts = getAllAccountsList();
        for (Account account : accounts) {
            rebuildRunningBalanceForAccount(account);
        }
    }

    /**
     * Re-populates running_balance for specific account
     *
     * @param account selected account
     */
    public void rebuildRunningBalanceForAccount(Account account) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            String accountId = String.valueOf(account.getId());
            db.execSQL("delete from running_balance where account_id=?", new Object[] { accountId });
            WhereFilter filter = new WhereFilter("");
            filter.put(Criteria.eq(BlotterFilter.FROM_ACCOUNT_ID, accountId));
            filter.asc("datetime");
            filter.asc("_id");
            Cursor c = getBlotterForAccountWithSplits(filter);
            Object[] values = new Object[4];
            values[0] = accountId;
            try {
                long balance = 0;
                while (c.moveToNext()) {
                    long parentId = c.getLong(BlotterColumns.parent_id.ordinal());
                    int isTransfer = c.getInt(BlotterColumns.is_transfer.ordinal());
                    if (parentId > 0) {
                        if (isTransfer >= 0) {
                            // we only interested in the second part of the transfer-split
                            // which is marked with is_transfer=-1 (see v_blotter_for_account_with_splits)
                            continue;
                        }
                    }
                    long fromAccountId = c.getLong(BlotterColumns.from_account_id.ordinal());
                    long toAccountId = c.getLong(BlotterColumns.to_account_id.ordinal());
                    if (toAccountId > 0 && toAccountId == fromAccountId) {
                        // weird bug when a transfer is done from an account to the same account
                        continue;
                    }
                    balance += c.getLong(DatabaseHelper.BlotterColumns.from_amount.ordinal());
                    values[1] = c.getString(DatabaseHelper.BlotterColumns._id.ordinal());
                    values[2] = c.getString(DatabaseHelper.BlotterColumns.datetime.ordinal());
                    values[3] = balance;
                    db.execSQL(
                            "insert into running_balance(account_id,transaction_id,datetime,balance) values (?,?,?,?)",
                            values);
                }
            } finally {
                c.close();
            }
            updateAccountLastTransactionDate(account.id);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private static final String[] SUM_FROM_AMOUNT = new String[] { "sum(from_amount)" };

    public long fetchBudgetBalance(LongSparseArray<Category> categories, LongSparseArray<Project> projects,
            Budget b) {
        String where = Budget.createWhere(b, categories, projects);
        Cursor c = db().query(V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS, SUM_FROM_AMOUNT, where, null, null, null, null);
        try {
            if (c.moveToNext()) {
                return c.getLong(0);
            }
        } finally {
            c.close();
        }
        return 0;
    }

    public void recalculateAccountsBalances() {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            Cursor accountsCursor = db.query(ACCOUNT_TABLE, new String[] { AccountColumns.ID }, null, null, null,
                    null, null);
            try {
                while (accountsCursor.moveToNext()) {
                    long accountId = accountsCursor.getLong(0);
                    recalculateAccountBalances(accountId);
                }
            } finally {
                accountsCursor.close();
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private void recalculateAccountBalances(long accountId) {
        long amount = fetchAccountBalance(accountId);
        ContentValues values = new ContentValues();
        values.put(AccountColumns.TOTAL_AMOUNT, amount);
        db().update(ACCOUNT_TABLE, values, AccountColumns.ID + "=?", new String[] { String.valueOf(accountId) });
        Log.i("DatabaseImport", "Recalculating amount for " + accountId);
    }

    private long fetchAccountBalance(long accountId) {
        Cursor c = db().query(V_BLOTTER_FOR_ACCOUNT_WITH_SPLITS,
                new String[] { "SUM(" + BlotterColumns.from_amount + ")" },
                BlotterColumns.from_account_id + "=? and (" + BlotterColumns.parent_id + "=0 or "
                        + BlotterColumns.is_transfer + "=-1)",
                new String[] { String.valueOf(accountId) }, null, null, null);
        try {
            if (c.moveToFirst()) {
                return c.getLong(0);
            }
            return 0;
        } finally {
            c.close();
        }
    }

    public void saveRate(ExchangeRate r) {
        replaceRate(r, r.date);
    }

    public void replaceRate(ExchangeRate rate, long originalDate) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            replaceRateInTransaction(rate, originalDate, db);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private void replaceRateInTransaction(ExchangeRate rate, long originalDate, SQLiteDatabase db) {
        deleteRateInTransaction(rate.fromCurrencyId, rate.toCurrencyId, originalDate, db);
        saveBothRatesInTransaction(rate, db);
    }

    private void saveBothRatesInTransaction(ExchangeRate r, SQLiteDatabase db) {
        r.date = DateUtils.atMidnight(r.date);
        saveRateInTransaction(db, r);
        saveRateInTransaction(db, r.flip());
    }

    private void saveRateInTransaction(SQLiteDatabase db, ExchangeRate r) {
        ContentValues values = r.toValues();
        values.remove("updated_on");
        values.put(CategoryColumns.updated_on.name(), System.currentTimeMillis());
        db.insert(EXCHANGE_RATES_TABLE, null, values);
    }

    public void saveDownloadedRates(List<ExchangeRate> downloadedRates) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            for (ExchangeRate r : downloadedRates) {
                if (r.isOk()) {
                    replaceRateInTransaction(r, r.date, db);
                }
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    public ExchangeRate findRate(Currency fromCurrency, Currency toCurrency, long date) {
        long day = DateUtils.atMidnight(date);
        Cursor c = db().query(EXCHANGE_RATES_TABLE, ExchangeRateColumns.NORMAL_PROJECTION,
                ExchangeRateColumns.NORMAL_PROJECTION_WHERE, new String[] { String.valueOf(fromCurrency.id),
                        String.valueOf(toCurrency.id), String.valueOf(day) },
                null, null, null);
        try {
            if (c.moveToFirst()) {
                return ExchangeRate.fromCursor(c);
            }
        } finally {
            c.close();
        }
        return null;
    }

    public List<ExchangeRate> findRates(Currency fromCurrency) {
        List<ExchangeRate> rates = new ArrayList<ExchangeRate>();
        Cursor c = db().query(EXCHANGE_RATES_TABLE, ExchangeRateColumns.NORMAL_PROJECTION,
                ExchangeRateColumns.from_currency_id + "=?", new String[] { String.valueOf(fromCurrency.id) }, null,
                null, ExchangeRateColumns.rate_date + " desc");
        try {
            while (c.moveToNext()) {
                rates.add(ExchangeRate.fromCursor(c));
            }
        } finally {
            c.close();
        }
        return rates;
    }

    public List<ExchangeRate> findRates(Currency fromCurrency, Currency toCurrency) {
        List<ExchangeRate> rates = new ArrayList<ExchangeRate>();
        Cursor c = db().query(EXCHANGE_RATES_TABLE, ExchangeRateColumns.NORMAL_PROJECTION,
                ExchangeRateColumns.from_currency_id + "=? and " + ExchangeRateColumns.to_currency_id + "=?",
                new String[] { String.valueOf(fromCurrency.id), String.valueOf(toCurrency.id) }, null, null,
                ExchangeRateColumns.rate_date + " desc");
        try {
            while (c.moveToNext()) {
                rates.add(ExchangeRate.fromCursor(c));
            }
        } finally {
            c.close();
        }
        return rates;
    }

    public ExchangeRateProvider getLatestRates() {
        LatestExchangeRates m = new LatestExchangeRates();
        Cursor c = db().query(EXCHANGE_RATES_TABLE, ExchangeRateColumns.LATEST_RATE_PROJECTION, null, null,
                ExchangeRateColumns.LATEST_RATE_GROUP_BY, null, null);
        fillRatesCollection(m, c);
        return m;
    }

    public ExchangeRateProvider getHistoryRates() {
        HistoryExchangeRates m = new HistoryExchangeRates();
        Cursor c = db().query(EXCHANGE_RATES_TABLE, ExchangeRateColumns.NORMAL_PROJECTION, null, null, null, null,
                null);
        fillRatesCollection(m, c);
        return m;
    }

    private void fillRatesCollection(ExchangeRatesCollection m, Cursor c) {
        try {
            while (c.moveToNext()) {
                ExchangeRate r = ExchangeRate.fromCursor(c);
                m.addRate(r);
            }
        } finally {
            c.close();
        }
    }

    public void deleteRate(ExchangeRate rate) {
        deleteRate(rate.fromCurrencyId, rate.toCurrencyId, rate.date);
    }

    public void deleteRate(long fromCurrencyId, long toCurrencyId, long date) {
        SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            deleteRateInTransaction(fromCurrencyId, toCurrencyId, date, db);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    private void deleteRateInTransaction(long fromCurrencyId, long toCurrencyId, long date, SQLiteDatabase db) {
        long d = DateUtils.atMidnight(date);
        db.delete(EXCHANGE_RATES_TABLE, ExchangeRateColumns.DELETE_CLAUSE,
                new String[] { String.valueOf(fromCurrencyId), String.valueOf(toCurrencyId), String.valueOf(d) });
        db.delete(EXCHANGE_RATES_TABLE, ExchangeRateColumns.DELETE_CLAUSE,
                new String[] { String.valueOf(toCurrencyId), String.valueOf(fromCurrencyId), String.valueOf(d) });
    }

    public Total getAccountsTotalInHomeCurrency() {
        Currency homeCurrency = getHomeCurrency();
        return getAccountsTotal(homeCurrency);
    }

    /**
     * Calculates total in every currency for all accounts
     */
    public Total[] getAccountsTotal() {
        List<Account> accounts = getAllAccountsList();
        Map<Currency, Total> totalsMap = new HashMap<Currency, Total>();
        for (Account account : accounts) {
            if (account.shouldIncludeIntoTotals()) {
                Currency currency = account.currency;
                Total total = totalsMap.get(currency);
                if (total == null) {
                    total = new Total(currency);
                    totalsMap.put(currency, total);
                }
                total.balance += account.totalAmount;
            }
        }
        Collection<Total> values = totalsMap.values();
        return values.toArray(new Total[values.size()]);
    }

    /**
     * Calculates total in home currency for all accounts
     */
    public Total getAccountsTotal(Currency homeCurrency) {
        ExchangeRateProvider rates = getLatestRates();
        List<Account> accounts = getAllAccountsList();
        BigDecimal total = BigDecimal.ZERO;
        for (Account account : accounts) {
            if (account.shouldIncludeIntoTotals()) {
                if (account.currency.id == homeCurrency.id) {
                    total = total.add(BigDecimal.valueOf(account.totalAmount));
                } else {
                    ExchangeRate rate = rates.getRate(account.currency, homeCurrency);
                    if (rate == ExchangeRate.NA) {
                        return new Total(homeCurrency, TotalError.lastRateError(account.currency));
                    } else {
                        total = total.add(BigDecimal.valueOf(rate.rate * account.totalAmount));
                    }
                }
            }
        }
        Total result = new Total(homeCurrency);
        result.balance = total.longValue();
        return result;
    }

    public boolean singleCurrencyOnly() {
        long currencyId = getSingleCurrencyId();
        return currencyId > 0;
    }

    private long getSingleCurrencyId() {
        Cursor c = db().rawQuery(
                "select distinct " + AccountColumns.CURRENCY_ID + " from " + ACCOUNT_TABLE + " where "
                        + AccountColumns.IS_INCLUDE_INTO_TOTALS + "=1 and " + AccountColumns.IS_ACTIVE + "=1",
                null);
        try {
            if (c.getCount() == 1) {
                c.moveToFirst();
                return c.getLong(0);
            }
            return -1;
        } finally {
            c.close();
        }
    }

    public void setDefaultHomeCurrency() {
        Currency homeCurrency = getHomeCurrency();
        long singleCurrencyId = getSingleCurrencyId();
        if (homeCurrency == Currency.EMPTY && singleCurrencyId > 0) {
            Currency c = get(Currency.class, singleCurrencyId);
            c.isDefault = true;
            saveOrUpdate(c);
        }
    }

    public void purgeAccountAtDate(Account account, long date) {
        long nearestTransactionId = findNearestOlderTransactionId(account, date);
        if (nearestTransactionId > 0) {
            SQLiteDatabase db = db();
            db.beginTransaction();
            try {
                Transaction newTransaction = createTransactionFromNearest(account, nearestTransactionId);
                breakSplitTransactions(account, date);
                deleteOldTransactions(account, date);
                insertWithoutUpdatingBalance(newTransaction);
                db.execSQL(INSERT_RUNNING_BALANCE, new Object[] { account.id, newTransaction.id,
                        newTransaction.dateTime, newTransaction.fromAmount });
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        }
    }

    private Transaction createTransactionFromNearest(Account account, long nearestTransactionId) {
        Transaction nearestTransaction = get(Transaction.class, nearestTransactionId);
        long balance = getAccountBalanceForTransaction(account, nearestTransaction);
        Transaction newTransaction = new Transaction();
        newTransaction.fromAccountId = account.id;
        newTransaction.dateTime = DateUtils.atDayEnd(nearestTransaction.dateTime);
        newTransaction.fromAmount = balance;
        Payee payee = insertPayee(context.getString(R.string.purge_account_payee));
        newTransaction.payeeId = payee != null ? payee.id : 0;
        newTransaction.status = TransactionStatus.CL;
        return newTransaction;
    }

    private static final String BREAK_SPLIT_TRANSACTIONS_1 = UPDATE_ORPHAN_TRANSACTIONS_1 + " " + "AND "
            + TransactionColumns.datetime + "<=?";
    private static final String BREAK_SPLIT_TRANSACTIONS_2 = UPDATE_ORPHAN_TRANSACTIONS_2 + " " + "AND "
            + TransactionColumns.datetime + "<=?";

    private void breakSplitTransactions(Account account, long date) {
        SQLiteDatabase db = db();
        long dayEnd = DateUtils.atDayEnd(date);
        db.execSQL(BREAK_SPLIT_TRANSACTIONS_1, new Object[] { account.id, dayEnd });
        db.execSQL(BREAK_SPLIT_TRANSACTIONS_2, new Object[] { account.id, dayEnd });
        db.delete(
                TRANSACTION_ATTRIBUTE_TABLE, TransactionAttributeColumns.TRANSACTION_ID + " in (SELECT _id from "
                        + TRANSACTION_TABLE + " where " + TransactionColumns.datetime + "<=?)",
                new String[] { String.valueOf(dayEnd) });
    }

    public void deleteOldTransactions(Account account, long date) {
        SQLiteDatabase db = db();
        long dayEnd = DateUtils.atDayEnd(date);
        db.delete("transactions", "from_account_id=? and datetime<=? and is_template=0",
                new String[] { String.valueOf(account.id), String.valueOf(dayEnd) });
        db.delete("running_balance", "account_id=? and datetime<=?",
                new String[] { String.valueOf(account.id), String.valueOf(dayEnd) });
    }

    public long getAccountBalanceForTransaction(Account a, Transaction t) {
        return DatabaseUtils.rawFetchLongValue(this,
                "select balance from running_balance where account_id=? and transaction_id=?",
                new String[] { String.valueOf(a.id), String.valueOf(t.id) });
    }

    public long findNearestOlderTransactionId(Account account, long date) {
        return DatabaseUtils.rawFetchId(this,
                "select _id from v_blotter where from_account_id=? and datetime<=? order by datetime desc limit 1",
                new String[] { String.valueOf(account.id), String.valueOf(DateUtils.atDayEnd(date)) });
    }

    public long findLatestTransactionDate(long accountId) {
        return DatabaseUtils.rawFetchLongValue(this,
                "select datetime from running_balance where account_id=? order by datetime desc limit 1",
                new String[] { String.valueOf(accountId) });
    }

    private static final String ACCOUNT_LAST_TRANSACTION_DATE_UPDATE = "UPDATE " + ACCOUNT_TABLE + " SET "
            + AccountColumns.LAST_TRANSACTION_DATE + "=? WHERE " + AccountColumns.ID + "=?";

    private void updateAccountLastTransactionDate(long accountId) {
        if (accountId <= 0) {
            return;
        }
        long lastTransactionDate = findLatestTransactionDate(accountId);
        db().execSQL(ACCOUNT_LAST_TRANSACTION_DATE_UPDATE, new Object[] { lastTransactionDate, accountId });
    }

    public void updateAccountsLastTransactionDate() {
        List<Account> accounts = getAllAccountsList();
        for (Account account : accounts) {
            updateAccountLastTransactionDate(account.id);
        }
    }

    //    public void restoreNoCategory() {
    //        Category c = getCategoryNoParent(Category.NO_CATEGORY_ID);
    //        if (c == null) {
    //            db().execSQL("INSERT INTO category (_id, title, left, right) VALUES (0, 'No category', 1, 2)");
    //        }
    //        CategoryTree tree = getCategoriesTree(false);
    //        tree.reIndex();
    //        updateCategoryTree(tree);
    //    }

    public long getLastRunningBalanceForAccount(Account account) {
        return DatabaseUtils.rawFetchLongValue(this,
                "select balance from running_balance where account_id=? order by datetime desc, transaction_id desc limit 1",
                new String[] { String.valueOf(account.id) });
    }

    public long writeDeleteLog(String tableName, String remoteKey) {
        if (remoteKey == null) {
            return 0;
        }
        if (remoteKey == "") {
            return 0;
        }
        ContentValues row = new ContentValues();
        row.put(deleteLogColumns.TABLE_NAME, tableName);
        row.put(deleteLogColumns.REMOTE_KEY, remoteKey);
        row.put(deleteLogColumns.DELETED_ON, System.currentTimeMillis());
        return db().insert(DELETE_LOG_TABLE, null, row);
    }

}