Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.sqlite;
7 import android.content.ContentValues;
8 import android.database.Cursor;
9 import android.database.sqlite.SQLiteDatabase;
10 import android.database.sqlite.SQLiteException;
11 import android.text.TextUtils;
12 import android.util.Log;
14 import org.mozilla.gecko.mozglue.RobocopTarget;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Map.Entry;
20 /*
21 * This class allows using the mozsqlite3 library included with Firefox
22 * to read SQLite databases, instead of the Android SQLiteDataBase API,
23 * which might use whatever outdated DB is present on the Android system.
24 */
25 public class SQLiteBridge {
26 private static final String LOGTAG = "SQLiteBridge";
28 // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
29 private String mDb;
31 // Pointer to the database if it was opened with openDatabase. 0 implies closed.
32 protected volatile long mDbPointer = 0L;
34 // Values remembered after a query.
35 private long[] mQueryResults;
37 private boolean mTransactionSuccess = false;
38 private boolean mInTransaction = false;
40 private static final int RESULT_INSERT_ROW_ID = 0;
41 private static final int RESULT_ROWS_CHANGED = 1;
43 // Shamelessly cribbed from db/sqlite3/src/moz.build.
44 private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
46 // The same size we use elsewhere.
47 private static final int MAX_WAL_SIZE_BYTES = 524288;
49 // JNI code in $(topdir)/mozglue/android/..
50 private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
51 String[] aParams,
52 long[] aUpdateResult)
53 throws SQLiteBridgeException;
54 private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
55 String[] aParams,
56 long[] aUpdateResult)
57 throws SQLiteBridgeException;
58 private static native long openDatabase(String aDb)
59 throws SQLiteBridgeException;
60 private static native void closeDatabase(long aDb);
62 // Takes the path to the database we want to access.
63 @RobocopTarget
64 public SQLiteBridge(String aDb) throws SQLiteBridgeException {
65 mDb = aDb;
66 }
68 // Executes a simple line of sql.
69 public void execSQL(String sql)
70 throws SQLiteBridgeException {
71 internalQuery(sql, null);
72 }
74 // Executes a simple line of sql. Allow you to bind arguments
75 public void execSQL(String sql, String[] bindArgs)
76 throws SQLiteBridgeException {
77 internalQuery(sql, bindArgs);
78 }
80 // Executes a DELETE statement on the database
81 public int delete(String table, String whereClause, String[] whereArgs)
82 throws SQLiteBridgeException {
83 StringBuilder sb = new StringBuilder("DELETE from ");
84 sb.append(table);
85 if (whereClause != null) {
86 sb.append(" WHERE " + whereClause);
87 }
89 internalQuery(sb.toString(), whereArgs);
90 return (int)mQueryResults[RESULT_ROWS_CHANGED];
91 }
93 public Cursor query(String table,
94 String[] columns,
95 String selection,
96 String[] selectionArgs,
97 String groupBy,
98 String having,
99 String orderBy,
100 String limit)
101 throws SQLiteBridgeException {
102 StringBuilder sb = new StringBuilder("SELECT ");
103 if (columns != null)
104 sb.append(TextUtils.join(", ", columns));
105 else
106 sb.append(" * ");
108 sb.append(" FROM ");
109 sb.append(table);
111 if (selection != null) {
112 sb.append(" WHERE " + selection);
113 }
115 if (groupBy != null) {
116 sb.append(" GROUP BY " + groupBy);
117 }
119 if (having != null) {
120 sb.append(" HAVING " + having);
121 }
123 if (orderBy != null) {
124 sb.append(" ORDER BY " + orderBy);
125 }
127 if (limit != null) {
128 sb.append(" " + limit);
129 }
131 return rawQuery(sb.toString(), selectionArgs);
132 }
134 @RobocopTarget
135 public Cursor rawQuery(String sql, String[] selectionArgs)
136 throws SQLiteBridgeException {
137 return internalQuery(sql, selectionArgs);
138 }
140 public long insert(String table, String nullColumnHack, ContentValues values)
141 throws SQLiteBridgeException {
142 if (values == null)
143 return 0;
145 ArrayList<String> valueNames = new ArrayList<String>();
146 ArrayList<String> valueBinds = new ArrayList<String>();
147 ArrayList<String> keyNames = new ArrayList<String>();
149 for (Entry<String, Object> value : values.valueSet()) {
150 keyNames.add(value.getKey());
152 Object val = value.getValue();
153 if (val == null) {
154 valueNames.add("NULL");
155 } else {
156 valueNames.add("?");
157 valueBinds.add(val.toString());
158 }
159 }
161 StringBuilder sb = new StringBuilder("INSERT into ");
162 sb.append(table);
164 sb.append(" (");
165 sb.append(TextUtils.join(", ", keyNames));
166 sb.append(")");
168 // XXX - Do we need to bind these values?
169 sb.append(" VALUES (");
170 sb.append(TextUtils.join(", ", valueNames));
171 sb.append(") ");
173 String[] binds = new String[valueBinds.size()];
174 valueBinds.toArray(binds);
175 internalQuery(sb.toString(), binds);
176 return mQueryResults[RESULT_INSERT_ROW_ID];
177 }
179 public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
180 throws SQLiteBridgeException {
181 if (values == null)
182 return 0;
184 ArrayList<String> valueNames = new ArrayList<String>();
186 StringBuilder sb = new StringBuilder("UPDATE ");
187 sb.append(table);
188 sb.append(" SET ");
190 boolean isFirst = true;
192 for (Entry<String, Object> value : values.valueSet()) {
193 if (isFirst)
194 isFirst = false;
195 else
196 sb.append(", ");
198 sb.append(value.getKey());
200 Object val = value.getValue();
201 if (val == null) {
202 sb.append(" = NULL");
203 } else {
204 sb.append(" = ?");
205 valueNames.add(val.toString());
206 }
207 }
209 if (!TextUtils.isEmpty(whereClause)) {
210 sb.append(" WHERE ");
211 sb.append(whereClause);
212 valueNames.addAll(Arrays.asList(whereArgs));
213 }
215 String[] binds = new String[valueNames.size()];
216 valueNames.toArray(binds);
218 internalQuery(sb.toString(), binds);
219 return (int)mQueryResults[RESULT_ROWS_CHANGED];
220 }
222 public int getVersion()
223 throws SQLiteBridgeException {
224 Cursor cursor = internalQuery("PRAGMA user_version", null);
225 int ret = -1;
226 if (cursor != null) {
227 cursor.moveToFirst();
228 String version = cursor.getString(0);
229 ret = Integer.parseInt(version);
230 }
231 return ret;
232 }
234 // Do an SQL query, substituting the parameters in the query with the passed
235 // parameters. The parameters are substituted in order: named parameters
236 // are not supported.
237 private Cursor internalQuery(String aQuery, String[] aParams)
238 throws SQLiteBridgeException {
240 mQueryResults = new long[2];
241 if (isOpen()) {
242 return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
243 }
244 return sqliteCall(mDb, aQuery, aParams, mQueryResults);
245 }
247 /*
248 * The second two parameters here are just provided for compatibility with SQLiteDatabase
249 * Support for them is not currently implemented.
250 */
251 public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
252 throws SQLiteException {
253 if (factory != null) {
254 throw new RuntimeException("factory not supported.");
255 }
256 if (flags != 0) {
257 throw new RuntimeException("flags not supported.");
258 }
260 SQLiteBridge bridge = null;
261 try {
262 bridge = new SQLiteBridge(path);
263 bridge.mDbPointer = SQLiteBridge.openDatabase(path);
264 } catch (SQLiteBridgeException ex) {
265 // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
266 throw new SQLiteException(ex.getMessage());
267 }
269 prepareWAL(bridge);
271 return bridge;
272 }
274 public void close() {
275 if (isOpen()) {
276 closeDatabase(mDbPointer);
277 }
278 mDbPointer = 0L;
279 }
281 public boolean isOpen() {
282 return mDbPointer != 0;
283 }
285 public void beginTransaction() throws SQLiteBridgeException {
286 if (inTransaction()) {
287 throw new SQLiteBridgeException("Nested transactions are not supported");
288 }
289 execSQL("BEGIN EXCLUSIVE");
290 mTransactionSuccess = false;
291 mInTransaction = true;
292 }
294 public void beginTransactionNonExclusive() throws SQLiteBridgeException {
295 if (inTransaction()) {
296 throw new SQLiteBridgeException("Nested transactions are not supported");
297 }
298 execSQL("BEGIN IMMEDIATE");
299 mTransactionSuccess = false;
300 mInTransaction = true;
301 }
303 public void endTransaction() {
304 if (!inTransaction())
305 return;
307 try {
308 if (mTransactionSuccess) {
309 execSQL("COMMIT TRANSACTION");
310 } else {
311 execSQL("ROLLBACK TRANSACTION");
312 }
313 } catch(SQLiteBridgeException ex) {
314 Log.e(LOGTAG, "Error ending transaction", ex);
315 }
316 mInTransaction = false;
317 mTransactionSuccess = false;
318 }
320 public void setTransactionSuccessful() throws SQLiteBridgeException {
321 if (!inTransaction()) {
322 throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
323 }
324 mTransactionSuccess = true;
325 }
327 public boolean inTransaction() {
328 return mInTransaction;
329 }
331 @Override
332 public void finalize() {
333 if (isOpen()) {
334 Log.e(LOGTAG, "Bridge finalized without closing the database");
335 close();
336 }
337 }
339 private static void prepareWAL(final SQLiteBridge bridge) {
340 // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
341 // set the checkpoint size appropriately. If we can't, then we fall back
342 // to truncating and synchronous writes.
343 final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
344 try {
345 if (cursor.moveToFirst()) {
346 String journalMode = cursor.getString(0);
347 Log.d(LOGTAG, "Journal mode: " + journalMode);
348 if ("wal".equals(journalMode)) {
349 // Success! Let's make sure we autocheckpoint at a reasonable interval.
350 final int pageSizeBytes = bridge.getPageSizeBytes();
351 final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
352 bridge.internalQuery("PRAGMA wal_autocheckpoint=" + checkpointPageCount, null).close();
353 } else {
354 if (!"truncate".equals(journalMode)) {
355 Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
356 bridge.internalQuery("PRAGMA journal_mode=TRUNCATE", null).close();
357 }
358 Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
359 bridge.internalQuery("PRAGMA synchronous=FULL", null).close();
360 }
361 }
362 } finally {
363 cursor.close();
364 }
365 }
367 private int getPageSizeBytes() {
368 if (!isOpen()) {
369 throw new IllegalStateException("Database not open.");
370 }
372 final Cursor cursor = internalQuery("PRAGMA page_size", null);
373 try {
374 if (!cursor.moveToFirst()) {
375 Log.w(LOGTAG, "Unable to retrieve page size.");
376 return DEFAULT_PAGE_SIZE_BYTES;
377 }
379 return cursor.getInt(0);
380 } finally {
381 cursor.close();
382 }
383 }
384 }