/* * Copyright (c) 2012 - 2015 Ngewi Fet <ngewif@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gnucash.android.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import android.widget.Toast; import com.crashlytics.android.Crashlytics; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.model.Commodity; import org.xml.sax.SAXException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javax.xml.parsers.ParserConfigurationException; import static org.gnucash.android.db.DatabaseSchema.AccountEntry; import static org.gnucash.android.db.DatabaseSchema.BudgetAmountEntry; import static org.gnucash.android.db.DatabaseSchema.BudgetEntry; import static org.gnucash.android.db.DatabaseSchema.CommodityEntry; import static org.gnucash.android.db.DatabaseSchema.CommonColumns; import static org.gnucash.android.db.DatabaseSchema.PriceEntry; import static org.gnucash.android.db.DatabaseSchema.RecurrenceEntry; import static org.gnucash.android.db.DatabaseSchema.ScheduledActionEntry; import static org.gnucash.android.db.DatabaseSchema.SplitEntry; import static org.gnucash.android.db.DatabaseSchema.TransactionEntry; /** * Helper class for managing the SQLite database. * Creates the database and handles upgrades * @author Ngewi Fet <ngewif@gmail.com> * */ public class DatabaseHelper extends SQLiteOpenHelper { /** * Tag for logging */ public static final String LOG_TAG = DatabaseHelper.class.getName(); /** * SQL statement to create the accounts table in the database */ private static final String ACCOUNTS_TABLE_CREATE = "create table " + AccountEntry.TABLE_NAME + " (" + AccountEntry._ID + " integer primary key autoincrement, " + AccountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + AccountEntry.COLUMN_NAME + " varchar(255) not null, " + AccountEntry.COLUMN_TYPE + " varchar(255) not null, " + AccountEntry.COLUMN_CURRENCY + " varchar(255) not null, " + AccountEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + AccountEntry.COLUMN_DESCRIPTION + " varchar(255), " + AccountEntry.COLUMN_COLOR_CODE + " varchar(255), " + AccountEntry.COLUMN_FAVORITE + " tinyint default 0, " + AccountEntry.COLUMN_HIDDEN + " tinyint default 0, " + AccountEntry.COLUMN_FULL_NAME + " varchar(255), " + AccountEntry.COLUMN_PLACEHOLDER + " tinyint default 0, " + AccountEntry.COLUMN_PARENT_ACCOUNT_UID + " varchar(255), " + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + " varchar(255), " + AccountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + AccountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " // + "FOREIGN KEY (" + AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE SET NULL, " + "FOREIGN KEY (" + AccountEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(AccountEntry.TABLE_NAME); /** * SQL statement to create the transactions table in the database */ private static final String TRANSACTIONS_TABLE_CREATE = "create table " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry._ID + " integer primary key autoincrement, " + TransactionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + TransactionEntry.COLUMN_DESCRIPTION + " varchar(255), " + TransactionEntry.COLUMN_NOTES + " text, " + TransactionEntry.COLUMN_TIMESTAMP + " integer not null, " + TransactionEntry.COLUMN_EXPORTED + " tinyint default 0, " + TransactionEntry.COLUMN_TEMPLATE + " tinyint default 0, " + TransactionEntry.COLUMN_CURRENCY + " varchar(255) not null, " + TransactionEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + " varchar(255), " + TransactionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + TransactionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + TransactionEntry.COLUMN_SCHEDX_ACTION_UID + ") REFERENCES " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry.COLUMN_UID + ") ON DELETE SET NULL, " + "FOREIGN KEY (" + TransactionEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(TransactionEntry.TABLE_NAME); /** * SQL statement to create the transaction splits table */ private static final String SPLITS_TABLE_CREATE = "CREATE TABLE " + SplitEntry.TABLE_NAME + " (" + SplitEntry._ID + " integer primary key autoincrement, " + SplitEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + SplitEntry.COLUMN_MEMO + " text, " + SplitEntry.COLUMN_TYPE + " varchar(255) not null, " + SplitEntry.COLUMN_VALUE_NUM + " integer not null, " + SplitEntry.COLUMN_VALUE_DENOM + " integer not null, " + SplitEntry.COLUMN_QUANTITY_NUM + " integer not null, " + SplitEntry.COLUMN_QUANTITY_DENOM + " integer not null, " + SplitEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + SplitEntry.COLUMN_TRANSACTION_UID + " varchar(255) not null, " + SplitEntry.COLUMN_RECONCILE_STATE + " varchar(1) not null default 'n', " + SplitEntry.COLUMN_RECONCILE_DATE + " timestamp not null default current_timestamp, " + SplitEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + SplitEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + SplitEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + "FOREIGN KEY (" + SplitEntry.COLUMN_TRANSACTION_UID + ") REFERENCES " + TransactionEntry.TABLE_NAME + " (" + TransactionEntry.COLUMN_UID + ") ON DELETE CASCADE " + ");" + createUpdatedAtTrigger(SplitEntry.TABLE_NAME); public static final String SCHEDULED_ACTIONS_TABLE_CREATE = "CREATE TABLE " + ScheduledActionEntry.TABLE_NAME + " (" + ScheduledActionEntry._ID + " integer primary key autoincrement, " + ScheduledActionEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + ScheduledActionEntry.COLUMN_ACTION_UID + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_TYPE + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_TEMPLATE_ACCT_UID + " varchar(255) not null, " + ScheduledActionEntry.COLUMN_LAST_RUN + " integer default 0, " + ScheduledActionEntry.COLUMN_START_TIME + " integer not null, " + ScheduledActionEntry.COLUMN_END_TIME + " integer default 0, " + ScheduledActionEntry.COLUMN_TAG + " text, " + ScheduledActionEntry.COLUMN_ENABLED + " tinyint default 1, " //enabled by default + ScheduledActionEntry.COLUMN_AUTO_CREATE + " tinyint default 1, " + ScheduledActionEntry.COLUMN_AUTO_NOTIFY + " tinyint default 0, " + ScheduledActionEntry.COLUMN_ADVANCE_CREATION + " integer default 0, " + ScheduledActionEntry.COLUMN_ADVANCE_NOTIFY + " integer default 0, " + ScheduledActionEntry.COLUMN_TOTAL_FREQUENCY + " integer default 0, " + ScheduledActionEntry.COLUMN_EXECUTION_COUNT + " integer default 0, " + ScheduledActionEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + ScheduledActionEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + ScheduledActionEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(ScheduledActionEntry.TABLE_NAME); public static final String COMMODITIES_TABLE_CREATE = "CREATE TABLE " + DatabaseSchema.CommodityEntry.TABLE_NAME + " (" + CommodityEntry._ID + " integer primary key autoincrement, " + CommodityEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + CommodityEntry.COLUMN_NAMESPACE + " varchar(255) not null default " + Commodity.Namespace.ISO4217.name() + ", " + CommodityEntry.COLUMN_FULLNAME + " varchar(255) not null, " + CommodityEntry.COLUMN_MNEMONIC + " varchar(255) not null, " + CommodityEntry.COLUMN_LOCAL_SYMBOL+ " varchar(255) not null default '', " + CommodityEntry.COLUMN_CUSIP + " varchar(255), " + CommodityEntry.COLUMN_SMALLEST_FRACTION + " integer not null, " + CommodityEntry.COLUMN_QUOTE_FLAG + " integer not null, " + CommodityEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + CommodityEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP " + ");" + createUpdatedAtTrigger(CommodityEntry.TABLE_NAME); /** * SQL statement to create the commodity prices table */ private static final String PRICES_TABLE_CREATE = "CREATE TABLE " + PriceEntry.TABLE_NAME + " (" + PriceEntry._ID + " integer primary key autoincrement, " + PriceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + PriceEntry.COLUMN_COMMODITY_UID + " varchar(255) not null, " + PriceEntry.COLUMN_CURRENCY_UID + " varchar(255) not null, " + PriceEntry.COLUMN_TYPE + " varchar(255), " + PriceEntry.COLUMN_DATE + " TIMESTAMP not null, " + PriceEntry.COLUMN_SOURCE + " text, " + PriceEntry.COLUMN_VALUE_NUM + " integer not null, " + PriceEntry.COLUMN_VALUE_DENOM + " integer not null, " + PriceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + PriceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "UNIQUE (" + PriceEntry.COLUMN_COMMODITY_UID + ", " + PriceEntry.COLUMN_CURRENCY_UID + ") ON CONFLICT REPLACE, " + "FOREIGN KEY (" + PriceEntry.COLUMN_COMMODITY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE, " + "FOREIGN KEY (" + PriceEntry.COLUMN_CURRENCY_UID + ") REFERENCES " + CommodityEntry.TABLE_NAME + " (" + CommodityEntry.COLUMN_UID + ") ON DELETE CASCADE " + ");" + createUpdatedAtTrigger(PriceEntry.TABLE_NAME); private static final String BUDGETS_TABLE_CREATE = "CREATE TABLE " + BudgetEntry.TABLE_NAME + " (" + BudgetEntry._ID + " integer primary key autoincrement, " + BudgetEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + BudgetEntry.COLUMN_NAME + " varchar(255) not null, " + BudgetEntry.COLUMN_DESCRIPTION + " varchar(255), " + BudgetEntry.COLUMN_RECURRENCE_UID + " varchar(255) not null, " + BudgetEntry.COLUMN_NUM_PERIODS + " integer, " + BudgetEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + BudgetEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + BudgetEntry.COLUMN_RECURRENCE_UID + ") REFERENCES " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry.COLUMN_UID + ") " + ");" + createUpdatedAtTrigger(BudgetEntry.TABLE_NAME); private static final String BUDGET_AMOUNTS_TABLE_CREATE = "CREATE TABLE " + BudgetAmountEntry.TABLE_NAME + " (" + BudgetAmountEntry._ID + " integer primary key autoincrement, " + BudgetAmountEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + BudgetAmountEntry.COLUMN_BUDGET_UID + " varchar(255) not null, " + BudgetAmountEntry.COLUMN_ACCOUNT_UID + " varchar(255) not null, " + BudgetAmountEntry.COLUMN_AMOUNT_NUM + " integer not null, " + BudgetAmountEntry.COLUMN_AMOUNT_DENOM + " integer not null, " + BudgetAmountEntry.COLUMN_PERIOD_NUM + " integer not null, " + BudgetAmountEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + BudgetAmountEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_ACCOUNT_UID + ") REFERENCES " + AccountEntry.TABLE_NAME + " (" + AccountEntry.COLUMN_UID + ") ON DELETE CASCADE, " + "FOREIGN KEY (" + BudgetAmountEntry.COLUMN_BUDGET_UID + ") REFERENCES " + BudgetEntry.TABLE_NAME + " (" + BudgetEntry.COLUMN_UID + ") ON DELETE CASCADE " + ");" + createUpdatedAtTrigger(BudgetAmountEntry.TABLE_NAME); private static final String RECURRENCE_TABLE_CREATE = "CREATE TABLE " + RecurrenceEntry.TABLE_NAME + " (" + RecurrenceEntry._ID + " integer primary key autoincrement, " + RecurrenceEntry.COLUMN_UID + " varchar(255) not null UNIQUE, " + RecurrenceEntry.COLUMN_MULTIPLIER + " integer not null default 1, " + RecurrenceEntry.COLUMN_PERIOD_TYPE + " varchar(255) not null, " + RecurrenceEntry.COLUMN_BYDAY + " varchar(255), " + RecurrenceEntry.COLUMN_PERIOD_START + " timestamp not null, " + RecurrenceEntry.COLUMN_PERIOD_END + " timestamp, " + RecurrenceEntry.COLUMN_CREATED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + RecurrenceEntry.COLUMN_MODIFIED_AT + " TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP); " + createUpdatedAtTrigger(RecurrenceEntry.TABLE_NAME); /** * Constructor * @param context Application context * @param databaseName Name of the database */ public DatabaseHelper(Context context, String databaseName){ super(context, databaseName, null, DatabaseSchema.DATABASE_VERSION); } /** * Creates an update trigger to update the updated_at column for all records in the database. * This has to be run per table, and is currently appended to the create table statement. * @param tableName Name of table on which to create trigger * @return SQL statement for creating trigger */ static String createUpdatedAtTrigger(String tableName){ return "CREATE TRIGGER update_time_trigger " + " AFTER UPDATE ON " + tableName + " FOR EACH ROW" + " BEGIN " + "UPDATE " + tableName + " SET " + CommonColumns.COLUMN_MODIFIED_AT + " = CURRENT_TIMESTAMP" + " WHERE OLD." + CommonColumns.COLUMN_UID + " = NEW." + CommonColumns.COLUMN_UID + ";" + " END;"; } @Override public void onCreate(SQLiteDatabase db) { createDatabaseTables(db); } @Override public void onOpen(SQLiteDatabase db) { super.onOpen(db); db.execSQL("PRAGMA foreign_keys=ON"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ Log.i(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); Toast.makeText(GnuCashApplication.getAppContext(), "Upgrading GnuCash database", Toast.LENGTH_SHORT).show(); /* * NOTE: In order to modify the database, create a new static method in the MigrationHelper class * called upgradeDbToVersion<#>, e.g. int upgradeDbToVersion10(SQLiteDatabase) in order to upgrade to version 10. * The upgrade method should return the new (upgraded) database version as the return value. * Then all you need to do is increment the DatabaseSchema.DATABASE_VERSION to the appropriate number to trigger an upgrade. */ if (oldVersion > newVersion) { throw new IllegalArgumentException("Database downgrades are not supported at the moment"); } while(oldVersion < newVersion){ try { Method method = MigrationHelper.class.getDeclaredMethod("upgradeDbToVersion" + (oldVersion+1), SQLiteDatabase.class); Object result = method.invoke(null, db); oldVersion = Integer.parseInt(result.toString()); } catch (NoSuchMethodException e) { String msg = String.format("Database upgrade method upgradeToVersion%d(SQLiteDatabase) definition not found ", newVersion); Log.e(LOG_TAG, msg, e); Crashlytics.log(msg); Crashlytics.logException(e); throw new RuntimeException(e); } catch (IllegalAccessException e) { String msg = String.format("Database upgrade to version %d failed. The upgrade method is inaccessible ", newVersion); Log.e(LOG_TAG, msg, e); Crashlytics.log(msg); Crashlytics.logException(e); throw new RuntimeException(e); } catch (InvocationTargetException e){ Crashlytics.logException(e.getTargetException()); throw new RuntimeException(e.getTargetException()); } } } /** * Creates the tables in the database and import default commodities into the database * @param db Database instance */ private void createDatabaseTables(SQLiteDatabase db) { Log.i(LOG_TAG, "Creating database tables"); db.execSQL(ACCOUNTS_TABLE_CREATE); db.execSQL(TRANSACTIONS_TABLE_CREATE); db.execSQL(SPLITS_TABLE_CREATE); db.execSQL(SCHEDULED_ACTIONS_TABLE_CREATE); db.execSQL(COMMODITIES_TABLE_CREATE); db.execSQL(PRICES_TABLE_CREATE); db.execSQL(RECURRENCE_TABLE_CREATE); db.execSQL(BUDGETS_TABLE_CREATE); db.execSQL(BUDGET_AMOUNTS_TABLE_CREATE); String createAccountUidIndex = "CREATE UNIQUE INDEX '" + AccountEntry.INDEX_UID + "' ON " + AccountEntry.TABLE_NAME + "(" + AccountEntry.COLUMN_UID + ")"; String createTransactionUidIndex = "CREATE UNIQUE INDEX '" + TransactionEntry.INDEX_UID + "' ON " + TransactionEntry.TABLE_NAME + "(" + TransactionEntry.COLUMN_UID + ")"; String createSplitUidIndex = "CREATE UNIQUE INDEX '" + SplitEntry.INDEX_UID + "' ON " + SplitEntry.TABLE_NAME + "(" + SplitEntry.COLUMN_UID + ")"; String createScheduledEventUidIndex = "CREATE UNIQUE INDEX '" + ScheduledActionEntry.INDEX_UID + "' ON " + ScheduledActionEntry.TABLE_NAME + "(" + ScheduledActionEntry.COLUMN_UID + ")"; String createCommodityUidIndex = "CREATE UNIQUE INDEX '" + CommodityEntry.INDEX_UID + "' ON " + CommodityEntry.TABLE_NAME + "(" + CommodityEntry.COLUMN_UID + ")"; String createPriceUidIndex = "CREATE UNIQUE INDEX '" + PriceEntry.INDEX_UID + "' ON " + PriceEntry.TABLE_NAME + "(" + PriceEntry.COLUMN_UID + ")"; String createBudgetUidIndex = "CREATE UNIQUE INDEX '" + BudgetEntry.INDEX_UID + "' ON " + BudgetEntry.TABLE_NAME + "(" + BudgetEntry.COLUMN_UID + ")"; String createBudgetAmountUidIndex = "CREATE UNIQUE INDEX '" + BudgetAmountEntry.INDEX_UID + "' ON " + BudgetAmountEntry.TABLE_NAME + "(" + BudgetAmountEntry.COLUMN_UID + ")"; String createRecurrenceUidIndex = "CREATE UNIQUE INDEX '" + RecurrenceEntry.INDEX_UID + "' ON " + RecurrenceEntry.TABLE_NAME + "(" + RecurrenceEntry.COLUMN_UID + ")"; db.execSQL(createAccountUidIndex); db.execSQL(createTransactionUidIndex); db.execSQL(createSplitUidIndex); db.execSQL(createScheduledEventUidIndex); db.execSQL(createCommodityUidIndex); db.execSQL(createPriceUidIndex); db.execSQL(createBudgetUidIndex); db.execSQL(createRecurrenceUidIndex); db.execSQL(createBudgetAmountUidIndex); try { MigrationHelper.importCommodities(db); } catch (SAXException | ParserConfigurationException | IOException e) { Log.e(LOG_TAG, "Error loading currencies into the database"); e.printStackTrace(); throw new RuntimeException(e); } } }