package com.boardgamegeek.util; import android.database.sqlite.SQLiteDatabase; import android.provider.BaseColumns; import android.text.TextUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; import timber.log.Timber; /** * A builder for creating and replacing tables. */ public class TableBuilder { private String tableName = null; private Column primaryKey = null; private List<Column> columns = new ArrayList<>(); private List<String> uniqueColumnNames = new ArrayList<>(); private CONFLICT_RESOLUTION resolution = CONFLICT_RESOLUTION.IGNORE; private boolean isFtsTable = false; public TableBuilder reset() { tableName = null; primaryKey = null; columns = new ArrayList<>(); uniqueColumnNames = new ArrayList<>(); resolution = CONFLICT_RESOLUTION.IGNORE; return this; } public void create(SQLiteDatabase db) { if (TextUtils.isEmpty(tableName)) { throw new IllegalStateException("Table not specified"); } if (primaryKey == null) { throw new IllegalStateException("Primary key not specified"); } StringBuilder sb = new StringBuilder(); if (isFtsTable) { sb.append("CREATE VIRTUAL TABLE ").append(tableName).append(" USING fts3"); } else { sb.append("CREATE TABLE ").append(tableName); } sb.append(" (").append(primaryKey.build()).append(" PRIMARY KEY AUTOINCREMENT,"); for (Column column : columns) { sb.append(column.build()).append(","); } if (uniqueColumnNames.size() > 0) { sb.append("UNIQUE ("); for (int i = 0; i < uniqueColumnNames.size(); i++) { if (i > 0) { sb.append(","); } sb.append(uniqueColumnNames.get(i)); } sb.append(") ON CONFLICT ").append(resolution); } else { // remove comma sb = new StringBuilder(sb.substring(0, sb.length() - 1)); } sb.append(")"); Timber.d(sb.toString()); db.execSQL(sb.toString()); } public void replace(SQLiteDatabase db) { replace(db, null, null, null); } public void replace(SQLiteDatabase db, Map<String, String> columnMap, String joinTable, String joinColumn) { if (TextUtils.isEmpty(tableName)) { throw new IllegalStateException("Table not specified"); } db.beginTransaction(); try { rename(db); create(db); copy(db, columnMap, joinTable, joinColumn); dropTemp(db); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } private String tempTable() { return tableName + "_tmp"; } private void rename(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + tableName + " RENAME TO " + tempTable()); } private void copy(SQLiteDatabase db, Map<String, String> columnMap, String joinTable, String joinColumn) { StringBuilder sourceColumns = new StringBuilder(); StringBuilder destinationColumns = new StringBuilder(); for (Column column : columns) { if (destinationColumns.length() > 0) destinationColumns.append(","); destinationColumns.append(column.name); if (sourceColumns.length() > 0) sourceColumns.append(","); String c = columnMap == null ? null : columnMap.get(column.name); if (TextUtils.isEmpty(c)) { sourceColumns.append(column.name); } else { sourceColumns.append(c); } } String destinationTable = tempTable(); if (!TextUtils.isEmpty(joinTable) && !TextUtils.isEmpty(joinColumn)) destinationTable += String.format(" INNER JOIN %1$s ON %1$s.%3$s=%2$s.%3$s", joinTable, tempTable(), joinColumn); String sql = String.format("INSERT INTO %s(%s) SELECT %s FROM %s", tableName, destinationColumns.toString(), sourceColumns.toString(), destinationTable); Timber.d(sql); db.execSQL(sql); } private void dropTemp(SQLiteDatabase db) { db.execSQL("DROP TABLE " + tempTable()); } public TableBuilder setTable(String table) { tableName = table; isFtsTable = false; return this; } public TableBuilder setFtsTable(String table) { tableName = table; isFtsTable = true; return this; } public TableBuilder setConflictResolution(CONFLICT_RESOLUTION resolution) { this.resolution = resolution; return this; } public TableBuilder setPrimaryKey(String columnName, COLUMN_TYPE type) { primaryKey = new Column(); primaryKey.name = columnName; primaryKey.type = type; return this; } /** * Add an _ID column and sets it as the primary key. */ public TableBuilder useDefaultPrimaryKey() { primaryKey = new Column(); primaryKey.name = BaseColumns._ID; primaryKey.type = COLUMN_TYPE.INTEGER; return this; } public TableBuilder addColumn(String name, COLUMN_TYPE type) { return addColumn(name, type, false, false); } public TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull) { return addColumn(name, type, notNull, false); } public TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull, int defaultValue) { return addColumn(name, type, notNull, false, null, null, false, String.valueOf(defaultValue)); } public TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull, boolean unique) { return addColumn(name, type, notNull, unique, null, null); } public TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull, boolean unique, String referenceTable, String referenceColumn) { return addColumn(name, type, notNull, unique, referenceTable, referenceColumn, false, null); } public TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull, boolean unique, String referenceTable, String referenceColumn, boolean onCascadeDelete) { return addColumn(name, type, notNull, unique, referenceTable, referenceColumn, onCascadeDelete, null); } TableBuilder addColumn(String name, COLUMN_TYPE type, boolean notNull, boolean unique, String referenceTable, String referenceColumn, boolean onCascadeDelete, String defaultValue) { Column c = new Column(); c.name = name; c.type = type; c.notNull = notNull; c.setReference(referenceTable, referenceColumn); c.onCascadeDelete = onCascadeDelete; c.defaultValue = defaultValue; columns.add(c); if (unique) { if (!notNull) { throw new IllegalStateException("Unique columns must be non-null"); } uniqueColumnNames.add(name); } return this; } public enum COLUMN_TYPE { INTEGER, TEXT, REAL } public enum CONFLICT_RESOLUTION { ROLLBACK, ABORT, FAIL, IGNORE, REPLACE } private class Column { String name; COLUMN_TYPE type; boolean notNull; private String refTable; private String refColumn; private boolean onCascadeDelete; private String defaultValue; private void setReference(String table, String column) { if (TextUtils.isEmpty(table) && !TextUtils.isEmpty(column) || TextUtils.isEmpty(column) && !TextUtils.isEmpty(table)) { throw new IllegalStateException("Table and column must be specified"); } refTable = table; refColumn = column; } String build() { // "NAME TEXT NOT NULL REFERENCES PARENT(NAME) ON DELETE CASCADE" String s = name + " " + type; if (notNull) { s += " NOT NULL"; } if (!TextUtils.isEmpty(defaultValue)) { s += " DEFAULT " + defaultValue + " "; } if (refTable != null) { s += " REFERENCES " + refTable + "(" + refColumn + ")"; } if (onCascadeDelete) { s += " ON DELETE CASCADE"; } return s; } } }