Index: build.gradle ================================================================== --- build.gradle +++ build.gradle @@ -3,11 +3,11 @@ buildscript { repositories { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } Index: gradle/wrapper/gradle-wrapper.properties ================================================================== --- gradle/wrapper/gradle-wrapper.properties +++ gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 27 23:58:23 ICT 2017 +#Sun Nov 12 03:46:22 ICT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip Index: sqlite3/build.gradle ================================================================== --- sqlite3/build.gradle +++ sqlite3/build.gradle @@ -1,15 +1,12 @@ apply plugin: 'com.android.library' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" - defaultConfig { minSdkVersion 16 - targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } DELETED sqlite3/src/androidTest/java/org/sqlite/database/ApplicationTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/ApplicationTest.java ================================================================== --- sqlite3/src/androidTest/java/org/sqlite/database/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.sqlite.database; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/AbstractCursorTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/AbstractCursorTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/AbstractCursorTest.java @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + +import android.content.Context; +import android.database.AbstractCursor; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.CursorIndexOutOfBoundsException; +import android.database.CursorWindow; +import android.database.DataSetObserver; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.test.InstrumentationTestCase; + +import java.io.File; +import java.util.ArrayList; +import java.util.Random; + +/** + * Test {@link AbstractCursor}. + */ +public class AbstractCursorTest extends InstrumentationTestCase { + private static final int POSITION0 = 0; + private static final int POSITION1 = 1; + private static final int ROW_MAX = 10; + private static final int DATA_COUNT = 10; + private static final String[] COLUMN_NAMES1 = new String[] { + "_id", // 0 + "number" // 1 + }; + private static final String[] COLUMN_NAMES = new String[] { "name", "number", "profit" }; + private TestAbstractCursor mTestAbstractCursor; + private Object mLockObj = new Object(); + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private AbstractCursor mDatabaseCursor; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + setupDatabase(); + ArrayList list = createTestList(ROW_MAX, COLUMN_NAMES.length); + mTestAbstractCursor = new TestAbstractCursor(COLUMN_NAMES, list); + } + + @Override + protected void tearDown() throws Exception { + mDatabaseCursor.close(); + mTestAbstractCursor.close(); + mDatabase.close(); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + super.tearDown(); + } + + public void testConstructor() { + TestAbstractCursor abstractCursor = new TestAbstractCursor(); + assertEquals(-1, abstractCursor.getPosition()); + } + + public void testGetBlob() { + try { + mTestAbstractCursor.getBlob(0); + fail("getBlob should throws a UnsupportedOperationException here"); + } catch (UnsupportedOperationException e) { + // expected + } + } + + public void testRegisterDataSetObserver() { + MockDataSetObserver datasetObserver = new MockDataSetObserver(); + + try { + mDatabaseCursor.unregisterDataSetObserver(datasetObserver); + fail("Can't unregister DataSetObserver before it is registered."); + } catch (IllegalStateException e) { + // expected + } + + mDatabaseCursor.registerDataSetObserver(datasetObserver); + + try { + mDatabaseCursor.registerDataSetObserver(datasetObserver); + fail("Can't register DataSetObserver twice before unregister it."); + } catch (IllegalStateException e) { + // expected + } + + mDatabaseCursor.unregisterDataSetObserver(datasetObserver); + mDatabaseCursor.registerDataSetObserver(datasetObserver); + } + + public void testRegisterContentObserver() { + MockContentObserver contentObserver = new MockContentObserver(); + + try { + mDatabaseCursor.unregisterContentObserver(contentObserver); + fail("Can't unregister ContentObserver before it is registered."); + } catch (IllegalStateException e) { + // expected + } + + mDatabaseCursor.registerContentObserver(contentObserver); + + try { + mDatabaseCursor.registerContentObserver(contentObserver); + fail("Can't register DataSetObserver twice before unregister it."); + } catch (IllegalStateException e) { + // expected + } + + mDatabaseCursor.unregisterContentObserver(contentObserver); + mDatabaseCursor.registerContentObserver(contentObserver); + } + + public void testSetNotificationUri() { + final Uri testUri = Settings.System.getUriFor(Settings.System.TIME_12_24); + mDatabaseCursor.setNotificationUri(getInstrumentation().getContext().getContentResolver(), + testUri); + } + + public void testRespond() { + Bundle b = new Bundle(); + Bundle bundle = mDatabaseCursor.respond(b); + assertSame(Bundle.EMPTY, bundle); + + bundle = mDatabaseCursor.respond(null); + assertSame(Bundle.EMPTY, bundle); + } + + public void testRequery() { + MockDataSetObserver mock = new MockDataSetObserver(); + mDatabaseCursor.registerDataSetObserver(mock); + assertFalse(mock.hadCalledOnChanged()); + mDatabaseCursor.requery(); + assertTrue(mock.hadCalledOnChanged()); + } + + public void testOnChange() throws InterruptedException { + MockContentObserver mock = new MockContentObserver(); + mTestAbstractCursor.registerContentObserver(mock); + assertFalse(mock.hadCalledOnChange()); + mTestAbstractCursor.onChange(true); + synchronized(mLockObj) { + if ( !mock.hadCalledOnChange() ) { + mLockObj.wait(5000); + } + } + assertTrue(mock.hadCalledOnChange()); + } + + public void testOnMove() { + assertFalse(mTestAbstractCursor.getOnMoveRet()); + mTestAbstractCursor.moveToFirst(); + assertTrue(mTestAbstractCursor.getOnMoveRet()); + assertEquals(1, mTestAbstractCursor.getRowsMovedSum()); + + mTestAbstractCursor.moveToPosition(5); + assertTrue(mTestAbstractCursor.getOnMoveRet()); + assertEquals(6, mTestAbstractCursor.getRowsMovedSum()); + assertEquals(0, mTestAbstractCursor.getOldPos()); + assertEquals(5, mTestAbstractCursor.getNewPos()); + } + + public void testOnMove_samePosition() { + mTestAbstractCursor.moveToFirst(); + mTestAbstractCursor.moveToPosition(5); + assertEquals(6, mTestAbstractCursor.getRowsMovedSum()); + mTestAbstractCursor.moveToPosition(5); + // Moving to the same position should either call onMove(5, 5) + // or be a no-op. It should no change the RowsMovedSum. + assertEquals(6, mTestAbstractCursor.getRowsMovedSum()); + } + + public void testMoveToPrevious() { + // Test moveToFirst, isFirst, moveToNext, getPosition + assertTrue(mDatabaseCursor.moveToFirst()); + assertTrue(mDatabaseCursor.isFirst()); + assertEquals(0, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.moveToNext()); + assertEquals(1, mDatabaseCursor.getPosition()); + assertFalse(mDatabaseCursor.isFirst()); + assertTrue(mDatabaseCursor.moveToNext()); + assertEquals(2, mDatabaseCursor.getPosition()); + + // invoke moveToPosition with a number larger than row count. + assertFalse(mDatabaseCursor.moveToPosition(30000)); + assertEquals(mDatabaseCursor.getCount(), mDatabaseCursor.getPosition()); + + assertFalse(mDatabaseCursor.moveToPosition(-1)); + assertEquals(-1, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.isBeforeFirst()); + + mDatabaseCursor.moveToPosition(5); + assertEquals(5, mDatabaseCursor.getPosition()); + + // Test moveToPrevious + assertTrue(mDatabaseCursor.moveToPrevious()); + assertEquals(4, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.moveToPrevious()); + assertEquals(3, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.moveToPrevious()); + assertEquals(2, mDatabaseCursor.getPosition()); + + // Test moveToLast, isLast, moveToPrevius, isAfterLast. + assertFalse(mDatabaseCursor.isLast()); + assertTrue(mDatabaseCursor.moveToLast()); + assertTrue(mDatabaseCursor.isLast()); + assertFalse(mDatabaseCursor.isAfterLast()); + + assertFalse(mDatabaseCursor.moveToNext()); + assertTrue(mDatabaseCursor.isAfterLast()); + assertFalse(mDatabaseCursor.moveToNext()); + assertTrue(mDatabaseCursor.isAfterLast()); + assertFalse(mDatabaseCursor.isLast()); + assertTrue(mDatabaseCursor.moveToPrevious()); + assertTrue(mDatabaseCursor.isLast()); + assertTrue(mDatabaseCursor.moveToPrevious()); + assertFalse(mDatabaseCursor.isLast()); + + // Test move(int). + mDatabaseCursor.moveToFirst(); + assertEquals(0, mDatabaseCursor.getPosition()); + assertFalse(mDatabaseCursor.move(-1)); + assertEquals(-1, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.move(1)); + assertEquals(0, mDatabaseCursor.getPosition()); + + assertTrue(mDatabaseCursor.move(5)); + assertEquals(5, mDatabaseCursor.getPosition()); + assertTrue(mDatabaseCursor.move(-1)); + assertEquals(4, mDatabaseCursor.getPosition()); + + mDatabaseCursor.moveToLast(); + assertTrue(mDatabaseCursor.isLast()); + assertFalse(mDatabaseCursor.isAfterLast()); + assertFalse(mDatabaseCursor.move(1)); + assertFalse(mDatabaseCursor.isLast()); + assertTrue(mDatabaseCursor.isAfterLast()); + assertTrue(mDatabaseCursor.move(-1)); + assertTrue(mDatabaseCursor.isLast()); + assertFalse(mDatabaseCursor.isAfterLast()); + } + + public void testIsClosed() { + assertFalse(mDatabaseCursor.isClosed()); + mDatabaseCursor.close(); + assertTrue(mDatabaseCursor.isClosed()); + } + + public void testGetWindow() { + CursorWindow window = new CursorWindow(false); + assertEquals(0, window.getNumRows()); + // fill window from position 0 + mDatabaseCursor.fillWindow(0, window); + + assertNotNull(mDatabaseCursor.getWindow()); + assertEquals(mDatabaseCursor.getCount(), window.getNumRows()); + + while (mDatabaseCursor.moveToNext()) { + assertEquals(mDatabaseCursor.getInt(POSITION1), + window.getInt(mDatabaseCursor.getPosition(), POSITION1)); + } + window.clear(); + } + + public void testGetWantsAllOnMoveCalls() { + assertFalse(mDatabaseCursor.getWantsAllOnMoveCalls()); + } + + public void testIsFieldUpdated() { + mTestAbstractCursor.moveToFirst(); + assertFalse(mTestAbstractCursor.isFieldUpdated(0)); + } + + public void testGetUpdatedField() { + mTestAbstractCursor.moveToFirst(); + assertNull(mTestAbstractCursor.getUpdatedField(0)); + } + + public void testGetExtras() { + assertSame(Bundle.EMPTY, mDatabaseCursor.getExtras()); + } + + public void testGetCount() { + assertEquals(DATA_COUNT, mDatabaseCursor.getCount()); + } + + public void testGetColumnNames() { + String[] names = mDatabaseCursor.getColumnNames(); + assertEquals(COLUMN_NAMES1.length, names.length); + + for (int i = 0; i < COLUMN_NAMES1.length; i++) { + assertEquals(COLUMN_NAMES1[i], names[i]); + } + } + + public void testGetColumnName() { + assertEquals(COLUMN_NAMES1[0], mDatabaseCursor.getColumnName(0)); + assertEquals(COLUMN_NAMES1[1], mDatabaseCursor.getColumnName(1)); + } + + public void testGetColumnIndexOrThrow() { + final String COLUMN_FAKE = "fake_name"; + assertEquals(POSITION0, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION0])); + assertEquals(POSITION1, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION1])); + assertEquals(POSITION0, mDatabaseCursor.getColumnIndexOrThrow(COLUMN_NAMES1[POSITION0])); + assertEquals(POSITION1, mDatabaseCursor.getColumnIndexOrThrow(COLUMN_NAMES1[POSITION1])); + + try { + mDatabaseCursor.getColumnIndexOrThrow(COLUMN_FAKE); + fail("IllegalArgumentException expected, but not thrown"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + public void testGetColumnIndex() { + assertEquals(POSITION0, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION0])); + assertEquals(POSITION1, mDatabaseCursor.getColumnIndex(COLUMN_NAMES1[POSITION1])); + } + + public void testGetColumnCount() { + assertEquals(COLUMN_NAMES1.length, mDatabaseCursor.getColumnCount()); + } + + public void testDeactivate() { + MockDataSetObserver mock = new MockDataSetObserver(); + mDatabaseCursor.registerDataSetObserver(mock); + assertFalse(mock.hadCalledOnInvalid()); + mDatabaseCursor.deactivate(); + assertTrue(mock.hadCalledOnInvalid()); + } + + public void testCopyStringToBuffer() { + CharArrayBuffer ca = new CharArrayBuffer(1000); + mTestAbstractCursor.moveToFirst(); + mTestAbstractCursor.copyStringToBuffer(0, ca); + CursorWindow window = new CursorWindow(false); + mTestAbstractCursor.fillWindow(0, window); + + StringBuffer sb = new StringBuffer(); + sb.append(window.getString(0, 0)); + String str = mTestAbstractCursor.getString(0); + assertEquals(str.length(), ca.sizeCopied); + assertEquals(sb.toString(), new String(ca.data, 0, ca.sizeCopied)); + } + + public void testCheckPosition() { + // Test with position = -1. + try { + mTestAbstractCursor.checkPosition(); + fail("copyStringToBuffer() should throws CursorIndexOutOfBoundsException here."); + } catch (CursorIndexOutOfBoundsException e) { + // expected + } + + // Test with position = count. + assertTrue(mTestAbstractCursor.moveToPosition(mTestAbstractCursor.getCount() - 1)); + mTestAbstractCursor.checkPosition(); + + try { + assertFalse(mTestAbstractCursor.moveToPosition(mTestAbstractCursor.getCount())); + assertEquals(mTestAbstractCursor.getCount(), mTestAbstractCursor.getPosition()); + mTestAbstractCursor.checkPosition(); + fail("copyStringToBuffer() should throws CursorIndexOutOfBoundsException here."); + } catch (CursorIndexOutOfBoundsException e) { + // expected + } + } + + public void testSetExtras() { + Bundle b = new Bundle(); + mTestAbstractCursor.setExtras(b); + assertSame(b, mTestAbstractCursor.getExtras()); + } + + @SuppressWarnings("unchecked") + private static ArrayList createTestList(int rows, int cols) { + ArrayList list = new ArrayList(); + Random ran = new Random(); + + for (int i = 0; i < rows; i++) { + ArrayList col = new ArrayList(); + list.add(col); + + for (int j = 0; j < cols; j++) { + // generate random number + Integer r = ran.nextInt(); + col.add(r); + } + } + + return list; + } + + private void setupDatabase() { + File dbDir = getInstrumentation().getTargetContext().getDir("tests", + Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabaseFile); + mDatabase.execSQL("CREATE TABLE test1 (_id INTEGER PRIMARY KEY, number TEXT);"); + generateData(); + mDatabaseCursor = (AbstractCursor) mDatabase.query("test1", null, null, null, null, null, + null); + } + + private void generateData() { + for ( int i = 0; i < DATA_COUNT; i++) { + mDatabase.execSQL("INSERT INTO test1 (number) VALUES ('" + i + "');"); + } + } + + private class TestAbstractCursor extends AbstractCursor { + private boolean mOnMoveReturnValue; + private int mOldPosition; + private int mNewPosition; + /** The accumulated number of rows this cursor has moved over. */ + private int mRowsMovedSum; + private String[] mColumnNames; + private ArrayList[] mRows; + private boolean mHadCalledOnChange = false; + + public TestAbstractCursor() { + super(); + } + @SuppressWarnings("unchecked") + public TestAbstractCursor(String[] columnNames, ArrayList rows) { + int colCount = columnNames.length; + boolean foundID = false; + + // Add an _id column if not in columnNames + for (int i = 0; i < colCount; ++i) { + if (columnNames[i].compareToIgnoreCase("_id") == 0) { + mColumnNames = columnNames; + foundID = true; + break; + } + } + + if (!foundID) { + mColumnNames = new String[colCount + 1]; + System.arraycopy(columnNames, 0, mColumnNames, 0, columnNames.length); + mColumnNames[colCount] = "_id"; + } + + int rowCount = rows.size(); + mRows = new ArrayList[rowCount]; + + for (int i = 0; i < rowCount; ++i) { + mRows[i] = rows.get(i); + + if (!foundID) { + mRows[i].add(Long.valueOf(i)); + } + } + } + + public boolean getOnMoveRet() { + return mOnMoveReturnValue; + } + + public void resetOnMoveRet() { + mOnMoveReturnValue = false; + } + + public int getOldPos() { + return mOldPosition; + } + + public int getNewPos() { + return mNewPosition; + } + + public int getRowsMovedSum() { + return mRowsMovedSum; + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + mOnMoveReturnValue = super.onMove(oldPosition, newPosition); + mOldPosition = oldPosition; + mNewPosition = newPosition; + mRowsMovedSum += Math.abs(newPosition - oldPosition); + return mOnMoveReturnValue; + } + + @Override + public int getCount() { + return mRows.length; + } + + @Override + public String[] getColumnNames() { + return mColumnNames; + } + + @Override + public String getString(int columnIndex) { + Object cell = mRows[mPos].get(columnIndex); + return (cell == null) ? null : cell.toString(); + } + + @Override + public short getShort(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.shortValue(); + } + + @Override + public int getInt(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.intValue(); + } + + @Override + public long getLong(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.longValue(); + } + + @Override + public float getFloat(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.floatValue(); + } + + @Override + public double getDouble(int columnIndex) { + Number num = (Number) mRows[mPos].get(columnIndex); + return num.doubleValue(); + } + + @Override + public boolean isNull(int column) { + return false; + } + + public boolean hadCalledOnChange() { + return mHadCalledOnChange; + } + + // the following are protected methods + @Override + protected void checkPosition() { + super.checkPosition(); + } + + @Override + protected Object getUpdatedField(int columnIndex) { + return super.getUpdatedField(columnIndex); + } + + @Override + protected boolean isFieldUpdated(int columnIndex) { + return super.isFieldUpdated(columnIndex); + } + + @Override + protected void onChange(boolean selfChange) { + super.onChange(selfChange); + mHadCalledOnChange = true; + } + } + + private class MockContentObserver extends ContentObserver { + public boolean mHadCalledOnChange; + + public MockContentObserver() { + super(null); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mHadCalledOnChange = true; + synchronized(mLockObj) { + mLockObj.notify(); + } + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + public boolean hadCalledOnChange() { + return mHadCalledOnChange; + } + } + + private class MockDataSetObserver extends DataSetObserver { + private boolean mHadCalledOnChanged; + private boolean mHadCalledOnInvalid; + + @Override + public void onChanged() { + super.onChanged(); + mHadCalledOnChanged = true; + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + mHadCalledOnInvalid = true; + } + + public boolean hadCalledOnChanged() { + return mHadCalledOnChanged; + } + + public boolean hadCalledOnInvalid() { + return mHadCalledOnInvalid; + } + } +} + ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorJoinerTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorJoinerTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorJoinerTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + + +import android.content.Context; +import android.database.Cursor; +import android.database.CursorJoiner; +import android.database.CursorJoiner.Result; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.test.AndroidTestCase; + +import java.io.File; + +public class CursorJoinerTest extends AndroidTestCase { + + private static final int TEST_ITEM_COUNT = 10; + private static final int DEFAULT_TABLE1_VALUE_BEGINS = 1; + private static final int DEFAULT_TABLE2_VALUE_BEGINS = 11; + private static final int EQUAL_START = 18; + // Every table has 7 unique numbers, and 3 other numbers they all have. + private static final int UNIQUE_COUNT = 7; + private static final int MAX_VALUE = 20; + private static final int EQUAL_VALUE_COUNT = MAX_VALUE - EQUAL_START + 1; + private static final String TABLE_NAME_1 = "test1"; + private static final String TABLE_NAME_2 = "test2"; + private static final String TABLE1_COLUMNS = " number TEXT"; + private static final String TABLE2_COLUMNS = " number TEXT, int_number INTEGER"; + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + setupDatabase(); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + public void testCursorJoinerAndIterator() { + Cursor cursor1 = getCursor(TABLE_NAME_1, null, null); + Cursor cursor2 = getCursor(TABLE_NAME_2, null, null); + // Test with different length ColumenNAmes + try { + new CursorJoiner(cursor1, cursor1.getColumnNames(), cursor2, cursor2.getColumnNames()); + fail("CursorJoiner's constructor should throws IllegalArgumentException here."); + } catch (IllegalArgumentException e) { + //expected + } + closeCursor(cursor1); + closeCursor(cursor2); + + String[] columnNames = new String[] { "number" }; + cursor1 = getCursor(TABLE_NAME_1, null, columnNames); + cursor2 = getCursor(TABLE_NAME_2, null, columnNames); + + CursorJoiner cursorJoiner = new CursorJoiner(cursor1, cursor1.getColumnNames(), cursor2, + cursor2.getColumnNames()); + + // Test remove() + try { + cursorJoiner.remove(); + fail("remove() should throws UnsupportedOperationException here"); + } catch (UnsupportedOperationException e) { + // expected + } + + assertEquals(TEST_ITEM_COUNT, cursor1.getCount()); + assertEquals(TEST_ITEM_COUNT, cursor2.getCount()); + + // Test iterator + for (CursorJoiner.Result joinResult : cursorJoiner) { + switch (joinResult) { + case LEFT: + // Add the values into table test1 which table test1 possess and table test2 don't. + assertTrue(cursor1.getString(0).compareTo(cursor2.getString(0)) < 0); + addValueIntoTable(TABLE_NAME_2, cursor1.getString(0)); + break; + case RIGHT: + // Add the values into table test2 which table test2 possess and table test1 don't. + assertTrue(cursor1.getString(0).compareTo(cursor2.getString(0)) > 0); + addValueIntoTable(TABLE_NAME_1, cursor2.getString(0)); + break; + case BOTH: + // Delete the values table test1 and test2 both possess. + assertEquals(cursor1.getString(0), cursor2.getString(0)); + deleteValueFromTable(TABLE_NAME_1, cursor1.getString(0)); + deleteValueFromTable(TABLE_NAME_2, cursor2.getString(0)); + break; + } + } + cursor1.requery(); + cursor2.requery(); + + // Finally, two tables's number columns have the same contents + assertEquals(UNIQUE_COUNT * 2, cursor1.getCount()); + assertEquals(UNIQUE_COUNT * 2, cursor2.getCount()); + + // For every table, merged with the other one's unique numbers, and deleted the originally + // mutual same numbers(EQUAL_START~MAX_VALUE); + cursor1.moveToFirst(); + cursor2.moveToFirst(); + for (int i = 0; i < UNIQUE_COUNT; i++) { + assertEquals(getOrderNumberString(DEFAULT_TABLE1_VALUE_BEGINS + i, MAX_VALUE), + cursor1.getString(0)); + assertEquals(cursor1.getString(0), cursor2.getString(0)); + cursor1.moveToNext(); + cursor2.moveToNext(); + } + closeCursor(cursor2); + closeCursor(cursor1); + } + + public void testNext() { + String[] columnNames = new String[] { "number" }; + Cursor cursor1 = getCursor(TABLE_NAME_1, null, columnNames); + Cursor cursor2 = getCursor(TABLE_NAME_2, null, columnNames); + + // For cursor1 , values are '01'~'07' and 'EQUAL_START'~'MAX_VALUE' + assertEquals(TEST_ITEM_COUNT, cursor1.getCount()); + // For cursor2 , values are '11'~'17' and 'EQUAL_START'~'MAX_VALUE' + assertEquals(TEST_ITEM_COUNT, cursor2.getCount()); + CursorJoiner cursorJoiner = new CursorJoiner(cursor1, cursor1.getColumnNames(), cursor2, + cursor2.getColumnNames()); + for (int i = 0; i < UNIQUE_COUNT; i++) { + // For cursor1, value 1~7 result value as LEFT to cursor2 value '11' + assertTrue(cursorJoiner.hasNext()); + assertEquals(Result.LEFT, cursorJoiner.next()); + assertEquals(getOrderNumberString(DEFAULT_TABLE1_VALUE_BEGINS + i, MAX_VALUE), cursor1 + .getString(0)); + assertEquals(getOrderNumberString(DEFAULT_TABLE2_VALUE_BEGINS, MAX_VALUE), cursor2 + .getString(0)); + } + for (int i = 0; i < UNIQUE_COUNT; i++) { + // For cursor2, value 11~17 result a value as LEFT to cursor1 value '18' + assertTrue(cursorJoiner.hasNext()); + assertEquals(Result.RIGHT, cursorJoiner.next()); + assertEquals(getOrderNumberString(EQUAL_START, MAX_VALUE), cursor1.getString(0)); + assertEquals(getOrderNumberString(DEFAULT_TABLE2_VALUE_BEGINS + i, MAX_VALUE), cursor2 + .getString(0)); + } + for (int i = 0; i < EQUAL_VALUE_COUNT; i++) { + // For cursor1 and cursor2, value 18~20 result a value as BOTH + assertTrue(cursorJoiner.hasNext()); + assertEquals(Result.BOTH, cursorJoiner.next()); + assertEquals(getOrderNumberString(EQUAL_START + i, MAX_VALUE), cursor1.getString(0)); + assertEquals(getOrderNumberString(EQUAL_START + i, MAX_VALUE), cursor2.getString(0)); + } + closeCursor(cursor1); + closeCursor(cursor2); + } + + /** + * This function accepts integer maxValue to determine max length of number. + * Return a converted decimal number string of input integer parameter 'value', + * according to the max length, '0' will be placeholder(s). + * For example: if max length is 2, 1 -> '01', 10 -> '10'. + * @param value + * @param maxValue + * @return + */ + private String getOrderNumberString(int value, int maxValue) { + // Convert decimal number as string, '0' as placeholder + int maxLength = Integer.toString(maxValue).length(); + int basicLength = Integer.toString(value).length(); + String placeHolders = ""; + for (int i = 0; i < (maxLength - basicLength); i++) { + placeHolders += "0"; + } + return placeHolders + Integer.toString(value); + } + + private void initializeTables() { + // Add 1 to 7 into Table1 + addValuesIntoTable(TABLE_NAME_1, DEFAULT_TABLE1_VALUE_BEGINS, + DEFAULT_TABLE1_VALUE_BEGINS + UNIQUE_COUNT - 1); + // Add 18 to 20 into Table1 + addValuesIntoTable(TABLE_NAME_1, DEFAULT_TABLE2_VALUE_BEGINS + UNIQUE_COUNT, MAX_VALUE); + // Add 11 to 17 into Table2 + addValuesIntoTable(TABLE_NAME_2, DEFAULT_TABLE2_VALUE_BEGINS, + DEFAULT_TABLE2_VALUE_BEGINS + UNIQUE_COUNT - 1); + // Add 18 to 20 into Table2 + addValuesIntoTable(TABLE_NAME_2, DEFAULT_TABLE2_VALUE_BEGINS + UNIQUE_COUNT, MAX_VALUE); + } + + private void setupDatabase() { + File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabaseFile); + createTable(TABLE_NAME_1, TABLE1_COLUMNS); + createTable(TABLE_NAME_2, TABLE2_COLUMNS); + initializeTables(); + } + + private void closeCursor(Cursor cursor) { + if (null != cursor) { + cursor.close(); + cursor = null; + } + } + + private void createTable(String tableName, String columnNames) { + String sql = "Create TABLE " + tableName + " (_id INTEGER PRIMARY KEY, " + columnNames + + " );"; + mDatabase.execSQL(sql); + } + + private void addValuesIntoTable(String tableName, int start, int end) { + for (int i = start; i <= end; i++) { + mDatabase.execSQL("INSERT INTO " + tableName + "(number) VALUES ('" + + getOrderNumberString(i, MAX_VALUE) + "');"); + } + } + + private void addValueIntoTable(String tableName, String value) { + mDatabase.execSQL("INSERT INTO " + tableName + "(number) VALUES ('" + value + "');"); + } + + private void deleteValueFromTable(String tableName, String value) { + mDatabase.execSQL("DELETE FROM " + tableName + " WHERE number = '" + value + "';"); + } + + private Cursor getCursor(String tableName, String selection, String[] columnNames) { + return mDatabase.query(tableName, columnNames, selection, null, null, null, "number"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWindowTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWindowTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWindowTest.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + +import android.database.CharArrayBuffer; +import android.database.CursorWindow; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; +import android.os.Parcel; +import android.test.AndroidTestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; + +public class CursorWindowTest extends AndroidTestCase { + + private static final String TEST_STRING = "Test String"; + + public void testWriteCursorToWindow() throws Exception { + // create cursor + String[] colNames = new String[]{"_id", "name", "number", "profit"}; + int colsize = colNames.length; + ArrayList> list = createTestList(10, colsize); + MatrixCursor cursor = new MatrixCursor(colNames, list.size()); + for (ArrayList row : list) { + cursor.addRow(row); + } + + // fill window + CursorWindow window = new CursorWindow(false); + cursor.fillWindow(0, window); + + // read from cursor window + for (int i = 0; i < list.size(); i++) { + ArrayList col = list.get(i); + for (int j = 0; j < colsize; j++) { + String s = window.getString(i, j); + int r2 = col.get(j); + int r1 = Integer.parseInt(s); + assertEquals(r2, r1); + } + } + + // test cursor window handle startpos != 0 + window.clear(); + cursor.fillWindow(1, window); + // read from cursor from window + for (int i = 1; i < list.size(); i++) { + ArrayList col = list.get(i); + for (int j = 0; j < colsize; j++) { + String s = window.getString(i, j); + int r2 = col.get(j); + int r1 = Integer.parseInt(s); + assertEquals(r2, r1); + } + } + + // Clear the window and make sure it's empty + window.clear(); + assertEquals(0, window.getNumRows()); + } + + public void testNull() { + CursorWindow window = getOneByOneWindow(); + + // Put in a null value and read it back as various types + assertTrue(window.putNull(0, 0)); + assertNull(window.getString(0, 0)); + assertEquals(0, window.getLong(0, 0)); + assertEquals(0.0, window.getDouble(0, 0)); + assertNull(window.getBlob(0, 0)); + } + + public void testEmptyString() { + CursorWindow window = getOneByOneWindow(); + + // put size 0 string and read it back as various types + assertTrue(window.putString("", 0, 0)); + assertEquals("", window.getString(0, 0)); + assertEquals(0, window.getLong(0, 0)); + assertEquals(0.0, window.getDouble(0, 0)); + } + + public void testConstructors() { + int TEST_NUMBER = 5; + CursorWindow cursorWindow; + + // Test constructor with 'true' input, and getStartPosition should return 0 + cursorWindow = new CursorWindow(true); + assertEquals(0, cursorWindow.getStartPosition()); + + // Test constructor with 'false' input + cursorWindow = new CursorWindow(false); + assertEquals(0, cursorWindow.getStartPosition()); + + // Test newFromParcel + Parcel parcel = Parcel.obtain(); + cursorWindow = new CursorWindow(true); + cursorWindow.setStartPosition(TEST_NUMBER); + cursorWindow.setNumColumns(1); + cursorWindow.allocRow(); + cursorWindow.putString(TEST_STRING, TEST_NUMBER, 0); + cursorWindow.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + cursorWindow = CursorWindow.CREATOR.createFromParcel(parcel); + assertEquals(TEST_NUMBER, cursorWindow.getStartPosition()); + assertEquals(TEST_STRING, cursorWindow.getString(TEST_NUMBER, 0)); + } + + public void testDataStructureOperations() { + CursorWindow cursorWindow = new CursorWindow(true); + + // Test with normal values + assertTrue(cursorWindow.setNumColumns(0)); + // If the column has been set to zero, can't put String. + assertFalse(cursorWindow.putString(TEST_STRING, 0, 0)); + + // Test allocRow(). + assertTrue(cursorWindow.allocRow()); + assertEquals(1, cursorWindow.getNumRows()); + assertTrue(cursorWindow.allocRow()); + assertEquals(2, cursorWindow.getNumRows()); + // Though allocate a row, but the column number is still 0, so can't putString. + assertFalse(cursorWindow.putString(TEST_STRING, 0, 0)); + + // Test freeLstRow + cursorWindow.freeLastRow(); + assertEquals(1, cursorWindow.getNumRows()); + cursorWindow.freeLastRow(); + assertEquals(0, cursorWindow.getNumRows()); + + cursorWindow = new CursorWindow(true); + assertTrue(cursorWindow.setNumColumns(6)); + assertTrue(cursorWindow.allocRow()); + // Column number set to negative number, so now can put values. + assertTrue(cursorWindow.putString(TEST_STRING, 0, 0)); + assertEquals(TEST_STRING, cursorWindow.getString(0, 0)); + + // Test with negative value + assertFalse(cursorWindow.setNumColumns(-1)); + + // Test with reference limitation + cursorWindow.releaseReference(); + try { + cursorWindow.setNumColumns(5); + fail("setNumColumns() should throws IllegalStateException here."); + } catch (IllegalStateException e) { + // expected + } + + // Test close(), close will also minus references, that will lead acquireReference() + // related operation failed. + cursorWindow.close(); + try { + cursorWindow.acquireReference(); + fail("setNumColumns() should throws IllegalStateException here."); + } catch (IllegalStateException e) { + // expected + } + } + + public void testAccessDataValues() { + final long NUMBER_LONG_INTEGER = (long) 0xaabbccddffL; + final long NUMBER_INTEGER = (int) NUMBER_LONG_INTEGER; + final long NUMBER_SHORT = (short) NUMBER_INTEGER; + final float NUMBER_FLOAT_SCIENCE = 7.332952E11f; + final double NUMBER_DOUBLE_SCIENCE = 7.33295205887E11; + final String NUMBER_FLOAT_SCIENCE_STRING = "7.332952E11"; + final String NUMBER_DOUBLE_SCIENCE_STRING = "7.33295205887E11"; + final String NUMBER_FLOAT_SCIENCE_STRING2 = "7.33295e+11"; + + byte[] originalBlob = new byte[Byte.MAX_VALUE]; + for (int i = 0; i < Byte.MAX_VALUE; i++) { + originalBlob[i] = (byte) i; + } + + CursorWindow cursorWindow = new CursorWindow(true); + cursorWindow.setNumColumns(5); + cursorWindow.allocRow(); + + // Test putString, getString, getLong, getInt, isBlob + assertTrue(cursorWindow.putString(Long.toString(NUMBER_LONG_INTEGER), 0, 0)); + assertEquals(Long.toString(NUMBER_LONG_INTEGER), cursorWindow.getString(0, 0)); + assertEquals(NUMBER_LONG_INTEGER, cursorWindow.getLong(0, 0)); + assertEquals(NUMBER_INTEGER, cursorWindow.getInt(0, 0)); + assertEquals(NUMBER_SHORT, cursorWindow.getShort(0, 0)); + // Converting of Float, there would be some little precision differences. So just compare + // first 6 digits. + assertEquals(NUMBER_FLOAT_SCIENCE_STRING.substring(0, 6), Float.toString( + cursorWindow.getFloat(0, 0)).substring(0, 6)); + assertEquals(NUMBER_DOUBLE_SCIENCE_STRING, Double.toString(cursorWindow.getDouble(0, 0))); + assertFalse(cursorWindow.isNull(0, 0)); + assertFalse(cursorWindow.isBlob(0, 0)); + + // Test null String + assertTrue(cursorWindow.putString("", 0, 0)); + assertEquals("", cursorWindow.getString(0, 0)); + assertEquals(0, cursorWindow.getLong(0, 0)); + assertEquals(0, cursorWindow.getInt(0, 0)); + assertEquals(0, cursorWindow.getShort(0, 0)); + assertEquals(0.0, cursorWindow.getDouble(0, 0)); + assertEquals(0.0f, cursorWindow.getFloat(0, 0), 0.00000001f); + assertFalse(cursorWindow.isNull(0, 0)); + assertFalse(cursorWindow.isBlob(0, 0)); + + // Test putNull, getString, getLong, getDouble, getBlob, getInd, getShort, getFloat, + // isBlob. + assertTrue(cursorWindow.putNull(0, 1)); + assertNull(cursorWindow.getString(0, 1)); + assertEquals(0, cursorWindow.getLong(0, 1)); + assertEquals(0, cursorWindow.getInt(0, 1)); + assertEquals(0, cursorWindow.getShort(0, 1)); + assertEquals(0.0, cursorWindow.getDouble(0, 1)); + assertEquals(0.0f, cursorWindow.getFloat(0, 1), 0.00000001f); + assertNull(cursorWindow.getBlob(0, 1)); + assertTrue(cursorWindow.isNull(0, 1)); + // If the field is null, isBlob will return true. + assertTrue(cursorWindow.isBlob(0, 1)); + + // Test putLong, getLong, getInt, getString , getShort, getFloat, getDouble, isBlob. + assertTrue(cursorWindow.putLong(NUMBER_LONG_INTEGER, 0, 2)); + assertEquals(NUMBER_LONG_INTEGER, cursorWindow.getLong(0, 2)); + assertEquals(NUMBER_INTEGER, cursorWindow.getInt(0, 2)); + assertEquals(Long.toString(NUMBER_LONG_INTEGER), cursorWindow.getString(0, 2)); + assertEquals(NUMBER_SHORT, cursorWindow.getShort(0, 2)); + assertEquals(NUMBER_FLOAT_SCIENCE, cursorWindow.getFloat(0, 2), 0.00000001f); + assertEquals(NUMBER_DOUBLE_SCIENCE, cursorWindow.getDouble(0, 2), 0.00000001); + try { + cursorWindow.getBlob(0, 2); + fail("Can't get Blob from a Integer value."); + } catch (SQLiteException e) { + // expected + } + assertFalse(cursorWindow.isNull(0, 2)); + assertFalse(cursorWindow.isBlob(0, 2)); + + // Test putDouble + assertTrue(cursorWindow.putDouble(NUMBER_DOUBLE_SCIENCE, 0, 3)); + assertEquals(NUMBER_LONG_INTEGER, cursorWindow.getLong(0, 3)); + assertEquals(NUMBER_INTEGER, cursorWindow.getInt(0, 3)); + // Converting from Double to String, there would be some little precision differences. So + // Just compare first 6 digits. + assertEquals(NUMBER_FLOAT_SCIENCE_STRING2.substring(0, 6), cursorWindow.getString(0, 3) + .substring(0, 6)); + assertEquals(NUMBER_SHORT, cursorWindow.getShort(0, 3)); + assertEquals(NUMBER_FLOAT_SCIENCE, cursorWindow.getFloat(0, 3), 0.00000001f); + assertEquals(NUMBER_DOUBLE_SCIENCE, cursorWindow.getDouble(0, 3), 0.00000001); + try { + cursorWindow.getBlob(0, 3); + fail("Can't get Blob from a Double value."); + } catch (SQLiteException e) { + // expected + } + assertFalse(cursorWindow.isNull(0, 3)); + assertFalse(cursorWindow.isBlob(0, 3)); + + // Test putBlob + assertTrue(cursorWindow.putBlob(originalBlob, 0, 4)); + byte[] targetBlob = cursorWindow.getBlob(0, 4); + assertTrue(Arrays.equals(originalBlob, targetBlob)); + assertFalse(cursorWindow.isNull(0, 4)); + // Test isBlob + assertTrue(cursorWindow.isBlob(0, 4)); + } + + public void testCopyStringToBuffer() { + int DEFAULT_ARRAY_LENGTH = 64; + String baseString = "0123456789"; + String expectedString = ""; + // Create a 60 characters string. + for (int i = 0; i < 6; i++) { + expectedString += baseString; + } + CharArrayBuffer charArrayBuffer = new CharArrayBuffer(null); + CursorWindow cursorWindow = new CursorWindow(true); + cursorWindow.setNumColumns(2); + cursorWindow.allocRow(); + + assertEquals(null, charArrayBuffer.data); + cursorWindow.putString(expectedString, 0, 0); + cursorWindow.copyStringToBuffer(0, 0, charArrayBuffer); + assertNotNull(charArrayBuffer.data); + // By default, if the field's string is shorter than 64, array will be allocated as 64. + assertEquals(DEFAULT_ARRAY_LENGTH, charArrayBuffer.data.length); + assertEquals(expectedString, + new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied)); + + // Test in case of string is longer than 64, + expectedString += baseString; + charArrayBuffer = new CharArrayBuffer(null); + cursorWindow.putString(expectedString, 0, 1); + cursorWindow.copyStringToBuffer(0, 1, charArrayBuffer); + assertNotNull(charArrayBuffer.data); + // If the string is longer than 64, array will be allocated as needed(longer than 64). + assertEquals(expectedString, + new String(charArrayBuffer.data, 0, charArrayBuffer.sizeCopied)); + assertEquals(70, expectedString.length()); + assertEquals(expectedString.length(), charArrayBuffer.data.length); + } + + public void testAccessStartPosition() { + final int TEST_POSITION_1 = 0; + final int TEST_POSITION_2 = 3; + + CursorWindow cursorWindow = new CursorWindow(true); + fillCursorTestContents(cursorWindow, 5); + + // Test setStartPosition + assertEquals(TEST_POSITION_1, cursorWindow.getStartPosition()); + assertEquals(3, cursorWindow.getInt(3, 0)); + assertEquals(TEST_STRING + "3", cursorWindow.getString(3, 1)); + assertEquals(4, cursorWindow.getInt(4, 0)); + assertEquals(TEST_STRING + "4", cursorWindow.getString(4, 1)); + cursorWindow.setStartPosition(TEST_POSITION_2); + + assertEquals(TEST_POSITION_2, cursorWindow.getStartPosition()); + + assertEquals(0, cursorWindow.getInt(3, 0)); + assertEquals(TEST_STRING + "0", cursorWindow.getString(3, 1)); + assertEquals(1, cursorWindow.getInt(4, 0)); + assertEquals(TEST_STRING + "1", cursorWindow.getString(4, 1)); + try { + cursorWindow.getBlob(0, 0); + fail("Row number is smaller than startPosition, will cause a IllegalStateException."); + } catch (IllegalStateException e) { + // expected + } + } + + public void testClearAndOnAllReferencesReleased() { + MockCursorWindow cursorWindow = new MockCursorWindow(true); + + assertEquals(0, cursorWindow.getNumRows()); + fillCursorTestContents(cursorWindow, 10); + assertEquals(10, cursorWindow.getNumRows()); + assertEquals(0, cursorWindow.getStartPosition()); + cursorWindow.setStartPosition(5); + assertEquals(5, cursorWindow.getStartPosition()); + + // Test clear(). a complete calling process of cursorWindow has a perfect acquiring and + // releasing pair, so the references number will be equal at the begin and the end. + assertFalse(cursorWindow.hasReleasedAllReferences()); + cursorWindow.clear(); + assertEquals(0, cursorWindow.getNumRows()); + assertEquals(0, cursorWindow.getStartPosition()); + assertFalse(cursorWindow.hasReleasedAllReferences()); + + // Test onAllReferencesReleased. + // By default, cursorWindow's reference is 1, when it reachs 0, onAllReferencesReleased + // be invoked. + cursorWindow = new MockCursorWindow(true); + cursorWindow.releaseReference(); + assertTrue(cursorWindow.hasReleasedAllReferences()); + } + + public void testDescribeContents() { + CursorWindow cursorWindow = new CursorWindow(true); + assertEquals(0, cursorWindow.describeContents()); + } + + private class MockCursorWindow extends CursorWindow { + private boolean mHasReleasedAllReferences = false; + + public MockCursorWindow(boolean localWindow) { + super(localWindow); + } + + @Override + protected void onAllReferencesReleased() { + super.onAllReferencesReleased(); + mHasReleasedAllReferences = true; + } + + public boolean hasReleasedAllReferences() { + return mHasReleasedAllReferences; + } + + public void resetStatus() { + mHasReleasedAllReferences = false; + } + } + + private void fillCursorTestContents(CursorWindow cursorWindow, int length) { + cursorWindow.clear(); + cursorWindow.setStartPosition(0); + cursorWindow.setNumColumns(2); + for (int i = 0; i < length; i++) { + cursorWindow.allocRow(); + cursorWindow.putLong(i, i, 0); + cursorWindow.putString(TEST_STRING + i, i, 1); + } + } + + private static ArrayList> createTestList(int rows, int cols) { + ArrayList> list = new ArrayList>(); + Random generator = new Random(); + + for (int i = 0; i < rows; i++) { + ArrayList col = new ArrayList(); + list.add(col); + for (int j = 0; j < cols; j++) { + // generate random number + col.add(j == 0 ? i : generator.nextInt()); + } + } + return list; + } + + /** + * The method comes from unit_test CursorWindowTest. + */ + private CursorWindow getOneByOneWindow() { + CursorWindow window = new CursorWindow(false); + assertTrue(window.setNumColumns(1)); + assertTrue(window.allocRow()); + return window; + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWrapperTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWrapperTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/CursorWrapperTest.java @@ -0,0 +1,540 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + + +import android.content.Context; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.CursorWrapper; +import android.database.DataSetObserver; +import android.database.StaleDataException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.os.Bundle; +import android.os.Handler; +import android.test.AndroidTestCase; + +import java.io.File; +import java.util.Arrays; + +public class CursorWrapperTest extends AndroidTestCase { + + private static final String FIRST_NUMBER = "123"; + private static final String SECOND_NUMBER = "5555"; + private static final int TESTVALUE1 = 199; + private static final int TESTVALUE2 = 200; + private static final String[] NUMBER_PROJECTION = new String[] { + "_id", // 0 + "number" // 1 + }; + private static final int DEFAULT_RECORD_COUNT = 2; + private static final int DEFAULT_COLUMN_COUNT = 2; + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private Cursor mCursor; + + private static final int CURRENT_DATABASE_VERSION = 42; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + setupDatabase(); + } + + @Override + protected void tearDown() throws Exception { + closeDatabase(); + super.tearDown(); + } + + public void testConstrucotorAndClose() { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + + assertTrue(cursorWrapper.requery()); + cursorWrapper.deactivate(); + cursorWrapper.move(1); + assertEquals(DEFAULT_RECORD_COUNT, cursorWrapper.getCount()); + + assertFalse(cursorWrapper.isClosed()); + assertTrue(cursorWrapper.requery()); + cursorWrapper.close(); + assertTrue(cursorWrapper.isClosed()); + assertFalse(cursorWrapper.requery()); + } + + private Cursor getCursor() { + Cursor cursor = mDatabase.query("test1", NUMBER_PROJECTION, null, null, null, null, null); + return cursor; + } + + public void testGetCount() { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + int defaultCount = cursorWrapper.getCount(); + + // Add two records into the table. + addWithValue(mDatabase, TESTVALUE1); + int expected = defaultCount + 1; + assertTrue(cursorWrapper.requery()); + assertEquals(expected, cursorWrapper.getCount()); + addWithValue(mDatabase, TESTVALUE2); + expected += 1; + assertTrue(cursorWrapper.requery()); + assertEquals(expected, cursorWrapper.getCount()); + + // Delete previous two records which have been added just now. + deleteWithValue(mDatabase, TESTVALUE1); + assertTrue(cursorWrapper.requery()); + assertEquals(defaultCount + 1, cursorWrapper.getCount()); + deleteWithValue(mDatabase, TESTVALUE2); + assertTrue(cursorWrapper.requery()); + assertEquals(defaultCount, cursorWrapper.getCount()); + + // Continue to delete all the records + deleteAllRecords(mDatabase); + assertTrue(cursorWrapper.requery()); + assertEquals(0, cursorWrapper.getCount()); + + cursorWrapper.close(); + assertFalse(cursorWrapper.requery()); + + // Restore original database status + rebuildDatabase(); + } + + public void testDeactivate() throws IllegalStateException { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + MockObserver observer = new MockObserver(); + + // one DataSetObserver can't unregistered before it had been registered. + try{ + cursorWrapper.unregisterDataSetObserver(observer); + fail("testUnregisterDataSetObserver failed"); + }catch(IllegalStateException e){ + } + + // Before registering, observer can't be notified. + assertFalse(observer.hasInvalidated()); + cursorWrapper.moveToLast(); + cursorWrapper.deactivate(); + assertFalse(observer.hasInvalidated()); + + // Test with registering DataSetObserver + assertTrue(cursorWrapper.requery()); + cursorWrapper.registerDataSetObserver(observer); + assertFalse(observer.hasInvalidated()); + cursorWrapper.moveToLast(); + assertEquals(Integer.parseInt(SECOND_NUMBER), cursorWrapper.getInt(1)); + cursorWrapper.deactivate(); + // deactivate method can invoke invalidate() method, can be observed by DataSetObserver. + assertTrue(observer.hasInvalidated()); + // After deactivating, the cursor can not provide values from database record. + try { + cursorWrapper.getInt(1); + fail("After deactivating, cursor cannot execute getting value operations."); + } catch (StaleDataException e) { + } + + // Can't register a same observer twice before unregister it. + try{ + cursorWrapper.registerDataSetObserver(observer); + fail("testRegisterDataSetObserver failed"); + }catch(IllegalStateException e){ + } + + // After runegistering, observer can't be notified. + cursorWrapper.unregisterDataSetObserver(observer); + observer.resetStatus(); + assertFalse(observer.hasInvalidated()); + cursorWrapper.moveToLast(); + cursorWrapper.deactivate(); + assertFalse(observer.hasInvalidated()); + + // one DataSetObserver can't be unregistered twice continuously. + try{ + cursorWrapper.unregisterDataSetObserver(observer); + fail("testUnregisterDataSetObserver failed"); + }catch(IllegalStateException e){ + } + } + + public void testGettingColumnInfos() { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + + assertEquals(DEFAULT_COLUMN_COUNT, cursorWrapper.getColumnCount()); + + // Test getColumnIndex + assertEquals(0, cursorWrapper.getColumnIndex("_id")); + assertEquals(1, cursorWrapper.getColumnIndex("number")); + assertEquals(-1, cursorWrapper.getColumnIndex("NON_EXISTENCE")); + + // Test getColumnIndexOrThrow + assertEquals(0, cursorWrapper.getColumnIndexOrThrow("_id")); + assertEquals(1, cursorWrapper.getColumnIndexOrThrow("number")); + try { + cursorWrapper.getColumnIndexOrThrow("NON_EXISTENCE"); + fail("getColumnIndexOrThrow should throws IllegalArgumentException if the column" + + "does not exist"); + } catch (IllegalArgumentException e) { + } + + assertEquals("_id", cursorWrapper.getColumnName(0)); + assertEquals("number", cursorWrapper.getColumnName(1)); + + String[] columnNames = cursorWrapper.getColumnNames(); + assertEquals(DEFAULT_COLUMN_COUNT, cursorWrapper.getColumnCount()); + assertEquals("_id", columnNames[0]); + assertEquals("number", columnNames[1]); + cursorWrapper.close(); + } + + public void testPositioning() { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + + // There are totally 2 records. + // At first, the cursor is at beginning position: -1 + // Test isBeforeFirst, getPosition, isFirst + assertTrue(cursorWrapper.isBeforeFirst()); + assertEquals(-1, cursorWrapper.getPosition()); + assertFalse(cursorWrapper.isFirst()); + + // Test moveToNext + assertTrue(cursorWrapper.moveToNext()); + assertEquals(0, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.isFirst()); + + // Test isLast + assertFalse(cursorWrapper.isLast()); + assertTrue(cursorWrapper.moveToNext()); + assertEquals(1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.isLast()); + + // move to the end + // Test isLast and isAfterLast + assertFalse(cursorWrapper.moveToNext()); + assertFalse(cursorWrapper.isLast()); + assertTrue(cursorWrapper.isAfterLast()); + assertEquals(2, cursorWrapper.getPosition()); + + // Test move(int) + assertTrue(cursorWrapper.move(-1)); + assertEquals(1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.move(-1)); + assertEquals(0, cursorWrapper.getPosition()); + // While reach the edge, function will return false + assertFalse(cursorWrapper.move(-1)); + assertEquals(-1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.move(2)); + assertEquals(1, cursorWrapper.getPosition()); + // While reach the edge, function will return false + assertFalse(cursorWrapper.move(1)); + assertTrue(cursorWrapper.isAfterLast()); + + // Test moveToPrevious() + assertEquals(2, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.moveToPrevious()); + assertEquals(1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.moveToPrevious()); + assertEquals(0, cursorWrapper.getPosition()); + // While reach the edge, function will return false + assertFalse(cursorWrapper.moveToPrevious()); + assertEquals(-1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.isBeforeFirst()); + + // Test moveToPosition + // While reach the edge, function will return false + assertFalse(cursorWrapper.moveToPosition(2)); + assertEquals(2, cursorWrapper.getPosition()); + // While reach the edge, function will return false + assertFalse(cursorWrapper.moveToPosition(-1)); + assertEquals(-1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.moveToPosition(1)); + assertEquals(1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.moveToPosition(0)); + assertEquals(0, cursorWrapper.getPosition()); + + // Test moveToFirst and moveToFirst + assertFalse(cursorWrapper.isLast()); + assertTrue(cursorWrapper.moveToLast()); + assertEquals(1, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.isLast()); + assertFalse(cursorWrapper.isFirst()); + assertTrue(cursorWrapper.moveToFirst()); + assertEquals(0, cursorWrapper.getPosition()); + assertTrue(cursorWrapper.isFirst()); + cursorWrapper.close(); + } + + public void testGettingValues() { + final byte NUMBER_BLOB_UNIT = 99; + final String STRING_TEXT = "Test String"; + final String STRING_TEXT2 = "Test String2"; + final double NUMBER_DOUBLE = Double.MAX_VALUE; + final double NUMBER_FLOAT = (float) NUMBER_DOUBLE; + final long NUMBER_LONG_INTEGER = 0xaabbccddffL; + final long NUMBER_INTEGER = (int) NUMBER_LONG_INTEGER; + final long NUMBER_SHORT = (short) NUMBER_INTEGER; + + assertTrue(NUMBER_DOUBLE != NUMBER_FLOAT); + assertTrue(NUMBER_LONG_INTEGER != NUMBER_INTEGER); + assertTrue(NUMBER_LONG_INTEGER != (short) NUMBER_SHORT); + assertTrue(NUMBER_INTEGER != (int) NUMBER_SHORT); + + // create table + mDatabase.execSQL("CREATE TABLE test2 (_id INTEGER PRIMARY KEY, string_text TEXT," + + "double_number REAL, int_number INTEGER, blob_data BLOB);"); + // insert blob and other values + Object[] args = new Object[4]; + byte[] originalBlob = new byte[1000]; + Arrays.fill(originalBlob, NUMBER_BLOB_UNIT); + args[0] = STRING_TEXT; + args[1] = NUMBER_DOUBLE; + args[2] = NUMBER_LONG_INTEGER; + args[3] = originalBlob; + + // Insert record. + String sql = "INSERT INTO test2 (string_text, double_number, int_number, blob_data)" + + "VALUES (?,?,?,?)"; + mDatabase.execSQL(sql, args); + // use cursor to access blob + Cursor cursor = mDatabase.query("test2", null, null, null, null, null, null); + + // Test getColumnCount + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + assertEquals(DEFAULT_COLUMN_COUNT, cursorWrapper.getColumnCount()); + cursorWrapper.close(); + cursorWrapper = new CursorWrapper(cursor); + assertEquals(5, cursorWrapper.getColumnCount()); + + cursorWrapper.moveToNext(); + int columnBlob = cursorWrapper.getColumnIndexOrThrow("blob_data"); + int columnString = cursorWrapper.getColumnIndexOrThrow("string_text"); + int columnDouble = cursorWrapper.getColumnIndexOrThrow("double_number"); + int columnInteger = cursorWrapper.getColumnIndexOrThrow("int_number"); + + // Test getting value methods. + byte[] targetBlob = cursorWrapper.getBlob(columnBlob); + assertTrue(Arrays.equals(originalBlob, targetBlob)); + + assertEquals(STRING_TEXT, cursorWrapper.getString(columnString)); + + assertEquals(NUMBER_DOUBLE, cursorWrapper.getDouble(columnDouble), 0.000000000001); + + assertEquals(NUMBER_FLOAT, cursorWrapper.getFloat(columnDouble), 0.000000000001f); + + assertEquals(NUMBER_LONG_INTEGER, cursorWrapper.getLong(columnInteger)); + + assertEquals(NUMBER_INTEGER, cursorWrapper.getInt(columnInteger)); + + assertEquals(NUMBER_SHORT, cursorWrapper.getShort(columnInteger)); + + // Test isNull(int). + assertFalse(cursorWrapper.isNull(columnBlob)); + sql = "INSERT INTO test2 (string_text) VALUES ('" + STRING_TEXT2 + "')"; + mDatabase.execSQL(sql); + cursorWrapper.close(); + cursor = mDatabase.query("test2", null, null, null, null, null, null); + cursorWrapper = new CursorWrapper(cursor); + cursorWrapper.moveToPosition(1); + assertTrue(cursorWrapper.isNull(columnBlob)); + + mDatabase.execSQL("DROP TABLE test2"); + } + + public void testGetExtras() { + CursorWrapper cursor = new CursorWrapper(getCursor()); + Bundle bundle = cursor.getExtras(); + assertSame(Bundle.EMPTY, bundle); + } + + public void testCopyStringToBuffer() { + CharArrayBuffer charArrayBuffer = new CharArrayBuffer(1000); + Cursor cursor = getCursor(); + + CursorWrapper cursorWrapper = new CursorWrapper(cursor); + cursorWrapper.moveToFirst(); + + assertEquals(0, charArrayBuffer.sizeCopied); + cursorWrapper.copyStringToBuffer(0, charArrayBuffer); + String string = new String(charArrayBuffer.data); + assertTrue(charArrayBuffer.sizeCopied > 0); + assertEquals("1", string.substring(0, charArrayBuffer.sizeCopied)); + + cursorWrapper.copyStringToBuffer(1, charArrayBuffer); + string = new String(charArrayBuffer.data); + assertTrue(charArrayBuffer.sizeCopied > 0); + assertEquals(FIRST_NUMBER, string.substring(0, charArrayBuffer.sizeCopied)); + + cursorWrapper.moveToNext(); + cursorWrapper.copyStringToBuffer(1, charArrayBuffer); + string = new String(charArrayBuffer.data); + assertTrue(charArrayBuffer.sizeCopied > 0); + assertEquals(SECOND_NUMBER, string.substring(0, charArrayBuffer.sizeCopied)); + cursorWrapper.close(); + } + + public void testRespond() { + Bundle b = new Bundle(); + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + Bundle bundle = cursorWrapper.respond(b); + assertSame(Bundle.EMPTY, bundle); + cursorWrapper.close(); + } + + public void testGetWantsAllOnMoveCalls() { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + assertFalse(cursorWrapper.getWantsAllOnMoveCalls()); + cursorWrapper.close(); + } + + public void testContentObserverOperations() throws IllegalStateException { + CursorWrapper cursorWrapper = new CursorWrapper(getCursor()); + MockContentObserver observer = new MockContentObserver(null); + + // Can't unregister a Observer before it has been registered. + try{ + cursorWrapper.unregisterContentObserver(observer); + fail("testUnregisterContentObserver failed"); + }catch(IllegalStateException e){ + assertTrue(true); + } + + cursorWrapper.registerContentObserver(observer); + + // Can't register a same observer twice before unregister it. + try{ + cursorWrapper.registerContentObserver(observer); + fail("testRegisterContentObserver failed"); + }catch(IllegalStateException e){ + } + + cursorWrapper.unregisterContentObserver(observer); + // one Observer can be registered again after it has been unregistered. + cursorWrapper.registerContentObserver(observer); + + cursorWrapper.unregisterContentObserver(observer); + + try{ + cursorWrapper.unregisterContentObserver(observer); + fail("testUnregisterContentObserver failed"); + }catch(IllegalStateException e){ + } + cursorWrapper.close(); + } + + public void testSetExtras() { + Cursor cursor = getCursor(); + CursorWrapper cursorWrapper = new CursorWrapper(cursor); + try { + Bundle b = new Bundle(); + cursorWrapper.setExtras(b); + assertSame(b, cursor.getExtras()); + } finally { + cursorWrapper.close(); + } + } + + private class MockContentObserver extends ContentObserver { + + public MockContentObserver(Handler handler) { + super(handler); + } + } + + private void deleteWithValue(SQLiteDatabase database, int value) { + database.execSQL("DELETE FROM test1 WHERE number = " + value + ";"); + } + + private void addWithValue(SQLiteDatabase database, int value) { + database.execSQL("INSERT INTO test1 (number) VALUES ('" + value + "');"); + } + + private void deleteAllRecords(SQLiteDatabase database) { + database.delete("test1", null, null); + } + + private void setupDatabase() { + File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + /* don't use the same database name as the one in super class + * this class's setUp() method deletes a database file just opened by super.setUp(). + * that can cause corruption in database in the following situation: + * super.setUp() creates the database, inserts some data into it. + * this class setUp() deletes just the database file but not the associated + * database files such as wal, shm files. + * solution is to have this class delete the whole database directory. + * better yet, this class shouldn't extend DatabaseCursortest at all. + * TODO: fix this bogus cts class hierarchy + */ + mDatabaseFile = new File(dbDir, "cursor_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + mDatabase.setVersion(CURRENT_DATABASE_VERSION); + mDatabase.execSQL("CREATE TABLE test1 (_id INTEGER PRIMARY KEY, number TEXT);"); + mDatabase.execSQL("INSERT INTO test1 (number) VALUES ('" + FIRST_NUMBER + "');"); + mDatabase.execSQL("INSERT INTO test1 (number) VALUES ('" + SECOND_NUMBER + "');"); + mCursor = getCursor(); + } + + private void closeDatabase() { + if (null != mCursor) { + mCursor.close(); + mCursor = null; + } + mDatabase.close(); + mDatabaseFile.delete(); + } + + private void rebuildDatabase() { + closeDatabase(); + setupDatabase(); + } + + private class MockObserver extends DataSetObserver { + private boolean mHasChanged = false; + private boolean mHasInvalidated = false; + + @Override + public void onChanged() { + super.onChanged(); + mHasChanged = true; + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + mHasInvalidated = true; + } + + protected void resetStatus() { + mHasChanged = false; + mHasInvalidated = false; + } + + protected boolean hasChanged() { + return mHasChanged; + } + + protected boolean hasInvalidated () { + return mHasInvalidated; + } + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseCursorTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseCursorTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseCursorTest.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.CursorIndexOutOfBoundsException; +import android.database.CursorWrapper; +import android.database.DataSetObserver; +import org.sqlite.database.DatabaseUtils; +import org.sqlite.database.sqlite.SQLiteCursor; +import org.sqlite.database.sqlite.SQLiteCursorDriver; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteQuery; +import org.sqlite.database.sqlite.SQLiteStatement; +import android.os.Looper; +import android.test.AndroidTestCase; +import android.test.PerformanceTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.util.Log; + +import java.io.File; +import java.util.Arrays; +import java.util.Random; + +public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTestCase { + private static final String sString1 = "this is a test"; + private static final String sString2 = "and yet another test"; + private static final String sString3 = "this string is a little longer, but still a test"; + + private static final int CURRENT_DATABASE_VERSION = 42; + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + protected static final int TYPE_CURSOR = 0; + protected static final int TYPE_CURSORWRAPPER = 1; + private int mTestType = TYPE_CURSOR; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + mDatabase.setVersion(CURRENT_DATABASE_VERSION); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + public void setupTestType(int testType) { + mTestType = testType; + } + + private Cursor getTestCursor(Cursor cursor) { + switch (mTestType) { + case TYPE_CURSORWRAPPER: + return new CursorWrapper(cursor); + case TYPE_CURSOR: + default: + return cursor; + } + } + + public boolean isPerformanceOnly() { + return false; + } + + // These test can only be run once. + public int startPerformance(Intermediates intermediates) { + return 1; + } + + private void populateDefaultTable() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString1 + "');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString2 + "');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString3 + "');"); + } + + @MediumTest + public void testBlob() throws Exception { + // create table + mDatabase.execSQL( + "CREATE TABLE test (_id INTEGER PRIMARY KEY, s TEXT, d REAL, l INTEGER, b BLOB);"); + // insert blob + Object[] args = new Object[4]; + + byte[] blob = new byte[1000]; + byte value = 99; + Arrays.fill(blob, value); + args[3] = blob; + + String s = new String("text"); + args[0] = s; + Double d = 99.9; + args[1] = d; + Long l = (long) 1000; + args[2] = l; + + String sql = "INSERT INTO test (s, d, l, b) VALUES (?,?,?,?)"; + mDatabase.execSQL(sql, args); + // use cursor to access blob + + Cursor testCursor = mDatabase.query("test", null, null, null, null, null, null); + + testCursor.moveToNext(); + ContentValues cv = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(testCursor, cv); + + int bCol = testCursor.getColumnIndexOrThrow("b"); + int sCol = testCursor.getColumnIndexOrThrow("s"); + int dCol = testCursor.getColumnIndexOrThrow("d"); + int lCol = testCursor.getColumnIndexOrThrow("l"); + byte[] cBlob = testCursor.getBlob(bCol); + assertTrue(Arrays.equals(blob, cBlob)); + assertEquals(s, testCursor.getString(sCol)); + assertEquals((double) d, testCursor.getDouble(dCol)); + assertEquals((long) l, testCursor.getLong(lCol)); + } + + @MediumTest + public void testRealColumns() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data REAL);"); + ContentValues values = new ContentValues(); + values.put("data", 42.11); + long id = mDatabase.insert("test", "data", values); + assertTrue(id > 0); + Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT data FROM test", null)); + assertNotNull(testCursor); + assertTrue(testCursor.moveToFirst()); + assertEquals(42.11, testCursor.getDouble(0)); + testCursor.close(); + } + + @MediumTest + public void testCursor1() throws Exception { + populateDefaultTable(); + + Cursor testCursor = getTestCursor(mDatabase.query("test", null, null, null, null, null, + null)); + + int dataColumn = testCursor.getColumnIndexOrThrow("data"); + + // The cursor should ignore text before the last period when looking for a column. (This + // is a temporary hack in all implementations of getColumnIndex.) + int dataColumn2 = testCursor.getColumnIndexOrThrow("junk.data"); + assertEquals(dataColumn, dataColumn2); + + assertSame(3, testCursor.getCount()); + + assertTrue(testCursor.isBeforeFirst()); + + try { + testCursor.getInt(0); + fail("CursorIndexOutOfBoundsException expected"); + } catch (CursorIndexOutOfBoundsException ex) { + // expected + } + + testCursor.moveToNext(); + assertEquals(1, testCursor.getInt(0)); + + String s = testCursor.getString(dataColumn); + assertEquals(sString1, s); + + testCursor.moveToNext(); + s = testCursor.getString(dataColumn); + assertEquals(sString2, s); + + testCursor.moveToNext(); + s = testCursor.getString(dataColumn); + assertEquals(sString3, s); + + testCursor.moveToPosition(-1); + testCursor.moveToNext(); + s = testCursor.getString(dataColumn); + assertEquals(sString1, s); + + testCursor.moveToPosition(2); + s = testCursor.getString(dataColumn); + assertEquals(sString3, s); + + int i; + + for (testCursor.moveToFirst(), i = 0; !testCursor.isAfterLast(); + testCursor.moveToNext(), i++) { + testCursor.getInt(0); + } + + assertEquals(3, i); + + try { + testCursor.getInt(0); + fail("CursorIndexOutOfBoundsException expected"); + } catch (CursorIndexOutOfBoundsException ex) { + // expected + } + testCursor.close(); + } + + @MediumTest + public void testCursor2() throws Exception { + populateDefaultTable(); + + Cursor testCursor = getTestCursor(mDatabase.query("test", null, "_id > 1000", null, null, + null, null)); + assertEquals(0, testCursor.getCount()); + assertTrue(testCursor.isBeforeFirst()); + + try { + testCursor.getInt(0); + fail("CursorIndexOutOfBoundsException expected"); + } catch (CursorIndexOutOfBoundsException ex) { + // expected + } + + int i; + for (testCursor.moveToFirst(), i = 0; !testCursor.isAfterLast(); + testCursor.moveToNext(), i++) { + testCursor.getInt(0); + } + assertEquals(0, i); + try { + testCursor.getInt(0); + fail("CursorIndexOutOfBoundsException expected"); + } catch (CursorIndexOutOfBoundsException ex) { + // expected + } + testCursor.close(); + } + + @MediumTest + public void testLargeField() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + + StringBuilder sql = new StringBuilder(2100); + sql.append("INSERT INTO test (data) VALUES ('"); + Random random = new Random(System.currentTimeMillis()); + StringBuilder randomString = new StringBuilder(1979); + for (int i = 0; i < 1979; i++) { + randomString.append((random.nextInt() & 0xf) % 10); + } + sql.append(randomString); + sql.append("');"); + mDatabase.execSQL(sql.toString()); + + Cursor testCursor = getTestCursor(mDatabase.query("test", null, null, null, null, null, + null)); + assertNotNull(testCursor); + assertEquals(1, testCursor.getCount()); + + assertTrue(testCursor.moveToFirst()); + assertEquals(0, testCursor.getPosition()); + String largeString = testCursor.getString(testCursor.getColumnIndexOrThrow("data")); + assertNotNull(largeString); + assertEquals(randomString.toString(), largeString); + testCursor.close(); + } + + private class TestObserver extends DataSetObserver { + int total; + SQLiteCursor c; + boolean quit = false; + + public TestObserver(int total_, SQLiteCursor cursor) { + c = cursor; + total = total_; + } + + @Override + public void onChanged() { + int count = c.getCount(); + if (total == count) { + int i = 0; + while (c.moveToNext()) { + assertEquals(i, c.getInt(1)); + i++; + } + assertEquals(count, i); + quit = true; + Looper.myLooper().quit(); + } + } + + @Override + public void onInvalidated() { + } + } + + @LargeTest + public void testManyRowsLong() throws Exception { + mDatabase.beginTransaction(); + final int count = 9000; + try { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); + + for (int i = 0; i < count; i++) { + mDatabase.execSQL("INSERT INTO test (data) VALUES (" + i + ");"); + } + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + + Cursor testCursor = getTestCursor(mDatabase.query("test", new String[] { "data" }, + null, null, null, null, null)); + assertNotNull(testCursor); + + int i = 0; + while (testCursor.moveToNext()) { + assertEquals(i, testCursor.getInt(0)); + i++; + } + assertEquals(count, i); + assertEquals(count, testCursor.getCount()); + + Log.d("testManyRows", "count " + Integer.toString(i)); + testCursor.close(); + } + + @LargeTest + public void testManyRowsTxt() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + StringBuilder sql = new StringBuilder(2100); + sql.append("INSERT INTO test (data) VALUES ('"); + Random random = new Random(System.currentTimeMillis()); + StringBuilder randomString = new StringBuilder(1979); + for (int i = 0; i < 1979; i++) { + randomString.append((random.nextInt() & 0xf) % 10); + } + sql.append(randomString); + sql.append("');"); + + // if cursor window size changed, adjust this value too + final int count = 600; // more than two fillWindow needed + for (int i = 0; i < count; i++) { + mDatabase.execSQL(sql.toString()); + } + + Cursor testCursor = getTestCursor(mDatabase.query("test", new String[] { "data" }, null, + null, null, null, null)); + assertNotNull(testCursor); + + int i = 0; + while (testCursor.moveToNext()) { + assertEquals(randomString.toString(), testCursor.getString(0)); + i++; + } + assertEquals(count, i); + assertEquals(count, testCursor.getCount()); + testCursor.close(); + } + + @LargeTest + public void testManyRowsTxtLong() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, txt TEXT, data INT);"); + + Random random = new Random(System.currentTimeMillis()); + StringBuilder randomString = new StringBuilder(1979); + for (int i = 0; i < 1979; i++) { + randomString.append((random.nextInt() & 0xf) % 10); + } + + // if cursor window size changed, adjust this value too + final int count = 600; + for (int i = 0; i < count; i++) { + StringBuilder sql = new StringBuilder(2100); + sql.append("INSERT INTO test (txt, data) VALUES ('"); + sql.append(randomString); + sql.append("','"); + sql.append(i); + sql.append("');"); + mDatabase.execSQL(sql.toString()); + } + + Cursor testCursor = getTestCursor(mDatabase.query("test", new String[] { "txt", "data" }, + null, null, null, null, null)); + assertNotNull(testCursor); + + int i = 0; + while (testCursor.moveToNext()) { + assertEquals(randomString.toString(), testCursor.getString(0)); + assertEquals(i, testCursor.getInt(1)); + i++; + } + assertEquals(count, i); + assertEquals(count, testCursor.getCount()); + testCursor.close(); + } + + @MediumTest + public void testRequery() throws Exception { + populateDefaultTable(); + + Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT * FROM test", null)); + assertNotNull(testCursor); + assertEquals(3, testCursor.getCount()); + testCursor.deactivate(); + testCursor.requery(); + assertEquals(3, testCursor.getCount()); + testCursor.close(); + } + + @MediumTest + public void testRequeryWithSelection() throws Exception { + populateDefaultTable(); + + Cursor testCursor = getTestCursor( + mDatabase.rawQuery("SELECT data FROM test WHERE data = '" + sString1 + "'", + null)); + assertNotNull(testCursor); + assertEquals(1, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + testCursor.deactivate(); + testCursor.requery(); + assertEquals(1, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + testCursor.close(); + } + + @MediumTest + public void testRequeryWithSelectionArgs() throws Exception { + populateDefaultTable(); + + Cursor testCursor = getTestCursor(mDatabase.rawQuery("SELECT data FROM test WHERE data = ?", + new String[] { sString1 })); + assertNotNull(testCursor); + assertEquals(1, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + testCursor.deactivate(); + testCursor.requery(); + assertEquals(1, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + testCursor.close(); + } + + @MediumTest + public void testRequeryWithAlteredSelectionArgs() throws Exception { + /** + * Test the ability of a subclass of SQLiteCursor to change its query arguments. + */ + populateDefaultTable(); + + SQLiteDatabase.CursorFactory factory = new SQLiteDatabase.CursorFactory() { + public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, + String editTable, SQLiteQuery query) { + return new SQLiteCursor(db, masterQuery, editTable, query) { + @Override + public boolean requery() { + setSelectionArguments(new String[] { "2" }); + return super.requery(); + } + }; + } + }; + Cursor testCursor = getTestCursor(mDatabase.rawQueryWithFactory(factory, + "SELECT data FROM test WHERE _id <= ?", + new String[] { "1" }, null)); + assertNotNull(testCursor); + assertEquals(1, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + + // Our hacked requery() changes the query arguments in the cursor. + testCursor.requery(); + + assertEquals(2, testCursor.getCount()); + assertTrue(testCursor.moveToFirst()); + assertEquals(sString1, testCursor.getString(0)); + assertTrue(testCursor.moveToNext()); + assertEquals(sString2, testCursor.getString(0)); + + // Test that setting query args on a deactivated cursor also works. + testCursor.deactivate(); + testCursor.requery(); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtilsTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtilsTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtilsTest.java @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.DatabaseUtils; +import org.sqlite.database.DatabaseUtils.InsertHelper; +import org.sqlite.database.sqlite.SQLiteAbortException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDoneException; +import org.sqlite.database.sqlite.SQLiteException; +import org.sqlite.database.sqlite.SQLiteStatement; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +public class DatabaseUtilsTest extends AndroidTestCase { + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private static final String[] TEST_PROJECTION = new String[] { + "_id", // 0 + "name", // 1 + "age", // 2 + "address" // 3 + }; + private static final String TABLE_NAME = "test"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabase); + mDatabase.execSQL("CREATE TABLE " + TABLE_NAME + " (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + mDatabase.execSQL( + "CREATE TABLE blob_test (_id INTEGER PRIMARY KEY, name TEXT, data BLOB)"); + mDatabase.execSQL( + "CREATE TABLE boolean_test (_id INTEGER PRIMARY KEY, value BOOLEAN)"); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + public void testAppendEscapedSQLString() { + String expected = "name='Mike'"; + StringBuilder sb = new StringBuilder("name="); + DatabaseUtils.appendEscapedSQLString(sb, "Mike"); + assertEquals(expected, sb.toString()); + + expected = "'name=''Mike'''"; + sb = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(sb, "name='Mike'"); + assertEquals(expected, sb.toString()); + } + + public void testSqlEscapeString() { + String expected = "'Jack'"; + assertEquals(expected, DatabaseUtils.sqlEscapeString("Jack")); + } + + public void testAppendValueToSql() { + String expected = "address='LA'"; + StringBuilder sb = new StringBuilder("address="); + DatabaseUtils.appendValueToSql(sb, "LA"); + assertEquals(expected, sb.toString()); + + expected = "address=NULL"; + sb = new StringBuilder("address="); + DatabaseUtils.appendValueToSql(sb, null); + assertEquals(expected, sb.toString()); + + expected = "flag=1"; + sb = new StringBuilder("flag="); + DatabaseUtils.appendValueToSql(sb, true); + assertEquals(expected, sb.toString()); + } + + public void testBindObjectToProgram() { + String name = "Mike"; + int age = 21; + String address = "LA"; + + // at the beginning, there are no records in the database. + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + + String sql = "INSERT INTO " + TABLE_NAME + " (name, age, address) VALUES (?, ?, ?);"; + SQLiteStatement statement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(statement, 1, name); + DatabaseUtils.bindObjectToProgram(statement, 2, age); + DatabaseUtils.bindObjectToProgram(statement, 3, address); + statement.execute(); + statement.close(); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals(name, cursor.getString(1)); + assertEquals(age, cursor.getInt(2)); + assertEquals(address, cursor.getString(3)); + } + + public void testCreateDbFromSqlStatements() { + String dbName = "ExampleName"; + String sqls = "CREATE TABLE " + TABLE_NAME + " (_id INTEGER PRIMARY KEY, name TEXT);\n" + + "INSERT INTO " + TABLE_NAME + " (name) VALUES ('Mike');\n"; + File f = mContext.getDatabasePath(dbName); + f.mkdirs(); + f.delete(); + DatabaseUtils.createDbFromSqlStatements(getContext(), f.toString(), 1, sqls); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f,null); + + final String[] PROJECTION = new String[] { + "_id", // 0 + "name" // 1 + }; + Cursor cursor = db.query(TABLE_NAME, PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(1)); + getContext().deleteDatabase(dbName); + } + + public void testCursorDoubleToContentValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + String key = "key"; + cursor.moveToFirst(); + DatabaseUtils.cursorDoubleToContentValues(cursor, "age", contentValues, key); + assertEquals(20.0, contentValues.getAsDouble(key)); + + DatabaseUtils.cursorDoubleToContentValues(cursor, "Error Field Name", contentValues, key); + assertNull(contentValues.getAsDouble(key)); + + DatabaseUtils.cursorDoubleToContentValues(cursor, "name", contentValues, key); + assertEquals(0.0, contentValues.getAsDouble(key)); + } + + public void testCursorDoubleToCursorValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + cursor.moveToFirst(); + DatabaseUtils.cursorDoubleToCursorValues(cursor, "age", contentValues); + assertEquals(20.0, contentValues.getAsDouble("age")); + + DatabaseUtils.cursorDoubleToCursorValues(cursor, "Error Field Name", contentValues); + assertNull(contentValues.getAsDouble("Error Field Name")); + + DatabaseUtils.cursorDoubleToCursorValues(cursor, "name", contentValues); + assertEquals(0.0, contentValues.getAsDouble("name")); + } + + public void testCursorIntToContentValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + String key = "key"; + cursor.moveToFirst(); + DatabaseUtils.cursorIntToContentValues(cursor, "age", contentValues, key); + assertEquals(Integer.valueOf(20), contentValues.getAsInteger(key)); + + DatabaseUtils.cursorIntToContentValues(cursor, "Error Field Name", contentValues, key); + assertNull(contentValues.getAsInteger(key)); + + DatabaseUtils.cursorIntToContentValues(cursor, "name", contentValues, key); + assertEquals(Integer.valueOf(0), contentValues.getAsInteger(key)); + + contentValues = new ContentValues(); + DatabaseUtils.cursorIntToContentValues(cursor, "age", contentValues); + assertEquals(Integer.valueOf(20), contentValues.getAsInteger("age")); + + DatabaseUtils.cursorIntToContentValues(cursor, "Error Field Name", contentValues); + assertNull(contentValues.getAsInteger("Error Field Name")); + + DatabaseUtils.cursorIntToContentValues(cursor, "name", contentValues); + assertEquals(Integer.valueOf(0), contentValues.getAsInteger("name")); + } + + public void testcursorLongToContentValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + String key = "key"; + cursor.moveToNext(); + DatabaseUtils.cursorLongToContentValues(cursor, "age", contentValues, key); + assertEquals(Long.valueOf(20), contentValues.getAsLong(key)); + + DatabaseUtils.cursorLongToContentValues(cursor, "Error Field Name", contentValues, key); + assertNull(contentValues.getAsLong(key)); + + DatabaseUtils.cursorLongToContentValues(cursor, "name", contentValues, key); + assertEquals(Long.valueOf(0), contentValues.getAsLong(key)); + + contentValues = new ContentValues(); + DatabaseUtils.cursorLongToContentValues(cursor, "age", contentValues); + assertEquals(Long.valueOf(20), contentValues.getAsLong("age")); + + DatabaseUtils.cursorLongToContentValues(cursor, "Error Field Name", contentValues); + assertNull(contentValues.getAsLong("Error Field Name")); + + DatabaseUtils.cursorLongToContentValues(cursor, "name", contentValues); + assertEquals(Long.valueOf(0), contentValues.getAsLong("name")); + } + + public void testCursorRowToContentValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + cursor.moveToNext(); + DatabaseUtils.cursorRowToContentValues(cursor, contentValues); + assertEquals("Mike", (String) contentValues.get("name")); + assertEquals("20", (String) contentValues.get("age")); + assertEquals("LA", (String) contentValues.get("address")); + + mDatabase.execSQL("INSERT INTO boolean_test (value)" + + " VALUES (0);"); + mDatabase.execSQL("INSERT INTO boolean_test (value)" + + " VALUES (1);"); + cursor = mDatabase.query("boolean_test", new String[] {"value"}, + null, null, null, null, null); + assertNotNull(cursor); + + contentValues = new ContentValues(); + cursor.moveToNext(); + DatabaseUtils.cursorRowToContentValues(cursor, contentValues); + assertFalse(contentValues.getAsBoolean("value")); + cursor.moveToNext(); + DatabaseUtils.cursorRowToContentValues(cursor, contentValues); + assertTrue(contentValues.getAsBoolean("value")); + } + + public void testCursorStringToContentValues() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + + ContentValues contentValues = new ContentValues(); + String key = "key"; + cursor.moveToNext(); + DatabaseUtils.cursorStringToContentValues(cursor, "age", contentValues, key); + assertEquals("20", (String) contentValues.get(key)); + + try { + DatabaseUtils.cursorStringToContentValues(cursor, "Error Field Name", + contentValues, key); + fail("should throw IllegalArgumentException."); + } catch (IllegalArgumentException e) { + // expected + } + + DatabaseUtils.cursorStringToContentValues(cursor, "name", contentValues, key); + assertEquals("Mike", contentValues.get(key)); + + contentValues = new ContentValues(); + DatabaseUtils.cursorStringToContentValues(cursor, "age", contentValues); + assertEquals("20", contentValues.get("age")); + + try { + DatabaseUtils.cursorStringToContentValues(cursor, "Error Field Name", contentValues); + fail("should throw IllegalArgumentException."); + } catch (IllegalArgumentException e) { + // expected + } + + DatabaseUtils.cursorStringToContentValues(cursor, "name", contentValues); + assertEquals("Mike", contentValues.get("name")); + } + + public void testCursorStringToInsertHelper() { + // create a new table. + mDatabase.execSQL("CREATE TABLE test_copy (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query("test_copy", TEST_PROJECTION, null, null, null, null, null); + assertEquals(0, cursor.getCount()); + + InsertHelper insertHelper = new InsertHelper(mDatabase, "test_copy"); + int indexName = insertHelper.getColumnIndex("name"); + int indexAge = insertHelper.getColumnIndex("age"); + int indexAddress = insertHelper.getColumnIndex("address"); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + cursor.moveToNext(); + insertHelper.prepareForInsert(); + DatabaseUtils.cursorStringToInsertHelper(cursor, "name", insertHelper, indexName); + DatabaseUtils.cursorStringToInsertHelper(cursor, "age", insertHelper, indexAge); + DatabaseUtils.cursorStringToInsertHelper(cursor, "address", insertHelper, indexAddress); + insertHelper.execute(); + + cursor = mDatabase.query("test_copy", TEST_PROJECTION, null, null, null, null, null); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(1)); + assertEquals(20, cursor.getInt(2)); + assertEquals("LA", cursor.getString(3)); + } + + public void testDumpCurrentRow() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + cursor.moveToNext(); + String expected = "0 {\n _id=1\n name=Mike\n age=20\n address=LA\n}\n"; + + DatabaseUtils.dumpCurrentRow(cursor); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream os = new PrintStream(bos); + DatabaseUtils.dumpCurrentRow(cursor, os); + os.flush(); + os.close(); + assertEquals(expected, bos.toString()); + + StringBuilder sb = new StringBuilder(); + DatabaseUtils.dumpCurrentRow(cursor, sb); + assertEquals(expected, sb.toString()); + + assertEquals(expected, DatabaseUtils.dumpCurrentRowToString(cursor)); + } + + public void testDumpCursor() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Jack', '30', 'London');"); + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + int pos = cursor.getPosition(); + String expected = ">>>>> Dumping cursor " + cursor + "\n" + + "0 {\n" + + " _id=1\n" + + " name=Mike\n" + + " age=20\n" + + " address=LA\n" + + "}\n" + + "1 {\n" + + " _id=2\n" + + " name=Jack\n" + + " age=30\n" + + " address=London\n" + + "}\n" + + "<<<<<\n"; + + DatabaseUtils.dumpCursor(cursor); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream os = new PrintStream(bos); + DatabaseUtils.dumpCursor(cursor, os); + os.flush(); + os.close(); + assertEquals(pos, cursor.getPosition()); // dumpCursor should not change status of cursor + assertEquals(expected, bos.toString()); + + StringBuilder sb = new StringBuilder(); + DatabaseUtils.dumpCursor(cursor, sb); + assertEquals(pos, cursor.getPosition()); // dumpCursor should not change status of cursor + assertEquals(expected, sb.toString()); + + assertEquals(expected, DatabaseUtils.dumpCursorToString(cursor)); + assertEquals(pos, cursor.getPosition()); // dumpCursor should not change status of cursor + } + + public void testCollationKey() { + String key1 = DatabaseUtils.getCollationKey("abc"); + String key2 = DatabaseUtils.getCollationKey("ABC"); + String key3 = DatabaseUtils.getCollationKey("bcd"); + + assertTrue(key1.equals(key2)); + assertFalse(key1.equals(key3)); + + key1 = DatabaseUtils.getHexCollationKey("abc"); + key2 = DatabaseUtils.getHexCollationKey("ABC"); + key3 = DatabaseUtils.getHexCollationKey("bcd"); + + assertTrue(key1.equals(key2)); + assertFalse(key1.equals(key3)); + } + + public void testLongForQuery() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + + String query = "SELECT age FROM " + TABLE_NAME; + assertEquals(20, DatabaseUtils.longForQuery(mDatabase, query, null)); + + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Jack', '35', 'London');"); + query = "SELECT age FROM " + TABLE_NAME + " WHERE name = ?"; + String[] args = new String[] { "Jack" }; + assertEquals(35, DatabaseUtils.longForQuery(mDatabase, query, args)); + args = new String[] { "No such name" }; + try { + DatabaseUtils.longForQuery(mDatabase, query, args); + fail("should throw SQLiteDoneException"); + } catch (SQLiteDoneException e) { + // expected + } + + query = "SELECT count(*) FROM " + TABLE_NAME + ";"; + SQLiteStatement statement = mDatabase.compileStatement(query); + assertEquals(2, DatabaseUtils.longForQuery(statement, null)); + + query = "SELECT age FROM " + TABLE_NAME + " WHERE address = ?;"; + statement = mDatabase.compileStatement(query); + args = new String[] { "London" }; + assertEquals(35, DatabaseUtils.longForQuery(statement, args)); + + args = new String[] { "No such address" }; + try { + DatabaseUtils.longForQuery(statement, args); + fail("should throw SQLiteDoneException"); + } catch (SQLiteDoneException e) { + // expected + } + statement.close(); + } + + public void testQueryNumEntries() { + assertEquals(0, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME)); + + mDatabase.execSQL( + "INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + assertEquals(1, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME)); + + mDatabase.execSQL( + "INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Susan', '20', 'AR');"); + assertEquals(2, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME)); + + mDatabase.execSQL( + "INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Christian', '25', 'AT');"); + assertEquals(3, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME)); + + assertEquals(2, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME, "AGE = 20")); + + assertEquals(1, DatabaseUtils.queryNumEntries(mDatabase, TABLE_NAME, "AGE = ?", + new String[] { "25" })); + + try { + DatabaseUtils.queryNumEntries(mDatabase, "NoSuchTable"); + fail("should throw SQLiteException."); + } catch (SQLiteException e) { + // expected + } + } + +// public void testExceptionFromParcel() { +// Parcel parcel = Parcel.obtain(); +// DatabaseUtils.writeExceptionToParcel(parcel, new IllegalArgumentException()); +// parcel.setDataPosition(0); +// try { +// DatabaseUtils.readExceptionFromParcel(parcel); +// fail("should throw IllegalArgumentException."); +// } catch (IllegalArgumentException e) { +// // expected +// } +// +// parcel = Parcel.obtain(); +// DatabaseUtils.writeExceptionToParcel(parcel, new SQLiteAbortException()); +// parcel.setDataPosition(0); +// try { +// DatabaseUtils.readExceptionFromParcel(parcel); +// fail("should throw SQLiteAbortException."); +// } catch (SQLiteAbortException e) { +// // expected +// } +// +// parcel = Parcel.obtain(); +// DatabaseUtils.writeExceptionToParcel(parcel, new FileNotFoundException()); +// parcel.setDataPosition(0); +// try { +// DatabaseUtils.readExceptionFromParcel(parcel); +// fail("should throw RuntimeException."); +// } catch (RuntimeException e) { +// // expected +// } +// +// parcel = Parcel.obtain(); +// DatabaseUtils.writeExceptionToParcel(parcel, new FileNotFoundException()); +// parcel.setDataPosition(0); +// try { +// DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(parcel); +// fail("should throw FileNotFoundException."); +// } catch (FileNotFoundException e) { +// // expected +// } +// } + + public void testStringForQuery() { + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Mike', '20', 'LA');"); + + String query = "SELECT name FROM " + TABLE_NAME; + assertEquals("Mike", DatabaseUtils.stringForQuery(mDatabase, query, null)); + + mDatabase.execSQL("INSERT INTO " + TABLE_NAME + " (name, age, address)" + + " VALUES ('Jack', '35', 'London');"); + query = "SELECT name FROM " + TABLE_NAME + " WHERE address = ?"; + String[] args = new String[] { "London" }; + assertEquals("Jack", DatabaseUtils.stringForQuery(mDatabase, query, args)); + args = new String[] { "No such address" }; + try { + DatabaseUtils.stringForQuery(mDatabase, query, args); + fail("should throw SQLiteDoneException"); + } catch (SQLiteDoneException e) { + // expected + } + + query = "SELECT name FROM " + TABLE_NAME + " WHERE age = ?;"; + SQLiteStatement statement = mDatabase.compileStatement(query); + args = new String[] { "20" }; + assertEquals("Mike", DatabaseUtils.stringForQuery(statement, args)); + + args = new String[] { "1000" }; // NO people can be older than this. + try { + DatabaseUtils.blobFileDescriptorForQuery(statement, args); + fail("should throw SQLiteDoneException"); + } catch (SQLiteDoneException e) { + // expected + } + statement.close(); + } + +// public void testBlobFileDescriptorForQuery() throws Exception { +// String data1 = "5300FEFF"; +// String data2 = "DECAFBAD"; +// mDatabase.execSQL("INSERT INTO blob_test (name, data) VALUES ('Mike', X'" + data1 + "')"); +// +// String query = "SELECT data FROM blob_test"; +// assertFileDescriptorContent(parseBlob(data1), +// DatabaseUtils.blobFileDescriptorForQuery(mDatabase, query, null)); +// +// mDatabase.execSQL("INSERT INTO blob_test (name, data) VALUES ('Jack', X'" + data2 + "');"); +// query = "SELECT data FROM blob_test WHERE name = ?"; +// String[] args = new String[] { "Jack" }; +// assertFileDescriptorContent(parseBlob(data2), +// DatabaseUtils.blobFileDescriptorForQuery(mDatabase, query, args)); +// +// args = new String[] { "No such name" }; +// try { +// DatabaseUtils.stringForQuery(mDatabase, query, args); +// fail("should throw SQLiteDoneException"); +// } catch (SQLiteDoneException e) { +// // expected +// } +// +// query = "SELECT data FROM blob_test WHERE name = ?;"; +// SQLiteStatement statement = mDatabase.compileStatement(query); +// args = new String[] { "Mike" }; +// assertFileDescriptorContent(parseBlob(data1), +// DatabaseUtils.blobFileDescriptorForQuery(statement, args)); +// +// args = new String[] { "No such name" }; +// try { +// DatabaseUtils.blobFileDescriptorForQuery(statement, args); +// fail("should throw SQLiteDoneException"); +// } catch (SQLiteDoneException e) { +// // expected +// } +// statement.close(); +// } + + private static byte[] parseBlob(String src) { + int len = src.length(); + byte[] result = new byte[len / 2]; + + for (int i = 0; i < len/2; i++) { + int val; + char c1 = src.charAt(i*2); + char c2 = src.charAt(i*2+1); + int val1 = Character.digit(c1, 16); + int val2 = Character.digit(c2, 16); + val = (val1 << 4) | val2; + result[i] = (byte)val; + } + return result; + } + + private static void assertFileDescriptorContent(byte[] expected, ParcelFileDescriptor fd) + throws IOException { + assertInputStreamContent(expected, new ParcelFileDescriptor.AutoCloseInputStream(fd)); + } + + private static void assertInputStreamContent(byte[] expected, InputStream is) + throws IOException { + try { + byte[] observed = new byte[expected.length]; + int count = is.read(observed); + assertEquals(expected.length, count); + assertEquals(-1, is.read()); + MoreAsserts.assertEquals(expected, observed); + } finally { + is.close(); + } + } + +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtils_InsertHelperTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtils_InsertHelperTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/DatabaseUtils_InsertHelperTest.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.DatabaseUtils.InsertHelper; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import java.io.File; + +public class DatabaseUtils_InsertHelperTest extends AndroidTestCase { + private static final String TEST_TABLE_NAME = "test"; + private static final String DATABASE_NAME = "database_test.db"; + + private SQLiteDatabase mDatabase; + private InsertHelper mInsertHelper; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + getContext().deleteDatabase(DATABASE_NAME); + File f = mContext.getDatabasePath(DATABASE_NAME); + mDatabase = SQLiteDatabase.openOrCreateDatabase(f, null); + assertNotNull(mDatabase); + mInsertHelper = new InsertHelper(mDatabase, TEST_TABLE_NAME); + } + + @Override + protected void tearDown() throws Exception { + mInsertHelper.close(); + mDatabase.close(); + getContext().deleteDatabase(DATABASE_NAME); + super.tearDown(); + } + + public void testConstructor() { + new InsertHelper(mDatabase, TEST_TABLE_NAME); + } + + public void testClose() { + mInsertHelper.close(); + } + + public void testGetColumnIndex() { + mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + " (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + assertEquals(1, mInsertHelper.getColumnIndex("_id")); + assertEquals(2, mInsertHelper.getColumnIndex("name")); + assertEquals(3, mInsertHelper.getColumnIndex("age")); + assertEquals(4, mInsertHelper.getColumnIndex("address")); + try { + mInsertHelper.getColumnIndex("missing_column"); + fail("Should throw exception (column does not exist)"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + public void testInsert() { + mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + + " boolean_value INTEGER, int_value INTEGER, long_value INTEGER," + + " double_value DOUBLE, float_value DOUBLE, string_value TEXT," + + " blob_value BLOB, null_value TEXT);"); + final int booleanValueIndex = 1; + final int intValueIndex = 2; + final int longValueIndex = 3; + final int doubleValueIndex = 4; + final int floatValueIndex = 5; + final int stringValueIndex = 6; + final int blobValueIndex = 7; + final int nullValueIndex = 8; + final String[] projection = new String[] { + "_id", // index 0 + "boolean_value", // index 1 + "int_value", // index 2 + "long_value", // index 3 + "double_value", // index 4 + "float_value", // index 5 + "string_value", // index 6 + "blob_value", // index 7 + "null_value" // index 8 + }; + + Cursor cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + try { + mInsertHelper.execute(); + fail("Should throw exception (execute without prepare)"); + } catch (IllegalStateException expected) { + // expected + } + + mInsertHelper.prepareForInsert(); + mInsertHelper.bind(mInsertHelper.getColumnIndex("boolean_value"), true); + mInsertHelper.bind(mInsertHelper.getColumnIndex("int_value"), 10); + mInsertHelper.bind(mInsertHelper.getColumnIndex("long_value"), 1000L); + mInsertHelper.bind(mInsertHelper.getColumnIndex("double_value"), 123.456); + mInsertHelper.bind(mInsertHelper.getColumnIndex("float_value"), 1.0f); + mInsertHelper.bind(mInsertHelper.getColumnIndex("string_value"), "test insert"); + byte[] blob = new byte[] { '1', '2', '3' }; + mInsertHelper.bind(mInsertHelper.getColumnIndex("blob_value"), blob); + mInsertHelper.bindNull(mInsertHelper.getColumnIndex("null_value")); + long id = mInsertHelper.execute(); + assertEquals(1, id); + + cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals(1, cursor.getInt(booleanValueIndex)); + assertEquals(10, cursor.getInt(intValueIndex)); + assertEquals(1000L, cursor.getLong(longValueIndex)); + assertEquals(123.456, cursor.getDouble(doubleValueIndex)); + assertEquals(1.0f, cursor.getFloat(floatValueIndex)); + assertEquals("test insert", cursor.getString(stringValueIndex)); + byte[] value = cursor.getBlob(blobValueIndex); + MoreAsserts.assertEquals(blob, value); + assertNull(cursor.getString(nullValueIndex)); + cursor.close(); + + // try inserting a conflicting column -> should return -1 + mInsertHelper.prepareForInsert(); + mInsertHelper.bind(mInsertHelper.getColumnIndex("_id"), id); + assertEquals(-1, mInsertHelper.execute()); + + // subsequent insert() should ignore existing bindings + ContentValues values = new ContentValues(); + values.put("boolean_value", false); + values.put("int_value", 123); + values.put("long_value", 987654L); + values.put("double_value", 654.321); + values.put("float_value", 21.1f); + values.put("string_value", "insert another row"); + values.put("blob_value", blob); + values.putNull("null_value"); + id = mInsertHelper.insert(values); + assertEquals(2, id); + cursor = mDatabase.query(TEST_TABLE_NAME, projection, "_id = " + id, + null, null, null, null); + assertNotNull(cursor); + cursor.moveToFirst(); + assertEquals(0, cursor.getInt(booleanValueIndex)); + assertEquals(123, cursor.getInt(intValueIndex)); + assertEquals(987654L, cursor.getLong(longValueIndex)); + assertEquals(654.321, cursor.getDouble(doubleValueIndex)); + assertEquals(21.1f, cursor.getFloat(floatValueIndex)); + assertEquals("insert another row", cursor.getString(stringValueIndex)); + value = cursor.getBlob(blobValueIndex); + MoreAsserts.assertEquals(blob, value); + assertNull(cursor.getString(nullValueIndex)); + cursor.close(); + + // try inserting a conflicting column -> should return -1 + values.put("_id", id); + assertEquals(-1, mInsertHelper.insert(values)); + } + + public void testReplace() { + mDatabase.execSQL("CREATE TABLE " + TEST_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + + " boolean_value INTEGER, int_value INTEGER, long_value INTEGER," + + " double_value DOUBLE, float_value DOUBLE, string_value TEXT," + + " blob_value BLOB, null_value TEXT);"); + final int booleanValueIndex = 1; + final int intValueIndex = 2; + final int longValueIndex = 3; + final int doubleValueIndex = 4; + final int floatValueIndex = 5; + final int stringValueIndex = 6; + final int blobValueIndex = 7; + final int nullValueIndex = 8; + final String[] projection = new String[] { + "_id", // index 0 + "boolean_value", // index 1 + "int_value", // index 2 + "long_value", // index 3 + "double_value", // index 4 + "float_value", // index 5 + "string_value", // index 6 + "blob_value", // index 7 + "null_value" // index 8 + }; + + Cursor cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + // without specifying a key, this becomes an insert + mInsertHelper.prepareForReplace(); + mInsertHelper.bind(mInsertHelper.getColumnIndex("boolean_value"), true); + mInsertHelper.bind(mInsertHelper.getColumnIndex("int_value"), 10); + mInsertHelper.bind(mInsertHelper.getColumnIndex("long_value"), 1000L); + mInsertHelper.bind(mInsertHelper.getColumnIndex("double_value"), 123.456); + mInsertHelper.bind(mInsertHelper.getColumnIndex("float_value"), 1.0f); + mInsertHelper.bind(mInsertHelper.getColumnIndex("string_value"), "test insert"); + byte[] blob = new byte[] { '1', '2', '3' }; + mInsertHelper.bind(mInsertHelper.getColumnIndex("blob_value"), blob); + mInsertHelper.bindNull(mInsertHelper.getColumnIndex("null_value")); + long id = mInsertHelper.execute(); + assertEquals(1, id); + + cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals(1, cursor.getInt(booleanValueIndex)); + assertEquals(10, cursor.getInt(intValueIndex)); + assertEquals(1000L, cursor.getLong(longValueIndex)); + assertEquals(123.456, cursor.getDouble(doubleValueIndex)); + assertEquals(1.0f, cursor.getFloat(floatValueIndex)); + assertEquals("test insert", cursor.getString(stringValueIndex)); + byte[] value = cursor.getBlob(blobValueIndex); + MoreAsserts.assertEquals(blob, value); + assertNull(cursor.getString(nullValueIndex)); + cursor.close(); + + mInsertHelper.prepareForReplace(); + mInsertHelper.bind(mInsertHelper.getColumnIndex("_id"), id); + mInsertHelper.bind(mInsertHelper.getColumnIndex("int_value"), 42); + mInsertHelper.execute(); + cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals(42, cursor.getInt(intValueIndex)); + // previous bindings are forgotten + assertNull(cursor.getString(stringValueIndex)); + cursor.close(); + + // illegal primary key -> should return -1 + mInsertHelper.prepareForReplace(); + mInsertHelper.bind(mInsertHelper.getColumnIndex("_id"), "illegal_id"); + assertEquals(-1, mInsertHelper.execute()); + + ContentValues values = new ContentValues(); + // will replace row id + values.put("_id", id); + values.put("boolean_value", false); + values.put("int_value", 123); + values.put("long_value", 987654L); + values.put("double_value", 654.321); + values.put("float_value", 21.1f); + values.put("string_value", "replace the row"); + values.put("blob_value", blob); + values.putNull("null_value"); + id = mInsertHelper.replace(values); + assertEquals(1, id); + cursor = mDatabase.query(TEST_TABLE_NAME, projection, null, null, null, null, null); + assertEquals(1, cursor.getCount()); + assertNotNull(cursor); + cursor.moveToFirst(); + assertEquals(0, cursor.getInt(booleanValueIndex)); + assertEquals(123, cursor.getInt(intValueIndex)); + assertEquals(987654L, cursor.getLong(longValueIndex)); + assertEquals(654.321, cursor.getDouble(doubleValueIndex)); + assertEquals(21.1f, cursor.getFloat(floatValueIndex)); + assertEquals("replace the row", cursor.getString(stringValueIndex)); + value = cursor.getBlob(blobValueIndex); + MoreAsserts.assertEquals(blob, value); + assertNull(cursor.getString(nullValueIndex)); + cursor.close(); + + // illegal primary key -> should return -1 + values.put("_id", "illegal_id"); + assertEquals(-1, mInsertHelper.replace(values)); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/MergeCursorTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/MergeCursorTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/MergeCursorTest.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.sqlite.database.database_cts; + + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.database.MergeCursor; +import android.database.StaleDataException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.test.AndroidTestCase; + +import java.io.File; +import java.util.Arrays; + +public class MergeCursorTest extends AndroidTestCase { + private final int NUMBER_1_COLUMN_INDEX = 1; + private static final String TABLE1_NAME = "test1"; + private static final String TABLE2_NAME = "test2"; + private static final String TABLE3_NAME = "test3"; + private static final String TABLE4_NAME = "test4"; + private static final String TABLE5_NAME = "test5"; + private static final String COLUMN_FOR_NULL_TEST = "Null Field"; + + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + + Cursor[] mCursors = null; + private static final String TABLE1_COLUMNS = " number_1 INTEGER"; + private static final String TABLE2_COLUMNS = " number_1 INTEGER, number_2 INTEGER"; + private static final String TABLE3_COLUMNS = " text_1 TEXT, number_3 INTEGER, number_4 REAL"; + private static final String TABLE2_COLUMN_NAMES = "_id,number_1,number_2"; + private static final String TABLE3_COLUMN_NAMES = "_id,text_1,number_3,number_4"; + private static final String TEXT_COLUMN_NAME = "text_1"; + private static final int TABLE2_COLUMN_COUNT = 3; + private static final int TABLE3_COLUMN_COUNT = 4; + private static final int DEFAULT_TABLE_VALUE_BEGINS = 1; + private static final int MAX_VALUE = 10; + private static final int HALF_VALUE = MAX_VALUE / 2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + setupDatabase(); + mCursors = new Cursor[2]; + } + + @Override + protected void tearDown() throws Exception { + for (int i = 0; i < mCursors.length; i++) { + if (null != mCursors[i]) { + mCursors[i].close(); + } + } + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + public void testConstructor() { + // If each item of mCursors are null, count will be zero. + MergeCursor mergeCursor = new MergeCursor(mCursors); + assertEquals(0, mergeCursor.getCount()); + + createCursors(); + + // if the items are not null, getCount() will return the sum of all cursors' count. + mergeCursor = new MergeCursor(mCursors); + assertEquals(mCursors[0].getCount() + mCursors[1].getCount(), mergeCursor.getCount()); + } + + public void testOnMove() { + createCursors(); + MergeCursor mergeCursor = new MergeCursor(mCursors); + for (int i = 0; i < MAX_VALUE; i++) { + mergeCursor.moveToNext(); + //From 1~5, mCursor should be in mCursors[0], larger than 5, it should be in + //mCursors[1]. + assertEquals(i + 1, mergeCursor.getInt(NUMBER_1_COLUMN_INDEX)); + } + } + + public void testCursorSwiching() { + mDatabase.execSQL("CREATE TABLE " + TABLE5_NAME + " (_id INTEGER PRIMARY KEY," + + TABLE3_COLUMNS + ");"); + String sql = "INSERT INTO " + TABLE5_NAME + " (" + TEXT_COLUMN_NAME + ") VALUES ('TEXT')"; + mDatabase.execSQL(sql); + + Cursor[] cursors = new Cursor[2]; + cursors[0] = mDatabase.query(TABLE5_NAME, null, null, null, null, null, null); + assertEquals(1, cursors[0].getCount()); + createCursors(); + cursors[1] = mCursors[1]; + assertTrue(cursors[1].getCount() > 0); + MergeCursor mergeCursor = new MergeCursor(cursors); + // MergeCursor should points to cursors[0] after moveToFirst. + mergeCursor.moveToFirst(); + + String[] tableColumns = TABLE3_COLUMN_NAMES.split("[,]"); + assertEquals(TABLE3_COLUMN_COUNT, mergeCursor.getColumnCount()); + assertTrue(Arrays.equals(tableColumns, mergeCursor.getColumnNames())); + + // MergeCursor should points to cursors[1] moveToNext. + mergeCursor.moveToNext(); + tableColumns = TABLE2_COLUMN_NAMES.split("[,]"); + assertEquals(TABLE2_COLUMN_COUNT, mergeCursor.getColumnCount()); + assertTrue(Arrays.equals(tableColumns, mergeCursor.getColumnNames())); + } + + public void testGetValues() { + byte NUMBER_BLOB_UNIT = 99; + String[] TEST_STRING = new String[] {"Test String1", "Test String2"}; + String[] tableNames = new String[] {TABLE3_NAME, TABLE4_NAME}; + + final double NUMBER_DOUBLE = Double.MAX_VALUE; + final double NUMBER_FLOAT = (float) NUMBER_DOUBLE; + final long NUMBER_LONG_INTEGER = (long) 0xaabbccddffL; + final long NUMBER_INTEGER = (int) NUMBER_LONG_INTEGER; + final long NUMBER_SHORT = (short) NUMBER_INTEGER; + + // create tables + byte[][] originalBlobs = new byte[2][]; + for (int i = 0; i < 2; i++) { + // insert blob and other values + originalBlobs[i] = new byte[1000]; + Arrays.fill(originalBlobs[i], (byte) (NUMBER_BLOB_UNIT - i)); + buildDatabaseWithTestValues(TEST_STRING[i], NUMBER_DOUBLE - i, NUMBER_LONG_INTEGER - i, + originalBlobs[i], tableNames[i]); + // Get cursors. + mCursors[i] = mDatabase.query(tableNames[i], null, null, null, null, null, null); + } + + MergeCursor mergeCursor = new MergeCursor(mCursors); + assertEquals(4, mergeCursor.getCount()); + String[] testColumns = new String[] {"_id", "string_text", "double_number", "int_number", + "blob_data"}; + // Test getColumnNames(). + assertTrue(Arrays.equals(testColumns, mergeCursor.getColumnNames())); + + int columnBlob = mCursors[0].getColumnIndexOrThrow("blob_data"); + int columnString = mCursors[0].getColumnIndexOrThrow("string_text"); + int columnDouble = mCursors[0].getColumnIndexOrThrow("double_number"); + int columnInteger = mCursors[0].getColumnIndexOrThrow("int_number"); + + // Test values. + for (int i = 0; i < 2; i++) { + mergeCursor.moveToNext(); + assertEquals(5, mergeCursor.getColumnCount()); + + // Test getting value methods. + byte[] targetBlob = mergeCursor.getBlob(columnBlob); + assertTrue(Arrays.equals(originalBlobs[i], targetBlob)); + + assertEquals(TEST_STRING[i], mergeCursor.getString(columnString)); + assertEquals(NUMBER_DOUBLE - i, mergeCursor.getDouble(columnDouble), 0.000000000001); + assertEquals(NUMBER_FLOAT - i, mergeCursor.getFloat(columnDouble), 0.000000000001f); + assertEquals(NUMBER_LONG_INTEGER - i, mergeCursor.getLong(columnInteger)); + assertEquals(NUMBER_INTEGER - i, mergeCursor.getInt(columnInteger)); + assertEquals(NUMBER_SHORT - i, mergeCursor.getShort(columnInteger)); + + // Test isNull(int). + assertFalse(mergeCursor.isNull(columnBlob)); + mergeCursor.moveToNext(); + assertEquals(COLUMN_FOR_NULL_TEST, mergeCursor.getString(columnString)); + assertTrue(mergeCursor.isNull(columnBlob)); + } + } + + public void testContentObsererOperations() throws IllegalStateException { + createCursors(); + MergeCursor mergeCursor = new MergeCursor(mCursors); + ContentObserver observer = new ContentObserver(null) {}; + + // Can't unregister a Observer before it has been registered. + try { + mergeCursor.unregisterContentObserver(observer); + fail("testUnregisterContentObserver failed"); + } catch (IllegalStateException e) { + // expected + } + + mergeCursor.registerContentObserver(observer); + + // Can't register a same observer twice before unregister it. + try { + mergeCursor.registerContentObserver(observer); + fail("testRegisterContentObserver failed"); + } catch (IllegalStateException e) { + // expected + } + + mergeCursor.unregisterContentObserver(observer); + // one Observer can be registered again after it has been unregistered. + mergeCursor.registerContentObserver(observer); + + mergeCursor.unregisterContentObserver(observer); + + try { + mergeCursor.unregisterContentObserver(observer); + fail("testUnregisterContentObserver failed"); + } catch (IllegalStateException e) { + // expected + } + } + + public void testDeactivate() throws IllegalStateException { + createCursors(); + MergeCursor mergeCursor = new MergeCursor(mCursors); + MockObserver observer = new MockObserver(); + + // one DataSetObserver can't unregistered before it had been registered. + try { + mergeCursor.unregisterDataSetObserver(observer); + fail("testUnregisterDataSetObserver failed"); + } catch (IllegalStateException e) { + // expected + } + + // Before registering, observer can't be notified. + assertFalse(observer.hasInvalidated()); + mergeCursor.moveToLast(); + mergeCursor.deactivate(); + assertFalse(observer.hasInvalidated()); + + // Test with registering DataSetObserver + assertTrue(mergeCursor.requery()); + mergeCursor.registerDataSetObserver(observer); + assertFalse(observer.hasInvalidated()); + mergeCursor.moveToLast(); + assertEquals(MAX_VALUE, mergeCursor.getInt(NUMBER_1_COLUMN_INDEX)); + mergeCursor.deactivate(); + // deactivate method can invoke invalidate() method, can be observed by DataSetObserver. + assertTrue(observer.hasInvalidated()); + // After deactivating, the cursor can not provide values from database record. + try { + mergeCursor.getInt(NUMBER_1_COLUMN_INDEX); + fail("After deactivating, cursor cannot execute getting value operations."); + } catch (StaleDataException e) { + // expected + } + + // Can't register a same observer twice before unregister it. + try { + mergeCursor.registerDataSetObserver(observer); + fail("testRegisterDataSetObserver failed"); + } catch (IllegalStateException e) { + // expected + } + + // After runegistering, observer can't be notified. + mergeCursor.unregisterDataSetObserver(observer); + observer.resetStatus(); + assertFalse(observer.hasInvalidated()); + mergeCursor.moveToLast(); + mergeCursor.deactivate(); + assertFalse(observer.hasInvalidated()); + + // one DataSetObserver can't be unregistered twice continuously. + try { + mergeCursor.unregisterDataSetObserver(observer); + fail("testUnregisterDataSetObserver failed"); + } catch (IllegalStateException e) { + // expected + } + } + + public void testRequery() { + final String TEST_VALUE1 = Integer.toString(MAX_VALUE + 1); + final String TEST_VALUE2 = Integer.toString(MAX_VALUE + 2); + createCursors(); + MergeCursor mergeCursor = new MergeCursor(mCursors); + int cursor1Count = mCursors[0].getCount(); + int cursor2Count = mCursors[0].getCount(); + + mDatabase.execSQL("INSERT INTO " + TABLE1_NAME + " (number_1) VALUES ('" + TEST_VALUE1 + + "');"); + assertEquals(cursor1Count + cursor2Count, mergeCursor.getCount()); + assertTrue(mergeCursor.requery()); + cursor1Count += 1; + assertEquals(cursor1Count + cursor2Count, mergeCursor.getCount()); + mDatabase.execSQL("INSERT INTO " + TABLE2_NAME + " (number_1) VALUES ('" + TEST_VALUE2 + + "');"); + cursor2Count += 1; + assertTrue(mergeCursor.requery()); + assertEquals(cursor1Count + cursor2Count, mergeCursor.getCount()); + + mergeCursor.close(); + assertFalse(mergeCursor.requery()); + } + + private void buildDatabaseWithTestValues(String text, double doubleNumber, long intNumber, + byte[] blob, String tablename) { + Object[] args = new Object[4]; + args[0] = text; + args[1] = doubleNumber; + args[2] = intNumber; + args[3] = blob; + mDatabase.execSQL("CREATE TABLE " + tablename + " (_id INTEGER PRIMARY KEY," + + "string_text TEXT, double_number REAL, int_number INTEGER, blob_data BLOB);"); + + // Insert record in giving table. + String sql = "INSERT INTO " + tablename + " (string_text, double_number, int_number," + + " blob_data) VALUES (?,?,?,?)"; + mDatabase.execSQL(sql, args); + // insert null blob. + sql = "INSERT INTO " + tablename + " (string_text) VALUES ('" + COLUMN_FOR_NULL_TEST + "')"; + mDatabase.execSQL(sql); + } + + private void setupDatabase() { + File dbDir = getContext().getDir("tests", Context.MODE_PRIVATE); + mDatabaseFile = new File(dbDir, "database_test.db"); + if (mDatabaseFile.exists()) { + mDatabaseFile.delete(); + } + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); + assertNotNull(mDatabaseFile); + createTable(TABLE1_NAME, TABLE1_COLUMNS); + createTable(TABLE2_NAME, TABLE2_COLUMNS); + addValuesIntoTable(TABLE1_NAME, DEFAULT_TABLE_VALUE_BEGINS, HALF_VALUE); + addValuesIntoTable(TABLE2_NAME, HALF_VALUE + 1, MAX_VALUE); + } + + private void createTable(String tableName, String columnNames) { + String sql = "Create TABLE " + tableName + " (_id INTEGER PRIMARY KEY, " + columnNames + + " );"; + mDatabase.execSQL(sql); + } + + private void addValuesIntoTable(String tableName, int start, int end) { + for (int i = start; i <= end; i++) { + mDatabase.execSQL("INSERT INTO " + tableName + "(number_1) VALUES ('" + + i + "');"); + } + } + + private Cursor getCursor(String tableName, String selection, String[] columnNames) { + return mDatabase.query(tableName, columnNames, selection, null, null, null, "number_1"); + } + + private void createCursors() { + mCursors[0] = getCursor(TABLE1_NAME, null, null); + mCursors[1] = getCursor(TABLE2_NAME, null, null); + } + + private class MockObserver extends DataSetObserver { + private boolean mHasChanged = false; + private boolean mHasInvalidated = false; + + @Override + public void onChanged() { + super.onChanged(); + mHasChanged = true; + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + mHasInvalidated = true; + } + + public void resetStatus() { + mHasChanged = false; + mHasInvalidated = false; + } + + public boolean hasChanged() { + return mHasChanged; + } + + public boolean hasInvalidated () { + return mHasInvalidated; + } + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/database_cts/README Index: sqlite3/src/androidTest/java/org/sqlite/database/database_cts/README ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/database_cts/README @@ -0,0 +1,22 @@ + +The tests in this directory are modified versions of the SQLite tests in the +Android CTS (Compatibility Test Suite) project. This project is stored in +a git repository here: + + https://android.googlesource.com/platform/cts + +The snapshot was taken from master branch commit id: + + 0794ecacb76b54eeee881762d0edfc83209ab05f + + https://android.googlesource.com/platform/cts/+/0794ecacb76b54eeee881762d0edfc83209ab05f + +Within that project, the tests are in directory: + + ./tests/tests/database/src/android/database/cts/ + +So: + + https://android.googlesource.com/platform/cts/+/0794ecacb76b54eeee881762d0edfc83209ab05f/tests/tests/database/src/android/database/cts + +Only those tests that use SQLite objects have been copied over. ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/DatabaseStatementTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/DatabaseStatementTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/DatabaseStatementTest.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.sqlite.SQLiteConstraintException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDoneException; +import org.sqlite.database.sqlite.SQLiteStatement; +import android.test.AndroidTestCase; +import android.test.PerformanceTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +import java.io.File; + +/* + * These tests were taken from + * frameworks/base/tests/AndroidTests/src/com/android/unit_tests/DatabaseStatementTest.java + * Modifications: + * - use Context to create and delete the DB to avoid hard-coded paths + */ +public class DatabaseStatementTest extends AndroidTestCase implements PerformanceTestCase { + + private static final String sString1 = "this is a test"; + private static final String sString2 = "and yet another test"; + private static final String sString3 = "this string is a little longer, but still a test"; + + private static final String DATABASE_NAME = "database_test.db"; + + private static final int CURRENT_DATABASE_VERSION = 42; + private SQLiteDatabase mDatabase; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File f = mContext.getDatabasePath(DATABASE_NAME); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + assertNotNull(mDatabase); + mDatabase.setVersion(CURRENT_DATABASE_VERSION); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + getContext().deleteDatabase(DATABASE_NAME); + super.tearDown(); + } + + public boolean isPerformanceOnly() { + return false; + } + + // These test can only be run once. + public int startPerformance(Intermediates intermediates) { + return 1; + } + + private void populateDefaultTable() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString1 + "');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString2 + "');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('" + sString3 + "');"); + } + + @MediumTest + public void testExecuteStatement() throws Exception { + populateDefaultTable(); + SQLiteStatement statement = mDatabase.compileStatement("DELETE FROM test"); + statement.execute(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + assertEquals(0, c.getCount()); + c.deactivate(); + statement.close(); + } + + @MediumTest + public void testSimpleQuery() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); + mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); + SQLiteStatement statement1 = + mDatabase.compileStatement("SELECT num FROM test WHERE str = ?"); + SQLiteStatement statement2 = + mDatabase.compileStatement("SELECT str FROM test WHERE num = ?"); + + try { + statement1.bindString(1, "hello"); + long value = statement1.simpleQueryForLong(); + assertEquals(1234, value); + + statement1.bindString(1, "world"); + statement1.simpleQueryForLong(); + fail("shouldn't get here"); + } catch (SQLiteDoneException e) { + // expected + } + + try { + statement2.bindLong(1, 1234); + String value = statement1.simpleQueryForString(); + assertEquals("hello", value); + + statement2.bindLong(1, 5678); + statement1.simpleQueryForString(); + fail("shouldn't get here"); + } catch (SQLiteDoneException e) { + // expected + } + + statement1.close(); + statement2.close(); + } + + @MediumTest + public void testStatementLongBinding() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); + + for (int i = 0; i < 10; i++) { + statement.bindLong(1, i); + statement.execute(); + } + statement.close(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + int numCol = c.getColumnIndexOrThrow("num"); + c.moveToFirst(); + for (long i = 0; i < 10; i++) { + long num = c.getLong(numCol); + assertEquals(i, num); + c.moveToNext(); + } + c.close(); + } + + @MediumTest + public void testStatementStringBinding() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num TEXT);"); + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); + + for (long i = 0; i < 10; i++) { + statement.bindString(1, Long.toHexString(i)); + statement.execute(); + } + statement.close(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + int numCol = c.getColumnIndexOrThrow("num"); + c.moveToFirst(); + for (long i = 0; i < 10; i++) { + String num = c.getString(numCol); + assertEquals(Long.toHexString(i), num); + c.moveToNext(); + } + c.close(); + } + + @MediumTest + public void testStatementClearBindings() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); + + for (long i = 0; i < 10; i++) { + statement.bindLong(1, i); + statement.clearBindings(); + statement.execute(); + } + statement.close(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, "ROWID"); + int numCol = c.getColumnIndexOrThrow("num"); + assertTrue(c.moveToFirst()); + for (long i = 0; i < 10; i++) { + assertTrue(c.isNull(numCol)); + c.moveToNext(); + } + c.close(); + } + + @MediumTest + public void testSimpleStringBinding() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num TEXT, value TEXT);"); + String statement = "INSERT INTO test (num, value) VALUES (?,?)"; + + String[] args = new String[2]; + for (int i = 0; i < 2; i++) { + args[i] = Integer.toHexString(i); + } + + mDatabase.execSQL(statement, args); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + int numCol = c.getColumnIndexOrThrow("num"); + int valCol = c.getColumnIndexOrThrow("value"); + c.moveToFirst(); + String num = c.getString(numCol); + assertEquals(Integer.toHexString(0), num); + + String val = c.getString(valCol); + assertEquals(Integer.toHexString(1), val); + c.close(); + } + + @MediumTest + public void testStatementMultipleBindings() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER, str TEXT);"); + SQLiteStatement statement = + mDatabase.compileStatement("INSERT INTO test (num, str) VALUES (?, ?)"); + + for (long i = 0; i < 10; i++) { + statement.bindLong(1, i); + statement.bindString(2, Long.toHexString(i)); + statement.execute(); + } + statement.close(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, "ROWID"); + int numCol = c.getColumnIndexOrThrow("num"); + int strCol = c.getColumnIndexOrThrow("str"); + assertTrue(c.moveToFirst()); + for (long i = 0; i < 10; i++) { + long num = c.getLong(numCol); + String str = c.getString(strCol); + assertEquals(i, num); + assertEquals(Long.toHexString(i), str); + c.moveToNext(); + } + c.close(); + } + + private static class StatementTestThread extends Thread { + private SQLiteDatabase mDatabase; + private SQLiteStatement mStatement; + + public StatementTestThread(SQLiteDatabase db, SQLiteStatement statement) { + super(); + mDatabase = db; + mStatement = statement; + } + + @Override + public void run() { + mDatabase.beginTransaction(); + for (long i = 0; i < 10; i++) { + mStatement.bindLong(1, i); + mStatement.bindString(2, Long.toHexString(i)); + mStatement.execute(); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, "ROWID"); + int numCol = c.getColumnIndexOrThrow("num"); + int strCol = c.getColumnIndexOrThrow("str"); + assertTrue(c.moveToFirst()); + for (long i = 0; i < 10; i++) { + long num = c.getLong(numCol); + String str = c.getString(strCol); + assertEquals(i, num); + assertEquals(Long.toHexString(i), str); + c.moveToNext(); + } + c.close(); + } + } + + @MediumTest + public void testStatementMultiThreaded() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER, str TEXT);"); + SQLiteStatement statement = + mDatabase.compileStatement("INSERT INTO test (num, str) VALUES (?, ?)"); + + StatementTestThread thread = new StatementTestThread(mDatabase, statement); + thread.start(); + try { + thread.join(); + } finally { + statement.close(); + } + } + + @MediumTest + public void testStatementConstraint() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL);"); + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test (num) VALUES (?)"); + + // Try to insert NULL, which violates the constraint + try { + statement.clearBindings(); + statement.execute(); + fail("expected exception not thrown"); + } catch (SQLiteConstraintException e) { + // expected + } + + // Make sure the statement can still be used + statement.bindLong(1, 1); + statement.execute(); + statement.close(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + int numCol = c.getColumnIndexOrThrow("num"); + c.moveToFirst(); + long num = c.getLong(numCol); + assertEquals(1, num); + c.close(); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/README Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/README ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/README @@ -0,0 +1,22 @@ + +The tests in this directory are modified versions of the SQLite tests in the +Android CTS (Compatibility Test Suite) project. This project is stored in +a git repository here: + + https://android.googlesource.com/platform/cts + +The snapshot was taken from master branch commit id: + + 0794ecacb76b54eeee881762d0edfc83209ab05f + + https://android.googlesource.com/platform/cts/+/0794ecacb76b54eeee881762d0edfc83209ab05f + +Within that project, the tests are in directory: + + ./tests/tests/database/src/android/database/sqlite/cts/ + +So: + + https://android.googlesource.com/platform/cts/+/0794ecacb76b54eeee881762d0edfc83209ab05f/tests/tests/database/src/android/database/sqlite/cts + + ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteAbortExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteAbortExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteAbortExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteAbortException; +import android.test.AndroidTestCase; + +public class SQLiteAbortExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteAbortException(); + + new SQLiteAbortException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteClosableTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteClosableTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteClosableTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteClosable; +import android.test.AndroidTestCase; + +public class SQLiteClosableTest extends AndroidTestCase { + private class MockSQLiteClosable extends SQLiteClosable { + private boolean mOnAllReferencesReleasedCalled = false; + private boolean mOnAllReferencesReleasedFromContainerCalled = false; + + @Override + protected void onAllReferencesReleased() { + mOnAllReferencesReleasedCalled = true; + } + + protected void onAllReferencesReleasedFromContainer() { + mOnAllReferencesReleasedFromContainerCalled = true; + } + + public boolean isOnAllReferencesReleasedCalled() { + return mOnAllReferencesReleasedCalled; + } + + public boolean isOnAllReferencesReleasedFromContainerCalled() { + return mOnAllReferencesReleasedFromContainerCalled; + } + } + + public void testAcquireReference() { + MockSQLiteClosable closable = new MockSQLiteClosable(); + + closable.acquireReference(); + closable.releaseReference(); + + assertFalse(closable.isOnAllReferencesReleasedCalled()); + closable.releaseReference(); + // the reference count is 0 now. + assertTrue(closable.isOnAllReferencesReleasedCalled()); + + try { + closable.acquireReference(); + fail("should throw IllegalStateException."); + } catch (IllegalStateException e) { + } + } + + public void testReleaseReferenceFromContainer() { + MockSQLiteClosable closable = new MockSQLiteClosable(); + + closable.acquireReference(); + closable.releaseReferenceFromContainer(); + + // the reference count is 1 now. + assertFalse(closable.isOnAllReferencesReleasedFromContainerCalled()); + closable.releaseReferenceFromContainer(); + // the reference count is 0 now. + assertTrue(closable.isOnAllReferencesReleasedFromContainerCalled()); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteConstraintExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteConstraintExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteConstraintExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteConstraintException; +import android.test.AndroidTestCase; + +public class SQLiteConstraintExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteConstraintException(); + + new SQLiteConstraintException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteCursorTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteCursorTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteCursorTest.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + + +import android.content.Context; +import android.database.AbstractCursor; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.DataSetObserver; +import android.database.StaleDataException; +import org.sqlite.database.sqlite.SQLiteCursor; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDirectCursorDriver; +import android.test.AndroidTestCase; + +import java.io.File; +import java.util.Arrays; + +/** + * Test {@link AbstractCursor}. + */ +public class SQLiteCursorTest extends AndroidTestCase { + private SQLiteDatabase mDatabase; + private static final String[] COLUMNS = new String[] { "_id", "number_1", "number_2" }; + private static final String TABLE_NAME = "test"; + private static final String TABLE_COLUMNS = " number_1 INTEGER, number_2 INTEGER"; + private static final int DEFAULT_TABLE_VALUE_BEGINS = 1; + private static final int TEST_COUNT = 10; + private static final String TEST_SQL = "SELECT * FROM test ORDER BY number_1"; + private static final String DATABASE_FILE = "database_test.db"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File f = mContext.getDatabasePath(DATABASE_FILE); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f, null); + createTable(TABLE_NAME, TABLE_COLUMNS); + addValuesIntoTable(TABLE_NAME, DEFAULT_TABLE_VALUE_BEGINS, TEST_COUNT); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + getContext().deleteDatabase(DATABASE_FILE); + super.tearDown(); + } + + public void testConstructor() { + SQLiteDirectCursorDriver cursorDriver = new SQLiteDirectCursorDriver(mDatabase, + TEST_SQL, TABLE_NAME, null); + try { + new SQLiteCursor(mDatabase, cursorDriver, TABLE_NAME, null); + fail("constructor didn't throw IllegalArgumentException when SQLiteQuery is null"); + } catch (IllegalArgumentException e) { + } + + // get SQLiteCursor by querying database + SQLiteCursor cursor = getCursor(); + assertNotNull(cursor); + } + + public void testClose() { + SQLiteCursor cursor = getCursor(); + assertTrue(cursor.moveToFirst()); + assertFalse(cursor.isClosed()); + assertTrue(cursor.requery()); + cursor.close(); + assertFalse(cursor.requery()); + try { + cursor.moveToFirst(); + fail("moveToFirst didn't throw IllegalStateException after closed."); + } catch (IllegalStateException e) { + } + assertTrue(cursor.isClosed()); + } + + public void testRegisterDataSetObserver() { + SQLiteCursor cursor = getCursor(); + MockCursorWindow cursorWindow = new MockCursorWindow(false); + + MockObserver observer = new MockObserver(); + + cursor.setWindow(cursorWindow); + // Before registering, observer can't be notified. + assertFalse(observer.hasInvalidated()); + cursor.moveToLast(); + assertFalse(cursorWindow.isClosed()); + cursor.deactivate(); + assertFalse(observer.hasInvalidated()); + // deactivate() will close the CursorWindow + assertTrue(cursorWindow.isClosed()); + + // test registering DataSetObserver + assertTrue(cursor.requery()); + cursor.registerDataSetObserver(observer); + assertFalse(observer.hasInvalidated()); + cursor.moveToLast(); + assertEquals(TEST_COUNT, cursor.getInt(1)); + cursor.deactivate(); + // deactivate method can invoke invalidate() method, can be observed by DataSetObserver. + assertTrue(observer.hasInvalidated()); + + try { + cursor.getInt(1); + fail("After deactivating, cursor cannot execute getting value operations."); + } catch (StaleDataException e) { + } + + assertTrue(cursor.requery()); + cursor.moveToLast(); + assertEquals(TEST_COUNT, cursor.getInt(1)); + + // can't register a same observer twice. + try { + cursor.registerDataSetObserver(observer); + fail("didn't throw IllegalStateException when register existed observer"); + } catch (IllegalStateException e) { + } + + // after unregistering, observer can't be notified. + cursor.unregisterDataSetObserver(observer); + observer.resetStatus(); + assertFalse(observer.hasInvalidated()); + cursor.deactivate(); + assertFalse(observer.hasInvalidated()); + } + + public void testRequery() { + final String DELETE = "DELETE FROM " + TABLE_NAME + " WHERE number_1 ="; + final String DELETE_1 = DELETE + "1;"; + final String DELETE_2 = DELETE + "2;"; + + mDatabase.execSQL(DELETE_1); + // when cursor is created, it refreshes CursorWindow and populates cursor count + SQLiteCursor cursor = getCursor(); + MockObserver observer = new MockObserver(); + cursor.registerDataSetObserver(observer); + assertEquals(TEST_COUNT - 1, cursor.getCount()); + assertFalse(observer.hasChanged()); + + mDatabase.execSQL(DELETE_2); + // when getCount() has invoked once, it can no longer refresh CursorWindow. + assertEquals(TEST_COUNT - 1, cursor.getCount()); + + assertTrue(cursor.requery()); + // only after requery, getCount can get most up-to-date counting info now. + assertEquals(TEST_COUNT - 2, cursor.getCount()); + assertTrue(observer.hasChanged()); + } + + public void testRequery2() { + mDatabase.disableWriteAheadLogging(); + mDatabase.execSQL("create table testRequery2 (i int);"); + mDatabase.execSQL("insert into testRequery2 values(1);"); + mDatabase.execSQL("insert into testRequery2 values(2);"); + Cursor c = mDatabase.rawQuery("select * from testRequery2 order by i", null); + assertEquals(2, c.getCount()); + assertTrue(c.moveToFirst()); + assertEquals(1, c.getInt(0)); + assertTrue(c.moveToNext()); + assertEquals(2, c.getInt(0)); + // add more data to the table and requery + mDatabase.execSQL("insert into testRequery2 values(3);"); + assertTrue(c.requery()); + assertEquals(3, c.getCount()); + assertTrue(c.moveToFirst()); + assertEquals(1, c.getInt(0)); + assertTrue(c.moveToNext()); + assertEquals(2, c.getInt(0)); + assertTrue(c.moveToNext()); + assertEquals(3, c.getInt(0)); + // close the database and see if requery throws an exception + mDatabase.close(); + assertFalse(c.requery()); + } + + public void testGetColumnIndex() { + SQLiteCursor cursor = getCursor(); + + for (int i = 0; i < COLUMNS.length; i++) { + assertEquals(i, cursor.getColumnIndex(COLUMNS[i])); + } + + assertTrue(Arrays.equals(COLUMNS, cursor.getColumnNames())); + } + + public void testSetSelectionArguments() { + final String SELECTION = "_id > ?"; + int TEST_ARG1 = 2; + int TEST_ARG2 = 5; + SQLiteCursor cursor = (SQLiteCursor) mDatabase.query(TABLE_NAME, null, SELECTION, + new String[] { Integer.toString(TEST_ARG1) }, null, null, null); + assertEquals(TEST_COUNT - TEST_ARG1, cursor.getCount()); + cursor.setSelectionArguments(new String[] { Integer.toString(TEST_ARG2) }); + cursor.requery(); + assertEquals(TEST_COUNT - TEST_ARG2, cursor.getCount()); + } + + public void testOnMove() { + // Do not test this API. It is callback which: + // 1. The callback mechanism has been tested in super class + // 2. The functionality is implementation details, no need to test + } + + private void createTable(String tableName, String columnNames) { + String sql = "Create TABLE " + tableName + " (_id INTEGER PRIMARY KEY, " + + columnNames + " );"; + mDatabase.execSQL(sql); + } + + private void addValuesIntoTable(String tableName, int start, int end) { + for (int i = start; i <= end; i++) { + mDatabase.execSQL("INSERT INTO " + tableName + "(number_1) VALUES ('" + i + "');"); + } + } + + private SQLiteCursor getCursor() { + SQLiteCursor cursor = (SQLiteCursor) mDatabase.query(TABLE_NAME, null, null, + null, null, null, null); + return cursor; + } + + private class MockObserver extends DataSetObserver { + private boolean mHasChanged = false; + private boolean mHasInvalidated = false; + + @Override + public void onChanged() { + super.onChanged(); + mHasChanged = true; + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + mHasInvalidated = true; + } + + protected void resetStatus() { + mHasChanged = false; + mHasInvalidated = false; + } + + protected boolean hasChanged() { + return mHasChanged; + } + + protected boolean hasInvalidated () { + return mHasInvalidated; + } + } + + private class MockCursorWindow extends CursorWindow { + private boolean mIsClosed = false; + + public MockCursorWindow(boolean localWindow) { + super(localWindow); + } + + @Override + public void close() { + super.close(); + mIsClosed = true; + } + + public boolean isClosed() { + return mIsClosed; + } + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseCorruptExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseCorruptExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseCorruptExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.test.AndroidTestCase; + +public class SQLiteDatabaseCorruptExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteDatabaseCorruptException(); + + new SQLiteDatabaseCorruptException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDatabaseTest.java @@ -0,0 +1,1498 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.Semaphore; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.DatabaseUtils; +import org.sqlite.database.SQLException; +import org.sqlite.database.sqlite.SQLiteCursor; +import org.sqlite.database.sqlite.SQLiteCursorDriver; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDatabase.CursorFactory; +import org.sqlite.database.sqlite.SQLiteException; +import org.sqlite.database.sqlite.SQLiteQuery; +import org.sqlite.database.sqlite.SQLiteStatement; +import org.sqlite.database.sqlite.SQLiteTransactionListener; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; + +public class SQLiteDatabaseTest extends AndroidTestCase { + private SQLiteDatabase mDatabase; + private File mDatabaseFile; + private String mDatabaseFilePath; + private String mDatabaseDir; + + private boolean mTransactionListenerOnBeginCalled; + private boolean mTransactionListenerOnCommitCalled; + private boolean mTransactionListenerOnRollbackCalled; + + private static final String DATABASE_FILE_NAME = "database_test.db"; + private static final String TABLE_NAME = "test"; + private static final int COLUMN_ID_INDEX = 0; + private static final int COLUMN_NAME_INDEX = 1; + private static final int COLUMN_AGE_INDEX = 2; + private static final int COLUMN_ADDR_INDEX = 3; + private static final String[] TEST_PROJECTION = new String[] { + "_id", // 0 + "name", // 1 + "age", // 2 + "address" // 3 + }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + System.loadLibrary("sqliteX"); + getContext().deleteDatabase(DATABASE_FILE_NAME); + mDatabaseFilePath = getContext().getDatabasePath(DATABASE_FILE_NAME).getPath(); + mDatabaseFile = getContext().getDatabasePath(DATABASE_FILE_NAME); + mDatabaseDir = mDatabaseFile.getParent(); + mDatabaseFile.getParentFile().mkdirs(); // directory may not exist + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile, null); + assertNotNull(mDatabase); + + mTransactionListenerOnBeginCalled = false; + mTransactionListenerOnCommitCalled = false; + mTransactionListenerOnRollbackCalled = false; + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + mDatabaseFile.delete(); + super.tearDown(); + } + + public void testOpenDatabase() { + CursorFactory factory = new CursorFactory() { + public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, + String editTable, SQLiteQuery query) { + return new MockSQLiteCursor(db, masterQuery, editTable, query); + } + }; + + SQLiteDatabase db = SQLiteDatabase.openDatabase(mDatabaseFilePath, + factory, SQLiteDatabase.CREATE_IF_NECESSARY); + assertNotNull(db); + db.close(); + + File dbFile = new File(mDatabaseDir, "database_test12345678.db"); + dbFile.delete(); + assertFalse(dbFile.exists()); + db = SQLiteDatabase.openOrCreateDatabase(dbFile.getPath(), factory); + assertNotNull(db); + db.close(); + dbFile.delete(); + + dbFile = new File(mDatabaseDir, DATABASE_FILE_NAME); + db = SQLiteDatabase.openOrCreateDatabase(dbFile, factory); + assertNotNull(db); + db.close(); + dbFile.delete(); + + db = SQLiteDatabase.create(factory); + assertNotNull(db); + db.close(); + } + + public void testDeleteDatabase() throws IOException { + File dbFile = new File(mDatabaseDir, "database_test12345678.db"); + File journalFile = new File(dbFile.getPath() + "-journal"); + File shmFile = new File(dbFile.getPath() + "-shm"); + File walFile = new File(dbFile.getPath() + "-wal"); + File mjFile1 = new File(dbFile.getPath() + "-mj00000000"); + File mjFile2 = new File(dbFile.getPath() + "-mj00000001"); + File innocentFile = new File(dbFile.getPath() + "-innocent"); + + dbFile.createNewFile(); + journalFile.createNewFile(); + shmFile.createNewFile(); + walFile.createNewFile(); + mjFile1.createNewFile(); + mjFile2.createNewFile(); + innocentFile.createNewFile(); + + boolean deleted = SQLiteDatabase.deleteDatabase(dbFile); + assertTrue(deleted); + + assertFalse(dbFile.exists()); + assertFalse(journalFile.exists()); + assertFalse(shmFile.exists()); + assertFalse(walFile.exists()); + assertFalse(mjFile1.exists()); + assertFalse(mjFile2.exists()); + assertTrue(innocentFile.exists()); + + innocentFile.delete(); + + boolean deletedAgain = SQLiteDatabase.deleteDatabase(dbFile); + assertFalse(deletedAgain); + } + + private class MockSQLiteCursor extends SQLiteCursor { + public MockSQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, + String editTable, SQLiteQuery query) { + super(db, driver, editTable, query); + } + } + + public void testTransaction() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + // test execSQL without any explicit transactions. + setNum(1); + assertNum(1); + + // Test a single-level transaction. + setNum(0); + assertFalse(mDatabase.inTransaction()); + mDatabase.beginTransaction(); + assertTrue(mDatabase.inTransaction()); + setNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertFalse(mDatabase.inTransaction()); + assertNum(1); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + + // Test a rolled-back transaction. + setNum(0); + assertFalse(mDatabase.inTransaction()); + mDatabase.beginTransaction(); + setNum(1); + assertTrue(mDatabase.inTransaction()); + mDatabase.endTransaction(); + assertFalse(mDatabase.inTransaction()); + assertNum(0); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + + // it should throw IllegalStateException if we end a non-existent transaction. + assertThrowsIllegalState(new Runnable() { + public void run() { + mDatabase.endTransaction(); + } + }); + + // it should throw IllegalStateException if a set a non-existent transaction as clean. + assertThrowsIllegalState(new Runnable() { + public void run() { + mDatabase.setTransactionSuccessful(); + } + }); + + mDatabase.beginTransaction(); + mDatabase.setTransactionSuccessful(); + // it should throw IllegalStateException if we mark a transaction as clean twice. + assertThrowsIllegalState(new Runnable() { + public void run() { + mDatabase.setTransactionSuccessful(); + } + }); + // it should throw IllegalStateException if we begin a transaction after marking the + // parent as clean. + assertThrowsIllegalState(new Runnable() { + public void run() { + mDatabase.beginTransaction(); + } + }); + mDatabase.endTransaction(); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + + assertFalse(mDatabase.inTransaction()); + // Test a two-level transaction. + setNum(0); + mDatabase.beginTransaction(); + assertTrue(mDatabase.inTransaction()); + mDatabase.beginTransaction(); + assertTrue(mDatabase.inTransaction()); + setNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertTrue(mDatabase.inTransaction()); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertFalse(mDatabase.inTransaction()); + assertNum(1); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + + // Test rolling back an inner transaction. + setNum(0); + mDatabase.beginTransaction(); + mDatabase.beginTransaction(); + setNum(1); + mDatabase.endTransaction(); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertNum(0); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + + // Test rolling back an outer transaction. + setNum(0); + mDatabase.beginTransaction(); + mDatabase.beginTransaction(); + setNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + mDatabase.endTransaction(); + assertNum(0); + assertFalse(mDatabase.isDbLockedByCurrentThread()); + assertFalse(mDatabase.isDbLockedByOtherThreads()); + } + + private void setNum(int num) { + mDatabase.execSQL("UPDATE test SET num = " + num); + } + + private void assertNum(int num) { + assertEquals(num, DatabaseUtils.longForQuery(mDatabase, + "SELECT num FROM test", null)); + } + + private void assertThrowsIllegalState(Runnable r) { + try { + r.run(); + fail("did not throw expected IllegalStateException"); + } catch (IllegalStateException e) { + } + } + + public void testAccessMaximumSize() { + long curMaximumSize = mDatabase.getMaximumSize(); + + // the new maximum size is less than the current size. + mDatabase.setMaximumSize(curMaximumSize - 1); + assertEquals(curMaximumSize, mDatabase.getMaximumSize()); + + // the new maximum size is more than the current size. + mDatabase.setMaximumSize(curMaximumSize + 1); + assertEquals(curMaximumSize + mDatabase.getPageSize(), mDatabase.getMaximumSize()); + assertTrue(mDatabase.getMaximumSize() > curMaximumSize); + } + + public void testAccessPageSize() { + File databaseFile = new File(mDatabaseDir, "database.db"); + if (databaseFile.exists()) { + databaseFile.delete(); + } + SQLiteDatabase database = null; + try { + database = SQLiteDatabase.openOrCreateDatabase(databaseFile.getPath(), null); + + long initialValue = database.getPageSize(); + // check that this does not throw an exception + // setting a different page size may not be supported after the DB has been created + database.setPageSize(initialValue); + assertEquals(initialValue, database.getPageSize()); + + } finally { + if (database != null) { + database.close(); + databaseFile.delete(); + } + } + } + + public void testCompileStatement() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + + String name = "Mike"; + int age = 21; + String address = "LA"; + + // at the beginning, there is no record in the database. + Cursor cursor = mDatabase.query("test", TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + + String sql = "INSERT INTO test (name, age, address) VALUES (?, ?, ?);"; + SQLiteStatement insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, name); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, age); + DatabaseUtils.bindObjectToProgram(insertStatement, 3, address); + insertStatement.execute(); + insertStatement.close(); + cursor.close(); + + cursor = mDatabase.query("test", TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(name, cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(age, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals(address, cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + SQLiteStatement deleteStatement = mDatabase.compileStatement("DELETE FROM test"); + deleteStatement.execute(); + + cursor = mDatabase.query("test", null, null, null, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.deactivate(); + deleteStatement.close(); + cursor.close(); + } + + public void testDelete() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Mike', 20, 'LA');"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Jack', 30, 'London');"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Jim', 35, 'Chicago');"); + + // delete one record. + int count = mDatabase.delete(TABLE_NAME, "name = 'Mike'", null); + assertEquals(1, count); + + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, + null, null, null, null); + assertNotNull(cursor); + // there are 2 records here. + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(30, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.moveToNext(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(35, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("Chicago", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + // delete another record. + count = mDatabase.delete(TABLE_NAME, "name = ?", new String[] { "Jack" }); + assertEquals(1, count); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, + null, null); + assertNotNull(cursor); + // there are 1 records here. + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(35, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("Chicago", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Mike', 20, 'LA');"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Jack', 30, 'London');"); + + // delete all records. + count = mDatabase.delete(TABLE_NAME, null, null); + assertEquals(3, count); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + public void testExecSQL() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + + // add a new record. + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Mike', 20, 'LA');"); + + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, + null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + // add other new record. + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Jack', 30, 'London');"); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.moveToNext(); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(30, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + // delete a record. + mDatabase.execSQL("DELETE FROM test WHERE name = ?;", new String[] { "Jack" }); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + // delete a non-exist record. + mDatabase.execSQL("DELETE FROM test WHERE name = ?;", new String[] { "Wrong Name" }); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + try { + // execSQL can not use for query. + mDatabase.execSQL("SELECT * FROM test;"); + fail("should throw SQLException."); + } catch (SQLException e) { + } + + // make sure execSQL can't be used to execute more than 1 sql statement at a time + mDatabase.execSQL("UPDATE test SET age = 40 WHERE name = 'Mike';" + + "UPDATE test SET age = 50 WHERE name = 'Mike';"); + // age should be updated to 40 not to 50 + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(40, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + // make sure sql injection is NOT allowed or has no effect when using query() + String harmfulQuery = "name = 'Mike';UPDATE test SET age = 50 WHERE name = 'Mike'"; + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, harmfulQuery, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + // row's age column SHOULD NOT be 50 + assertEquals(40, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close();; + } + + public void testFindEditTable() { + String tables = "table1 table2 table3"; + assertEquals("table1", SQLiteDatabase.findEditTable(tables)); + + tables = "table1,table2,table3"; + assertEquals("table1", SQLiteDatabase.findEditTable(tables)); + + tables = "table1"; + assertEquals("table1", SQLiteDatabase.findEditTable(tables)); + + try { + SQLiteDatabase.findEditTable(""); + fail("should throw IllegalStateException."); + } catch (IllegalStateException e) { + } + } + + public void testGetPath() { + assertEquals(mDatabaseFilePath, mDatabase.getPath()); + } + + public void testAccessVersion() { + mDatabase.setVersion(1); + assertEquals(1, mDatabase.getVersion()); + + mDatabase.setVersion(3); + assertEquals(3, mDatabase.getVersion()); + } + + public void testInsert() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + + ContentValues values = new ContentValues(); + values.put("name", "Jack"); + values.put("age", 20); + values.put("address", "LA"); + mDatabase.insert(TABLE_NAME, "name", values); + + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, + null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + mDatabase.insert(TABLE_NAME, "name", null); + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, + null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.moveToNext(); + assertNull(cursor.getString(COLUMN_NAME_INDEX)); + cursor.close(); + + values = new ContentValues(); + values.put("Wrong Key", "Wrong value"); + mDatabase.insert(TABLE_NAME, "name", values); + // there are still 2 records. + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, + null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.close(); + + // delete all record. + mDatabase.execSQL("DELETE FROM test;"); + + values = new ContentValues(); + values.put("name", "Mike"); + values.put("age", 30); + values.put("address", "London"); + mDatabase.insertOrThrow(TABLE_NAME, "name", values); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, + null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(30, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + mDatabase.insertOrThrow(TABLE_NAME, "name", null); + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, + null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(30, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.moveToNext(); + assertNull(cursor.getString(COLUMN_NAME_INDEX)); + cursor.close(); + + values = new ContentValues(); + values.put("Wrong Key", "Wrong value"); + try { + mDatabase.insertOrThrow(TABLE_NAME, "name", values); + fail("should throw SQLException."); + } catch (SQLException e) { + } + } + + public void testIsOpen() { + assertTrue(mDatabase.isOpen()); + + mDatabase.close(); + assertFalse(mDatabase.isOpen()); + } + + public void testIsReadOnly() { + assertFalse(mDatabase.isReadOnly()); + + SQLiteDatabase database = null; + try { + database = SQLiteDatabase.openDatabase(mDatabaseFilePath, null, + SQLiteDatabase.OPEN_READONLY); + assertTrue(database.isReadOnly()); + } finally { + if (database != null) { + database.close(); + } + } + } + + public void testReleaseMemory() { + SQLiteDatabase.releaseMemory(); + } + + public void testSetLockingEnabled() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + mDatabase.setLockingEnabled(false); + + mDatabase.beginTransaction(); + setNum(1); + assertNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + @SuppressWarnings("deprecation") + public void testYieldIfContendedWhenNotContended() { + assertFalse(mDatabase.yieldIfContended()); + + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + // Make sure that things work outside an explicit transaction. + setNum(1); + assertNum(1); + + setNum(0); + assertFalse(mDatabase.inTransaction()); + mDatabase.beginTransaction(); + assertTrue(mDatabase.inTransaction()); + assertFalse(mDatabase.yieldIfContended()); + setNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + mDatabase.beginTransaction(); + assertTrue(mDatabase.inTransaction()); + assertFalse(mDatabase.yieldIfContendedSafely()); + setNum(1); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + @SuppressWarnings("deprecation") + public void testYieldIfContendedWhenContended() throws Exception { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + // Begin a transaction and update a value. + mDatabase.beginTransaction(); + setNum(1); + assertNum(1); + + // On another thread, begin a transaction there. This causes contention + // for use of the database. When the main thread yields, the second thread + // begin its own transaction. It should perceive the new state that was + // committed by the main thread when it yielded. + final Semaphore s = new Semaphore(0); + Thread t = new Thread() { + @Override + public void run() { + s.release(); // let main thread continue + + mDatabase.beginTransaction(); + assertNum(1); + setNum(2); + assertNum(2); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + }; + t.start(); + + // Wait for thread to try to begin its transaction. + s.acquire(); + Thread.sleep(500); + + // Yield. There should be contention for the database now, so yield will + // return true. + assertTrue(mDatabase.yieldIfContendedSafely()); + + // Since we reacquired the transaction, the other thread must have finished + // its transaction. We should observe its changes and our own within this transaction. + assertNum(2); + setNum(3); + assertNum(3); + + // Go ahead and finish the transaction. + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + assertNum(3); + + t.join(); + } + + public void testQuery() { + mDatabase.execSQL("CREATE TABLE employee (_id INTEGER PRIMARY KEY, " + + "name TEXT, month INTEGER, salary INTEGER);"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Mike', '1', '1000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Mike', '2', '3000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('jack', '1', '2000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('jack', '3', '1500');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Jim', '1', '1000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Jim', '3', '3500');"); + + Cursor cursor = mDatabase.query(true, "employee", new String[] { "name", "sum(salary)" }, + null, null, "name", "sum(salary)>1000", "name", null); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + + final int COLUMN_NAME_INDEX = 0; + final int COLUMN_SALARY_INDEX = 1; + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.close(); + + CursorFactory factory = new CursorFactory() { + public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, + String editTable, SQLiteQuery query) { + return new MockSQLiteCursor(db, masterQuery, editTable, query); + } + }; + cursor = mDatabase.queryWithFactory(factory, true, "employee", + new String[] { "name", "sum(salary)" }, + null, null, "name", "sum(salary) > 1000", "name", null); + assertNotNull(cursor); + assertTrue(cursor instanceof MockSQLiteCursor); + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.close(); + + cursor = mDatabase.query("employee", new String[] { "name", "sum(salary)" }, + null, null, "name", "sum(salary) <= 4000", "name"); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.close(); + + cursor = mDatabase.query("employee", new String[] { "name", "sum(salary)" }, + null, null, "name", "sum(salary) > 1000", "name", "2"); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.close(); + + String sql = "SELECT name, month FROM employee WHERE salary > ?;"; + cursor = mDatabase.rawQuery(sql, new String[] { "2000" }); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + + final int COLUMN_MONTH_INDEX = 1; + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(2, cursor.getInt(COLUMN_MONTH_INDEX)); + cursor.moveToNext(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3, cursor.getInt(COLUMN_MONTH_INDEX)); + cursor.close(); + + cursor = mDatabase.rawQueryWithFactory(factory, sql, new String[] { "2000" }, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + assertTrue(cursor instanceof MockSQLiteCursor); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(2, cursor.getInt(COLUMN_MONTH_INDEX)); + cursor.moveToNext(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3, cursor.getInt(COLUMN_MONTH_INDEX)); + cursor.close(); + } + + public void testReplace() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + + ContentValues values = new ContentValues(); + values.put("name", "Jack"); + values.put("age", 20); + values.put("address", "LA"); + mDatabase.replace(TABLE_NAME, "name", values); + + Cursor cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, + null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + int id = cursor.getInt(COLUMN_ID_INDEX); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + values = new ContentValues(); + values.put("_id", id); + values.put("name", "Mike"); + values.put("age", 40); + values.put("address", "London"); + mDatabase.replace(TABLE_NAME, "name", values); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); // there is still ONLY 1 record. + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(40, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + values = new ContentValues(); + values.put("name", "Jack"); + values.put("age", 20); + values.put("address", "LA"); + mDatabase.replaceOrThrow(TABLE_NAME, "name", values); + + cursor = mDatabase.query(TABLE_NAME, TEST_PROJECTION, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(40, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("London", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.moveToNext(); + assertEquals("Jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(20, cursor.getInt(COLUMN_AGE_INDEX)); + assertEquals("LA", cursor.getString(COLUMN_ADDR_INDEX)); + cursor.close(); + + values = new ContentValues(); + values.put("Wrong Key", "Wrong value"); + try { + mDatabase.replaceOrThrow(TABLE_NAME, "name", values); + fail("should throw SQLException."); + } catch (SQLException e) { + } + } + + public void testUpdate() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + + mDatabase.execSQL("INSERT INTO test (data) VALUES ('string1');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('string2');"); + mDatabase.execSQL("INSERT INTO test (data) VALUES ('string3');"); + + String updatedString = "this is an updated test"; + ContentValues values = new ContentValues(1); + values.put("data", updatedString); + assertEquals(1, mDatabase.update("test", values, "_id=1", null)); + Cursor cursor = mDatabase.query("test", null, "_id=1", null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + String value = cursor.getString(cursor.getColumnIndexOrThrow("data")); + assertEquals(updatedString, value); + cursor.close(); + } + + public void testNeedUpgrade() { + mDatabase.setVersion(0); + assertTrue(mDatabase.needUpgrade(1)); + mDatabase.setVersion(1); + assertFalse(mDatabase.needUpgrade(1)); + } + + public void testSetLocale() { +// final String[] STRINGS = { +// "c\u00f4t\u00e9", +// "cote", +// "c\u00f4te", +// "cot\u00e9", +// "boy", +// "dog", +// "COTE", +// }; +// +// mDatabase.execSQL("CREATE TABLE test (data TEXT COLLATE LOCALIZED);"); +// for (String s : STRINGS) { +// mDatabase.execSQL("INSERT INTO test VALUES('" + s + "');"); +// } +// +// mDatabase.setLocale(new Locale("en", "US")); +// +// String sql = "SELECT data FROM test ORDER BY data COLLATE LOCALIZED ASC"; +// Cursor cursor = mDatabase.rawQuery(sql, null); +// assertNotNull(cursor); +// ArrayList items = new ArrayList(); +// while (cursor.moveToNext()) { +// items.add(cursor.getString(0)); +// } +// String[] results = items.toArray(new String[items.size()]); +// assertEquals(STRINGS.length, results.length); +// cursor.close(); +// +// // The database code currently uses PRIMARY collation strength, +// // meaning that all versions of a character compare equal (regardless +// // of case or accents), leaving the "cote" flavors in database order. +// MoreAsserts.assertEquals(results, new String[] { +// STRINGS[4], // "boy" +// STRINGS[0], // sundry forms of "cote" +// STRINGS[1], +// STRINGS[2], +// STRINGS[3], +// STRINGS[6], // "COTE" +// STRINGS[5], // "dog" +// }); + } + + public void testOnAllReferencesReleased() { + assertTrue(mDatabase.isOpen()); + mDatabase.releaseReference(); + assertFalse(mDatabase.isOpen()); + } + + public void testTransactionWithSQLiteTransactionListener() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + assertEquals(mTransactionListenerOnBeginCalled, false); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + mDatabase.beginTransactionWithListener(new TestSQLiteTransactionListener()); + + // Assert that the transcation has started + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + + setNum(1); + + // State shouldn't have changed + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + + // commit the transaction + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + // the listener should have been told that commit was called + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, true); + assertEquals(mTransactionListenerOnRollbackCalled, false); + } + + public void testRollbackTransactionWithSQLiteTransactionListener() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER);"); + mDatabase.execSQL("INSERT INTO test (num) VALUES (0)"); + + assertEquals(mTransactionListenerOnBeginCalled, false); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + mDatabase.beginTransactionWithListener(new TestSQLiteTransactionListener()); + + // Assert that the transcation has started + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + + setNum(1); + + // State shouldn't have changed + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, false); + + // commit the transaction + mDatabase.endTransaction(); + + // the listener should have been told that commit was called + assertEquals(mTransactionListenerOnBeginCalled, true); + assertEquals(mTransactionListenerOnCommitCalled, false); + assertEquals(mTransactionListenerOnRollbackCalled, true); + } + + private class TestSQLiteTransactionListener implements SQLiteTransactionListener { + public void onBegin() { + mTransactionListenerOnBeginCalled = true; + } + + public void onCommit() { + mTransactionListenerOnCommitCalled = true; + } + + public void onRollback() { + mTransactionListenerOnRollbackCalled = true; + } + } + + public void testGroupConcat() { + mDatabase.execSQL("CREATE TABLE test (i INT, j TEXT);"); + + // insert 2 rows + String sql = "INSERT INTO test (i) VALUES (?);"; + SQLiteStatement insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 1); + insertStatement.execute(); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 2); + insertStatement.execute(); + insertStatement.close(); + + // make sure there are 2 rows in the table + Cursor cursor = mDatabase.rawQuery("SELECT count(*) FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(2, cursor.getInt(0)); + cursor.close(); + + // concatenate column j from all the rows. should return NULL + cursor = mDatabase.rawQuery("SELECT group_concat(j, ' ') FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertNull(cursor.getString(0)); + cursor.close(); + + // drop the table + mDatabase.execSQL("DROP TABLE test;"); + // should get no exceptions + } + + public void testSchemaChanges() { + mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); + + // at the beginning, there is no record in the database. + Cursor cursor = mDatabase.rawQuery("SELECT * FROM test", null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + String sql = "INSERT INTO test VALUES (?, ?);"; + SQLiteStatement insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 1); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, 2); + insertStatement.execute(); + insertStatement.close(); + + // read the data from the table and make sure it is correct + cursor = mDatabase.rawQuery("SELECT i,j FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + cursor.close(); + + // alter the table and execute another statement + mDatabase.execSQL("ALTER TABLE test ADD COLUMN k int;"); + sql = "INSERT INTO test VALUES (?, ?, ?);"; + insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 3); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, 4); + DatabaseUtils.bindObjectToProgram(insertStatement, 3, 5); + insertStatement.execute(); + insertStatement.close(); + + // read the data from the table and make sure it is correct + cursor = mDatabase.rawQuery("SELECT i,j,k FROM test", null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + assertNull(cursor.getString(2)); + cursor.moveToNext(); + assertEquals(3, cursor.getInt(0)); + assertEquals(4, cursor.getInt(1)); + assertEquals(5, cursor.getInt(2)); + cursor.close(); + + // make sure the old statement - which should *try to reuse* cached query plan - + // still works + cursor = mDatabase.rawQuery("SELECT i,j FROM test", null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + cursor.moveToNext(); + assertEquals(3, cursor.getInt(0)); + assertEquals(4, cursor.getInt(1)); + cursor.close(); + + SQLiteStatement deleteStatement = mDatabase.compileStatement("DELETE FROM test"); + deleteStatement.execute(); + deleteStatement.close(); + } + + public void testSchemaChangesNewTable() { + mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); + + // at the beginning, there is no record in the database. + Cursor cursor = mDatabase.rawQuery("SELECT * FROM test", null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + String sql = "INSERT INTO test VALUES (?, ?);"; + SQLiteStatement insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 1); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, 2); + insertStatement.execute(); + insertStatement.close(); + + // read the data from the table and make sure it is correct + cursor = mDatabase.rawQuery("SELECT i,j FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + cursor.close(); + + // alter the table and execute another statement + mDatabase.execSQL("CREATE TABLE test_new (i INT, j INT, k INT);"); + sql = "INSERT INTO test_new VALUES (?, ?, ?);"; + insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 3); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, 4); + DatabaseUtils.bindObjectToProgram(insertStatement, 3, 5); + insertStatement.execute(); + insertStatement.close(); + + // read the data from the table and make sure it is correct + cursor = mDatabase.rawQuery("SELECT i,j,k FROM test_new", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(3, cursor.getInt(0)); + assertEquals(4, cursor.getInt(1)); + assertEquals(5, cursor.getInt(2)); + cursor.close(); + + // make sure the old statement - which should *try to reuse* cached query plan - + // still works + cursor = mDatabase.rawQuery("SELECT i,j FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + cursor.close(); + + SQLiteStatement deleteStatement = mDatabase.compileStatement("DELETE FROM test"); + deleteStatement.execute(); + deleteStatement.close(); + + SQLiteStatement deleteStatement2 = mDatabase.compileStatement("DELETE FROM test_new"); + deleteStatement2.execute(); + deleteStatement2.close(); + } + + public void testSchemaChangesDropTable() { + mDatabase.execSQL("CREATE TABLE test (i INT, j INT);"); + + // at the beginning, there is no record in the database. + Cursor cursor = mDatabase.rawQuery("SELECT * FROM test", null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + String sql = "INSERT INTO test VALUES (?, ?);"; + SQLiteStatement insertStatement = mDatabase.compileStatement(sql); + DatabaseUtils.bindObjectToProgram(insertStatement, 1, 1); + DatabaseUtils.bindObjectToProgram(insertStatement, 2, 2); + insertStatement.execute(); + insertStatement.close(); + + // read the data from the table and make sure it is correct + cursor = mDatabase.rawQuery("SELECT i,j FROM test", null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + assertEquals(1, cursor.getInt(0)); + assertEquals(2, cursor.getInt(1)); + } + + /** + * With sqlite's write-ahead-logging (WAL) enabled, readers get old version of data + * from the table that a writer is modifying at the same time. + *

+ * This method does the following to test this sqlite3 feature + *

    + *
  1. creates a table in the database and populates it with 5 rows of data
  2. + *
  3. do "select count(*) from this_table" and expect to receive 5
  4. + *
  5. start a writer thread who BEGINs a transaction, INSERTs a single row + * into this_table
  6. + *
  7. writer stops the transaction at this point, kicks off a reader thread - which will + * do the above SELECT query: "select count(*) from this_table"
  8. + *
  9. this query should return value 5 - because writer is still in transaction and + * sqlite returns OLD version of the data
  10. + *
  11. writer ends the transaction, thus making the extra row now visible to everyone
  12. + *
  13. reader is kicked off again to do the same query. this time query should + * return value = 6 which includes the newly inserted row into this_table.
  14. + *

    + * @throws InterruptedException + */ + @LargeTest + public void testReaderGetsOldVersionOfDataWhenWriterIsInXact() throws InterruptedException { + // redo setup to create WAL enabled database + mDatabase.close(); + new File(mDatabase.getPath()).delete(); + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, null); + boolean rslt = mDatabase.enableWriteAheadLogging(); + assertTrue(rslt); + assertNotNull(mDatabase); + + // create a new table and insert 5 records into it. + mDatabase.execSQL("CREATE TABLE t1 (i int, j int);"); + mDatabase.beginTransaction(); + for (int i = 0; i < 5; i++) { + mDatabase.execSQL("insert into t1 values(?,?);", new String[] {i+"", i+""}); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + // make sure a reader can read the above data + ReaderQueryingData r1 = new ReaderQueryingData(5); + r1.start(); + Thread.yield(); + try {r1.join();} catch (Exception e) {} + + WriterDoingSingleTransaction w = new WriterDoingSingleTransaction(); + w.start(); + w.join(); + } + + private class WriterDoingSingleTransaction extends Thread { + @Override public void run() { + // start a transaction + mDatabase.beginTransactionNonExclusive(); + mDatabase.execSQL("insert into t1 values(?,?);", new String[] {"11", "11"}); + assertTrue(mDatabase.isOpen()); + + // while the writer is in a transaction, start a reader and make sure it can still + // read 5 rows of data (= old data prior to the current transaction) + ReaderQueryingData r1 = new ReaderQueryingData(5); + r1.start(); + try {r1.join();} catch (Exception e) {} + + // now, have the writer do the select count(*) + // it should execute on the same connection as this transaction + // and count(*) should reflect the newly inserted row + Long l = DatabaseUtils.longForQuery(mDatabase, "select count(*) from t1", null); + assertEquals(6, l.intValue()); + + // end transaction + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + // reader should now be able to read 6 rows = new data AFTER this transaction + r1 = new ReaderQueryingData(6); + r1.start(); + try {r1.join();} catch (Exception e) {} + } + } + + private class ReaderQueryingData extends Thread { + private int count; + /** + * constructor with a param to indicate the number of rows expected to be read + */ + public ReaderQueryingData(int count) { + this.count = count; + } + @Override public void run() { + Long l = DatabaseUtils.longForQuery(mDatabase, "select count(*) from t1", null); + assertEquals(count, l.intValue()); + } + } + + public void testExceptionsFromEnableWriteAheadLogging() { + // attach a database + // redo setup to create WAL enabled database + mDatabase.close(); + new File(mDatabase.getPath()).delete(); + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, null); + + // attach a database and call enableWriteAheadLogging - should not be allowed + mDatabase.execSQL("attach database ':memory:' as memoryDb"); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + assertFalse(mDatabase.enableWriteAheadLogging()); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + + // enableWriteAheadLogging on memory database is not allowed + SQLiteDatabase db = SQLiteDatabase.create(null); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + assertFalse(db.enableWriteAheadLogging()); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + db.close(); + } + + public void testEnableThenDisableWriteAheadLogging() { + // Enable WAL. + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(mDatabase.enableWriteAheadLogging()); + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase("WAL")); + + // Enabling when already enabled should have no observable effect. + assertTrue(mDatabase.enableWriteAheadLogging()); + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase("WAL")); + + // Disabling when there are no connections should work. + mDatabase.disableWriteAheadLogging(); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + } + + public void testEnableThenDisableWriteAheadLoggingUsingOpenFlag() { + new File(mDatabase.getPath()).delete(); + mDatabase = SQLiteDatabase.openDatabase(mDatabaseFile.getPath(), null, + SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING, + null); + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase("WAL")); + + // Enabling when already enabled should have no observable effect. + assertTrue(mDatabase.enableWriteAheadLogging()); + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase("WAL")); + + // Disabling when there are no connections should work. + mDatabase.disableWriteAheadLogging(); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + } + + public void testEnableWriteAheadLoggingFromContextUsingModeFlag() { + // Without the MODE_ENABLE_WRITE_AHEAD_LOGGING flag, database opens without WAL. + getContext().deleteDatabase(DATABASE_FILE_NAME); + + File f = getContext().getDatabasePath(DATABASE_FILE_NAME); + mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + mDatabase.close(); + + // // With the MODE_ENABLE_WRITE_AHEAD_LOGGING flag, database opens with WAL. + // getContext().deleteDatabase(DATABASE_FILE_NAME); + // mDatabase = getContext().openOrCreateDatabase(DATABASE_FILE_NAME, + // Context.MODE_PRIVATE | Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null); + // assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + // mDatabase.close(); + } + + public void testEnableWriteAheadLoggingShouldThrowIfTransactionInProgress() { + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + String oldJournalMode = DatabaseUtils.stringForQuery( + mDatabase, "PRAGMA journal_mode", null); + + // Begin transaction. + mDatabase.beginTransaction(); + + try { + // Attempt to enable WAL should fail. + mDatabase.enableWriteAheadLogging(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException ex) { + // expected + } + + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase(oldJournalMode)); + } + + public void testDisableWriteAheadLoggingShouldThrowIfTransactionInProgress() { + // Enable WAL. + assertFalse(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(mDatabase.enableWriteAheadLogging()); + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + + // Begin transaction. + mDatabase.beginTransaction(); + + try { + // Attempt to disable WAL should fail. + mDatabase.disableWriteAheadLogging(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException ex) { + // expected + } + + assertTrue(mDatabase.isWriteAheadLoggingEnabled()); + assertTrue(DatabaseUtils.stringForQuery(mDatabase, "PRAGMA journal_mode", null) + .equalsIgnoreCase("WAL")); + } + + public void testEnableAndDisableForeignKeys() { + // Initially off. + assertEquals(0, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); + + // Enable foreign keys. + mDatabase.setForeignKeyConstraintsEnabled(true); + assertEquals(1, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); + + // Disable foreign keys. + mDatabase.setForeignKeyConstraintsEnabled(false); + assertEquals(0, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); + + // Cannot configure foreign keys if there are transactions in progress. + mDatabase.beginTransaction(); + try { + mDatabase.setForeignKeyConstraintsEnabled(true); + fail("Expected IllegalStateException"); + } catch (IllegalStateException ex) { + // expected + } + assertEquals(0, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); + mDatabase.endTransaction(); + + // Enable foreign keys should work again after transaction complete. + mDatabase.setForeignKeyConstraintsEnabled(true); + assertEquals(1, DatabaseUtils.longForQuery(mDatabase, "PRAGMA foreign_keys", null)); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDiskIOExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDiskIOExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDiskIOExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteDiskIOException; +import android.test.AndroidTestCase; + +public class SQLiteDiskIOExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteDiskIOException(); + + new SQLiteDiskIOException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDoneExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDoneExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteDoneExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteDoneException; +import android.test.AndroidTestCase; + +public class SQLiteDoneExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteDoneException(); + + new SQLiteDoneException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteException; +import android.test.AndroidTestCase; + +public class SQLiteExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteException(); + + new SQLiteException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFtsTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFtsTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFtsTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import android.content.ContentValues; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import org.sqlite.database.sqlite.SQLiteDatabase; +import android.test.AndroidTestCase; + +import java.io.File; + +/** + * Tests to verify FTS3/4 SQLite support. + */ +public class SQLiteFtsTest extends AndroidTestCase { + + private static final String TEST_TABLE = "cts_fts"; + + private static final String[] TEST_CONTENT = { + "Any sufficiently advanced TECHnology is indistinguishable from magic.", + "Those who would give up Essential Liberty to purchase a little Temporary Safety, deserve neither Liberty nor Safety.", + "It is poor civic hygiene to install technologies that could someday facilitate a police state.", + }; + + private SQLiteDatabase mDatabase; + + @Override + public void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File f = mContext.getDatabasePath("CTS_FTS"); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + } + + @Override + public void tearDown() throws Exception { + try { + final String path = mDatabase.getPath(); + mDatabase.close(); + SQLiteDatabase.deleteDatabase(new File(path)); + } finally { + super.tearDown(); + } + } + + public void testFts3Porter() throws Exception { + prepareFtsTable(TEST_TABLE, "fts3", "tokenize=porter"); + + // Porter should include stemmed words + final Cursor cursor = queryFtsTable(TEST_TABLE, "technology"); + try { + assertEquals(2, cursor.getCount()); + cursor.moveToPosition(0); + assertTrue(cursor.getString(0).contains(">TECHnology<")); + cursor.moveToPosition(1); + assertTrue(cursor.getString(0).contains(">technologies<")); + } finally { + cursor.close(); + } + } + + public void testFts3Simple() throws Exception { + prepareFtsTable(TEST_TABLE, "fts3", "tokenize=simple"); + + // Simple shouldn't include stemmed words + final Cursor cursor = queryFtsTable(TEST_TABLE, "technology"); + try { + assertEquals(1, cursor.getCount()); + cursor.moveToPosition(0); + assertTrue(cursor.getString(0).contains(">TECHnology<")); + } finally { + cursor.close(); + } + } + + public void testFts4Simple() throws Exception { + prepareFtsTable(TEST_TABLE, "fts4", "tokenize=simple"); + + // Simple shouldn't include stemmed words + final Cursor cursor = queryFtsTable(TEST_TABLE, "technology"); + try { + assertEquals(1, cursor.getCount()); + cursor.moveToPosition(0); + assertTrue(cursor.getString(0).contains(">TECHnology<")); + } finally { + cursor.close(); + } + } + + private void prepareFtsTable(String table, String ftsType, String options) + throws Exception { + mDatabase.execSQL( + "CREATE VIRTUAL TABLE " + table + " USING " + ftsType + + "(content TEXT, " + options + ");"); + + final Resources res = getContext().getResources(); + final ContentValues values = new ContentValues(); + for (String content : TEST_CONTENT) { + values.clear(); + values.put("content", content); + mDatabase.insert(table, null, values); + } + } + + private Cursor queryFtsTable(String table, String match) { + return mDatabase.query(table, new String[] { "snippet(" + table + ")" }, + "content MATCH ?", new String[] { match }, + null, null, "rowid ASC"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFullExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFullExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteFullExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteFullException; +import android.test.AndroidTestCase; + +public class SQLiteFullExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteFullException(); + + new SQLiteFullException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteMisuseExceptionTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteMisuseExceptionTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteMisuseExceptionTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import org.sqlite.database.sqlite.SQLiteMisuseException; +import android.test.AndroidTestCase; + +public class SQLiteMisuseExceptionTest extends AndroidTestCase { + public void testConstructor() { + new SQLiteMisuseException(); + + new SQLiteMisuseException("error"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteOpenHelperTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteOpenHelperTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteOpenHelperTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.sqlite.SQLiteCursor; +import org.sqlite.database.sqlite.SQLiteCursorDriver; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteOpenHelper; +import org.sqlite.database.sqlite.SQLiteQuery; +import org.sqlite.database.sqlite.SQLiteDatabase.CursorFactory; +import android.test.AndroidTestCase; + +import java.io.File; + +/** + * Test {@link SQLiteOpenHelper}. + */ +public class SQLiteOpenHelperTest extends AndroidTestCase { + private static final String TEST_DATABASE_NAME = "database_test.db"; + static String DATABASE_PATH; + private static final int TEST_VERSION = 1; + private static final int TEST_ILLEGAL_VERSION = 0; + private MockOpenHelper mOpenHelper; + private SQLiteDatabase.CursorFactory mFactory = new SQLiteDatabase.CursorFactory() { + public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, + String editTable, SQLiteQuery query) { + return new MockCursor(db, masterQuery, editTable, query); + } + }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + DATABASE_PATH = mContext.getDatabasePath(TEST_DATABASE_NAME).toString(); + mOpenHelper = getOpenHelper(); + } + + public void testConstructor() { + new MockOpenHelper(mContext, DATABASE_PATH, mFactory, TEST_VERSION); + + // Test with illegal version number. + try { + new MockOpenHelper(mContext, DATABASE_PATH, mFactory, TEST_ILLEGAL_VERSION); + fail("Constructor of SQLiteOpenHelp should throws a IllegalArgumentException here."); + } catch (IllegalArgumentException e) { + } + + // Test with null factory + new MockOpenHelper(mContext, DATABASE_PATH, null, TEST_VERSION); + } + + public void testGetDatabase() { + SQLiteDatabase database = null; + assertFalse(mOpenHelper.hasCalledOnOpen()); + // Test getReadableDatabase. + database = mOpenHelper.getReadableDatabase(); + assertNotNull(database); + assertTrue(database.isOpen()); + assertTrue(mOpenHelper.hasCalledOnOpen()); + + // Database has been opened, so onOpen can not be invoked. + mOpenHelper.resetStatus(); + assertFalse(mOpenHelper.hasCalledOnOpen()); + // Test getWritableDatabase. + SQLiteDatabase database2 = mOpenHelper.getWritableDatabase(); + assertSame(database, database2); + assertTrue(database.isOpen()); + assertFalse(mOpenHelper.hasCalledOnOpen()); + + mOpenHelper.close(); + assertFalse(database.isOpen()); + + // After close(), onOpen() will be invoked by getWritableDatabase. + mOpenHelper.resetStatus(); + assertFalse(mOpenHelper.hasCalledOnOpen()); + SQLiteDatabase database3 = mOpenHelper.getWritableDatabase(); + assertNotNull(database); + assertNotSame(database, database3); + assertTrue(mOpenHelper.hasCalledOnOpen()); + assertTrue(database3.isOpen()); + mOpenHelper.close(); + assertFalse(database3.isOpen()); + } + + private MockOpenHelper getOpenHelper() { + return new MockOpenHelper(mContext, DATABASE_PATH, mFactory, TEST_VERSION); + } + + private class MockOpenHelper extends SQLiteOpenHelper { + private boolean mHasCalledOnOpen = false; + + public MockOpenHelper(Context context, String name, CursorFactory factory, int version) { + super(context, name, factory, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + + @Override + public void onOpen(SQLiteDatabase db) { + mHasCalledOnOpen = true; + } + + public boolean hasCalledOnOpen() { + return mHasCalledOnOpen; + } + + public void resetStatus() { + mHasCalledOnOpen = false; + } + } + + private class MockCursor extends SQLiteCursor { + public MockCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, + SQLiteQuery query) { + super(db, driver, editTable, query); + } + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteProgramTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteProgramTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteProgramTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + + +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDoneException; +import org.sqlite.database.sqlite.SQLiteException; +import org.sqlite.database.sqlite.SQLiteQuery; +import org.sqlite.database.sqlite.SQLiteStatement; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import java.io.File; + +public class SQLiteProgramTest extends AndroidTestCase { + private static final String DATABASE_NAME = "database_test.db"; + + private SQLiteDatabase mDatabase; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File f = mContext.getDatabasePath(DATABASE_NAME); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + assertNotNull(mDatabase); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + getContext().deleteDatabase(DATABASE_NAME); + + super.tearDown(); + } + + public void testBind() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + + "num1 INTEGER, num2 INTEGER, image BLOB);"); + mDatabase.execSQL("INSERT INTO test (text1, text2, num1, num2, image) " + + "VALUES ('Mike', 'Jack', 12, 30, 'abcdefg');"); + mDatabase.execSQL("INSERT INTO test (text1, text2, num1, num2, image) " + + "VALUES ('test1', 'test2', 213, 589, '123456789');"); + SQLiteStatement statement; + + statement = mDatabase.compileStatement("SELECT num1 FROM test WHERE num2 = ?;"); + statement.bindLong(1, 30); + assertEquals(12, statement.simpleQueryForLong()); + + // re-bind without clearing + statement.bindDouble(1, 589.0); + assertEquals(213, statement.simpleQueryForLong()); + statement.close(); + + statement = mDatabase.compileStatement("SELECT text1 FROM test WHERE text2 = ?;"); + + statement.bindDouble(1, 589.0); // Wrong binding + try { + statement.simpleQueryForString(); + fail("Should throw exception (no rows found)"); + } catch (SQLiteDoneException expected) { + // expected + } + statement.bindString(1, "test2"); + assertEquals("test1", statement.simpleQueryForString()); + statement.clearBindings(); + try { + statement.simpleQueryForString(); + fail("Should throw exception (unbound value)"); + } catch (SQLiteDoneException expected) { + // expected + } + statement.close(); + + Cursor cursor = null; + try { + cursor = mDatabase.query("test", new String[]{"text1"}, "where text1='a'", + new String[]{"foo"}, null, null, null); + fail("Should throw exception (no value to bind)"); + } catch (SQLiteException expected) { + // expected + } finally { + if (cursor != null) { + cursor.close(); + } + } + try { + cursor = mDatabase.query("test", new String[]{"text1"}, "where text1='a'", + new String[]{"foo", "bar"}, null, null, null); + fail("Should throw exception (index too large)"); + } catch (SQLiteException expected) { + // expected + } finally { + if (cursor != null) { + cursor.close(); + } + } + // test positive case + statement = mDatabase.compileStatement( + "SELECT text1 FROM test WHERE text2 = ? AND num2 = ?;"); + statement.bindString(1, "Jack"); + statement.bindLong(2, 30); + assertEquals("Mike", statement.simpleQueryForString()); + statement.close(); + } + + public void testBindNull() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + + "num1 INTEGER, num2 INTEGER, image BLOB);"); + + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test " + + "(text1,text2,num1,image) VALUES (?,?,?,?)"); + statement.bindString(1, "string1"); + statement.bindString(2, "string2"); + statement.bindLong(3, 100); + statement.bindNull(4); + statement.execute(); + statement.close(); + + final int COLUMN_TEXT1_INDEX = 0; + final int COLUMN_TEXT2_INDEX = 1; + final int COLUMN_NUM1_INDEX = 2; + final int COLUMN_IMAGE_INDEX = 3; + + Cursor cursor = mDatabase.query("test", new String[] { "text1", "text2", "num1", "image" }, + null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("string1", cursor.getString(COLUMN_TEXT1_INDEX)); + assertEquals("string2", cursor.getString(COLUMN_TEXT2_INDEX)); + assertEquals(100, cursor.getInt(COLUMN_NUM1_INDEX)); + assertNull(cursor.getString(COLUMN_IMAGE_INDEX)); + cursor.close(); + } + + public void testBindBlob() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + + "num1 INTEGER, num2 INTEGER, image BLOB);"); + + SQLiteStatement statement = mDatabase.compileStatement("INSERT INTO test " + + "(text1,text2,num1,image) VALUES (?,?,?,?)"); + statement.bindString(1, "string1"); + statement.bindString(2, "string2"); + statement.bindLong(3, 100); + byte[] blob = new byte[] { '1', '2', '3' }; + statement.bindBlob(4, blob); + statement.execute(); + statement.close(); + + final int COLUMN_TEXT1_INDEX = 0; + final int COLUMN_TEXT2_INDEX = 1; + final int COLUMN_NUM1_INDEX = 2; + final int COLUMN_IMAGE_INDEX = 3; + + Cursor cursor = mDatabase.query("test", new String[] { "text1", "text2", "num1", "image" }, + null, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("string1", cursor.getString(COLUMN_TEXT1_INDEX)); + assertEquals("string2", cursor.getString(COLUMN_TEXT2_INDEX)); + assertEquals(100, cursor.getInt(COLUMN_NUM1_INDEX)); + byte[] value = cursor.getBlob(COLUMN_IMAGE_INDEX); + MoreAsserts.assertEquals(blob, value); + cursor.close(); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryBuilderTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryBuilderTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryBuilderTest.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + + +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.sqlite.SQLiteCursor; +import org.sqlite.database.sqlite.SQLiteCursorDriver; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteQuery; +import org.sqlite.database.sqlite.SQLiteQueryBuilder; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.test.AndroidTestCase; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.io.File; + +public class SQLiteQueryBuilderTest extends AndroidTestCase { + private SQLiteDatabase mDatabase; + private final String TEST_TABLE_NAME = "test"; + private final String EMPLOYEE_TABLE_NAME = "employee"; + private static final String DATABASE_FILE = "database_test.db"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + File f = mContext.getDatabasePath(DATABASE_FILE); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f,null); + assertNotNull(mDatabase); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + getContext().deleteDatabase(DATABASE_FILE); + super.tearDown(); + } + + public void testConstructor() { + new SQLiteQueryBuilder(); + } + + public void testSetDistinct() { + String expected; + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(TEST_TABLE_NAME); + sqliteQueryBuilder.setDistinct(false); + sqliteQueryBuilder.appendWhere("age=20"); + String sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" }, + null, null, null, null, null, null); + assertEquals(TEST_TABLE_NAME, sqliteQueryBuilder.getTables()); + expected = "SELECT age, address FROM " + TEST_TABLE_NAME + " WHERE (age=20)"; + assertEquals(expected, sql); + + sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME); + sqliteQueryBuilder.setDistinct(true); + sqliteQueryBuilder.appendWhere("age>32"); + sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" }, + null, null, null, null, null, null); + assertEquals(EMPLOYEE_TABLE_NAME, sqliteQueryBuilder.getTables()); + expected = "SELECT DISTINCT age, address FROM " + EMPLOYEE_TABLE_NAME + " WHERE (age>32)"; + assertEquals(expected, sql); + + sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME); + sqliteQueryBuilder.setDistinct(true); + sqliteQueryBuilder.appendWhereEscapeString("age>32"); + sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" }, + null, null, null, null, null, null); + assertEquals(EMPLOYEE_TABLE_NAME, sqliteQueryBuilder.getTables()); + expected = "SELECT DISTINCT age, address FROM " + EMPLOYEE_TABLE_NAME + + " WHERE ('age>32')"; + assertEquals(expected, sql); + } + + public void testSetProjectionMap() { + String expected; + Map projectMap = new HashMap(); + projectMap.put("EmployeeName", "name"); + projectMap.put("EmployeeAge", "age"); + projectMap.put("EmployeeAddress", "address"); + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(TEST_TABLE_NAME); + sqliteQueryBuilder.setDistinct(false); + sqliteQueryBuilder.setProjectionMap(projectMap); + String sql = sqliteQueryBuilder.buildQuery(new String[] { "EmployeeName", "EmployeeAge" }, + null, null, null, null, null, null); + expected = "SELECT name, age FROM " + TEST_TABLE_NAME; + assertEquals(expected, sql); + + sql = sqliteQueryBuilder.buildQuery(null, // projectionIn is null + null, null, null, null, null, null); + assertTrue(sql.matches("SELECT (age|name|address), (age|name|address), (age|name|address) " + + "FROM " + TEST_TABLE_NAME)); + assertTrue(sql.contains("age")); + assertTrue(sql.contains("name")); + assertTrue(sql.contains("address")); + + sqliteQueryBuilder.setProjectionMap(null); + sql = sqliteQueryBuilder.buildQuery(new String[] { "name", "address" }, + null, null, null, null, null, null); + assertTrue(sql.matches("SELECT (name|address), (name|address) " + + "FROM " + TEST_TABLE_NAME)); + assertTrue(sql.contains("name")); + assertTrue(sql.contains("address")); + } + + public void testSetCursorFactory() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, " + + "name TEXT, age INTEGER, address TEXT);"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('Mike', '20', 'LA');"); + mDatabase.execSQL("INSERT INTO test (name, age, address) VALUES ('jack', '40', 'LA');"); + + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(TEST_TABLE_NAME); + Cursor cursor = sqliteQueryBuilder.query(mDatabase, new String[] { "name", "age" }, + null, null, null, null, null); + assertNotNull(cursor); + assertTrue(cursor instanceof SQLiteCursor); + + SQLiteDatabase.CursorFactory factory = new SQLiteDatabase.CursorFactory() { + public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, + String editTable, SQLiteQuery query) { + return new MockCursor(db, masterQuery, editTable, query); + } + }; + + sqliteQueryBuilder.setCursorFactory(factory); + cursor = sqliteQueryBuilder.query(mDatabase, new String[] { "name", "age" }, + null, null, null, null, null); + assertNotNull(cursor); + assertTrue(cursor instanceof MockCursor); + } + + private static class MockCursor extends SQLiteCursor { + public MockCursor(SQLiteDatabase db, SQLiteCursorDriver driver, + String editTable, SQLiteQuery query) { + super(db, driver, editTable, query); + } + } + + public void testBuildQueryString() { + String expected; + final String[] DEFAULT_TEST_PROJECTION = new String [] { "name", "age", "sum(salary)" }; + final String DEFAULT_TEST_WHERE = "age > 25"; + final String DEFAULT_HAVING = "sum(salary) > 3000"; + + String sql = SQLiteQueryBuilder.buildQueryString(false, "Employee", + DEFAULT_TEST_PROJECTION, + DEFAULT_TEST_WHERE, "name", DEFAULT_HAVING, "name", "100"); + + expected = "SELECT name, age, sum(salary) FROM Employee WHERE " + DEFAULT_TEST_WHERE + + " GROUP BY name " + + "HAVING " + DEFAULT_HAVING + " " + + "ORDER BY name " + + "LIMIT 100"; + assertEquals(expected, sql); + } + + public void testBuildQuery() { + final String[] DEFAULT_TEST_PROJECTION = new String[] { "name", "sum(salary)" }; + final String DEFAULT_TEST_WHERE = "age > 25"; + final String DEFAULT_HAVING = "sum(salary) > 2000"; + + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(TEST_TABLE_NAME); + sqliteQueryBuilder.setDistinct(false); + String sql = sqliteQueryBuilder.buildQuery(DEFAULT_TEST_PROJECTION, + DEFAULT_TEST_WHERE, null, "name", DEFAULT_HAVING, "name", "2"); + String expected = "SELECT name, sum(salary) FROM " + TEST_TABLE_NAME + + " WHERE (" + DEFAULT_TEST_WHERE + ") " + + "GROUP BY name HAVING " + DEFAULT_HAVING + " ORDER BY name LIMIT 2"; + assertEquals(expected, sql); + } + + public void testAppendColumns() { + StringBuilder sb = new StringBuilder(); + String[] columns = new String[] { "name", "age" }; + + assertEquals("", sb.toString()); + SQLiteQueryBuilder.appendColumns(sb, columns); + assertEquals("name, age ", sb.toString()); + } + + public void testQuery() { + createEmployeeTable(); + + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("Employee"); + Cursor cursor = sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name"); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + + final int COLUMN_NAME_INDEX = 0; + final int COLUMN_SALARY_INDEX = 1; + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("jack", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(3500, cursor.getInt(COLUMN_SALARY_INDEX)); + + sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME); + cursor = sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name", "2" // limit is 2 + ); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX)); + cursor.moveToNext(); + assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX)); + assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX)); + } + + public void testUnionQuery() { + String expected; + String[] innerProjection = new String[] {"name", "age", "location"}; + SQLiteQueryBuilder employeeQueryBuilder = new SQLiteQueryBuilder(); + SQLiteQueryBuilder peopleQueryBuilder = new SQLiteQueryBuilder(); + + employeeQueryBuilder.setTables("employee"); + peopleQueryBuilder.setTables("people"); + + String employeeSubQuery = employeeQueryBuilder.buildUnionSubQuery( + "_id", innerProjection, + null, 2, "employee", + "age=25", + null, null, null); + String peopleSubQuery = peopleQueryBuilder.buildUnionSubQuery( + "_id", innerProjection, + null, 2, "people", + "location=LA", + null, null, null); + expected = "SELECT name, age, location FROM employee WHERE (age=25)"; + assertEquals(expected, employeeSubQuery); + expected = "SELECT name, age, location FROM people WHERE (location=LA)"; + assertEquals(expected, peopleSubQuery); + + SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); + + unionQueryBuilder.setDistinct(true); + + String unionQuery = unionQueryBuilder.buildUnionQuery( + new String[] { employeeSubQuery, peopleSubQuery }, null, null); + expected = "SELECT name, age, location FROM employee WHERE (age=25) " + + "UNION SELECT name, age, location FROM people WHERE (location=LA)"; + assertEquals(expected, unionQuery); + } + + public void testCancelableQuery_WhenNotCanceled_ReturnsResultSet() { + createEmployeeTable(); + + CancellationSignal cancellationSignal = new CancellationSignal(); + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("Employee"); + Cursor cursor = sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name", null, cancellationSignal); + + assertEquals(3, cursor.getCount()); + } + + public void testCancelableQuery_WhenCanceledBeforeQuery_ThrowsImmediately() { + createEmployeeTable(); + + CancellationSignal cancellationSignal = new CancellationSignal(); + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("Employee"); + + cancellationSignal.cancel(); + try { + sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name", null, cancellationSignal); + fail("Expected OperationCanceledException"); + } catch (OperationCanceledException ex) { + // expected + } + } + + public void testCancelableQuery_WhenCanceledAfterQuery_ThrowsWhenExecuted() { + createEmployeeTable(); + + CancellationSignal cancellationSignal = new CancellationSignal(); + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("Employee"); + + Cursor cursor = sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name", null, cancellationSignal); + + cancellationSignal.cancel(); + try { + cursor.getCount(); // force execution + fail("Expected OperationCanceledException"); + } catch (OperationCanceledException ex) { + // expected + } + } + + public void testCancelableQuery_WhenCanceledDueToContention_StopsWaitingAndThrows() { + createEmployeeTable(); + + for (int i = 0; i < 5; i++) { + final CancellationSignal cancellationSignal = new CancellationSignal(); + final Semaphore barrier1 = new Semaphore(0); + final Semaphore barrier2 = new Semaphore(0); + Thread contentionThread = new Thread() { + @Override + public void run() { + mDatabase.beginTransaction(); // acquire the only available connection + barrier1.release(); // release query to start running + try { + barrier2.acquire(); // wait for test to end + } catch (InterruptedException e) { + } + mDatabase.endTransaction(); // release the connection + } + }; + Thread cancellationThread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(300); + } catch (InterruptedException ex) { + } + cancellationSignal.cancel(); + } + }; + try { + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("Employee"); + + contentionThread.start(); + cancellationThread.start(); + + try { + barrier1.acquire(); // wait for contention thread to start transaction + } catch (InterruptedException e) { + } + + final long startTime = System.nanoTime(); + try { + Cursor cursor = sqliteQueryBuilder.query(mDatabase, + new String[] { "name", "sum(salary)" }, null, null, + "name", "sum(salary)>1000", "name", null, cancellationSignal); + cursor.getCount(); // force execution + fail("Expected OperationCanceledException"); + } catch (OperationCanceledException ex) { + // expected + } + + // We want to confirm that the query really was blocked trying to acquire a + // connection for a certain amount of time before it was freed by cancel. + final long waitTime = System.nanoTime() - startTime; + if (waitTime > 150 * 1000000L) { + return; // success! + } + } finally { + barrier1.release(); + barrier2.release(); + try { + contentionThread.join(); + cancellationThread.join(); + } catch (InterruptedException e) { + } + } + } + + // Occasionally we might miss the timing deadline due to factors in the + // environment, but if after several trials we still couldn't demonstrate + // that the query was blocked, then the test must be broken. + fail("Could not prove that the query actually blocked before cancel() was called."); + } + + public void testCancelableQuery_WhenCanceledDuringLongRunningQuery_CancelsQueryAndThrows() { + // Populate a table with a bunch of integers. + mDatabase.execSQL("CREATE TABLE x (v INTEGER);"); + for (int i = 0; i < 100; i++) { + mDatabase.execSQL("INSERT INTO x VALUES (?)", new Object[] { i }); + } + + for (int i = 0; i < 5; i++) { + final CancellationSignal cancellationSignal = new CancellationSignal(); + Thread cancellationThread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(300); + } catch (InterruptedException ex) { + } + cancellationSignal.cancel(); + } + }; + try { + // Build an unsatisfiable 5-way cross-product query over 100 values but + // produces no output. This should force SQLite to loop for a long time + // as it tests 10^10 combinations. + SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder(); + sqliteQueryBuilder.setTables("x AS a, x AS b, x AS c, x AS d, x AS e"); + + cancellationThread.start(); + + final long startTime = System.nanoTime(); + try { + Cursor cursor = sqliteQueryBuilder.query(mDatabase, null, + "a.v + b.v + c.v + d.v + e.v > 1000000", + null, null, null, null, null, cancellationSignal); + cursor.getCount(); // force execution + fail("Expected OperationCanceledException"); + } catch (OperationCanceledException ex) { + // expected + } + + // We want to confirm that the query really was running and then got + // canceled midway. + final long waitTime = System.nanoTime() - startTime; + if (waitTime > 150 * 1000000L && waitTime < 600 * 1000000L) { + return; // success! + } + } finally { + try { + cancellationThread.join(); + } catch (InterruptedException e) { + } + } + } + + // Occasionally we might miss the timing deadline due to factors in the + // environment, but if after several trials we still couldn't demonstrate + // that the query was canceled, then the test must be broken. + fail("Could not prove that the query actually canceled midway during execution."); + } + + private void createEmployeeTable() { + mDatabase.execSQL("CREATE TABLE employee (_id INTEGER PRIMARY KEY, " + + "name TEXT, month INTEGER, salary INTEGER);"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Mike', '1', '1000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Mike', '2', '3000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('jack', '1', '2000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('jack', '3', '1500');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Jim', '1', '1000');"); + mDatabase.execSQL("INSERT INTO employee (name, month, salary) " + + "VALUES ('Jim', '3', '3500');"); + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteQueryTest.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + +import junit.framework.TestCase; + +public class SQLiteQueryTest extends TestCase { + public void testMethods() { + // cannot obtain an instance of SQLiteQuery + } +} ADDED sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteStatementTest.java Index: sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteStatementTest.java ================================================================== --- /dev/null +++ sqlite3/src/androidTest/java/org/sqlite/database/sqlite_cts/SQLiteStatementTest.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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.sqlite.database.sqlite_cts; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import org.sqlite.database.DatabaseUtils; +import org.sqlite.database.SQLException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDoneException; +import org.sqlite.database.sqlite.SQLiteStatement; +import android.os.ParcelFileDescriptor; +import android.support.test.filters.Suppress; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import java.io.IOException; +import java.io.InputStream; +import java.io.File; + +public class SQLiteStatementTest extends AndroidTestCase { + private static final String STRING1 = "this is a test"; + private static final String STRING2 = "another test"; + + private static final byte[][] BLOBS = new byte [][] { + parseBlob("86FADCF1A820666AEBD0789F47932151A2EF734269E8AC4E39630AB60519DFD8"), + new byte[1], + null, + parseBlob("00"), + parseBlob("FF"), + parseBlob("D7B500FECF25F7A4D83BF823D3858690790F2526013DE6CAE9A69170E2A1E47238"), + }; + + private static final String DATABASE_NAME = "database_test.db"; + + private static final int CURRENT_DATABASE_VERSION = 42; + private SQLiteDatabase mDatabase; + + @Override + protected void setUp() throws Exception { + super.setUp(); + System.loadLibrary("sqliteX"); + getContext().deleteDatabase(DATABASE_NAME); + File f = mContext.getDatabasePath(DATABASE_NAME); + f.mkdirs(); + if (f.exists()) { f.delete(); } + mDatabase = SQLiteDatabase.openOrCreateDatabase(f, null); + assertNotNull(mDatabase); + mDatabase.setVersion(CURRENT_DATABASE_VERSION); + } + + @Override + protected void tearDown() throws Exception { + mDatabase.close(); + getContext().deleteDatabase(DATABASE_NAME); + super.tearDown(); + } + + private void populateDefaultTable() { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data TEXT);"); + } + + private void populateBlobTable() { + mDatabase.execSQL("CREATE TABLE blob_test (_id INTEGER PRIMARY KEY, data BLOB)"); + for (int i = 0; i < BLOBS.length; i++) { + ContentValues values = new ContentValues(); + values.put("_id", i); + values.put("data", BLOBS[i]); + mDatabase.insert("blob_test", null, values); + } + } + + public void testExecute() { + mDatabase.disableWriteAheadLogging(); + populateDefaultTable(); + + assertEquals(0, DatabaseUtils.longForQuery(mDatabase, "select count(*) from test", null)); + + // test update + // insert 2 rows and then update them. + SQLiteStatement statement1 = mDatabase.compileStatement( + "INSERT INTO test (data) VALUES ('" + STRING2 + "')"); + assertEquals(1, statement1.executeInsert()); + assertEquals(2, statement1.executeInsert()); + SQLiteStatement statement2 = + mDatabase.compileStatement("UPDATE test set data = 'a' WHERE _id > 0"); + assertEquals(2, statement2.executeUpdateDelete()); + statement2.close(); + // should still have 2 rows in the table + assertEquals(2, DatabaseUtils.longForQuery(mDatabase, "select count(*) from test", null)); + + // test delete + // insert 2 more rows and delete 3 of them + assertEquals(3, statement1.executeInsert()); + assertEquals(4, statement1.executeInsert()); + statement1.close(); + statement2 = mDatabase.compileStatement("DELETE from test WHERE _id < 4"); + assertEquals(3, statement2.executeUpdateDelete()); + statement2.close(); + // should still have 1 row1 in the table + assertEquals(1, DatabaseUtils.longForQuery(mDatabase, "select count(*) from test", null)); + + // if the SQL statement is something that causes rows of data to + // be returned, executeUpdateDelete() (and execute()) throw an exception. + statement2 = mDatabase.compileStatement("SELECT count(*) FROM test"); + try { + statement2.executeUpdateDelete(); + fail("exception expected"); + } catch (SQLException e) { + // expected + } finally { + statement2.close(); + } + } + + public void testExecuteInsert() { + populateDefaultTable(); + + Cursor c = mDatabase.query("test", null, null, null, null, null, null); + assertEquals(0, c.getCount()); + + // test insert + SQLiteStatement statement = mDatabase.compileStatement( + "INSERT INTO test (data) VALUES ('" + STRING2 + "')"); + assertEquals(1, statement.executeInsert()); + statement.close(); + + // try to insert another row with the same id. last inserted rowid should be -1 + statement = mDatabase.compileStatement("insert or ignore into test values(1, 1);"); + assertEquals(-1, statement.executeInsert()); + statement.close(); + + c = mDatabase.query("test", null, null, null, null, null, null); + assertEquals(1, c.getCount()); + + c.moveToFirst(); + assertEquals(STRING2, c.getString(c.getColumnIndex("data"))); + c.close(); + + // if the sql statement is something that causes rows of data to + // be returned, executeInsert() throws an exception + statement = mDatabase.compileStatement( + "SELECT * FROM test WHERE data=\"" + STRING2 + "\""); + try { + statement.executeInsert(); + fail("exception expected"); + } catch (SQLException e) { + // expected + } finally { + statement.close(); + + } + } + + public void testSimpleQueryForLong() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); + mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); + SQLiteStatement statement = + mDatabase.compileStatement("SELECT num FROM test WHERE str = ?"); + + // test query long + statement.bindString(1, "hello"); + long value = statement.simpleQueryForLong(); + assertEquals(1234, value); + + // test query returns zero rows + statement.bindString(1, "world"); + + try { + statement.simpleQueryForLong(); + fail("There should be a SQLiteDoneException thrown out."); + } catch (SQLiteDoneException e) { + // expected. + } + + statement.close(); + } + + public void testSimpleQueryForString() { + mDatabase.execSQL("CREATE TABLE test (num INTEGER NOT NULL, str TEXT NOT NULL);"); + mDatabase.execSQL("INSERT INTO test VALUES (1234, 'hello');"); + SQLiteStatement statement = + mDatabase.compileStatement("SELECT str FROM test WHERE num = ?"); + + // test query String + statement.bindLong(1, 1234); + String value = statement.simpleQueryForString(); + assertEquals("hello", value); + + // test query returns zero rows + statement.bindLong(1, 5678); + + try { + statement.simpleQueryForString(); + fail("There should be a SQLiteDoneException thrown out."); + } catch (SQLiteDoneException e) { + // expected. + } + + statement.close(); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessNormal() throws IOException { + doTestSimpleQueryForBlobFileDescriptorSuccess(0); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessEmpty() throws IOException { + doTestSimpleQueryForBlobFileDescriptorSuccess(1); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessNull() { + populateBlobTable(); + + String sql = "SELECT data FROM blob_test WHERE _id = " + 2; + SQLiteStatement stm = mDatabase.compileStatement(sql); + assertNull(stm.simpleQueryForBlobFileDescriptor()); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccess00() throws IOException { + doTestSimpleQueryForBlobFileDescriptorSuccess(3); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessFF() throws IOException { + doTestSimpleQueryForBlobFileDescriptorSuccess(4); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessEmbeddedNul() throws IOException { + doTestSimpleQueryForBlobFileDescriptorSuccess(5); + } + + @Suppress + private void doTestSimpleQueryForBlobFileDescriptorSuccess(int i) throws IOException { + populateBlobTable(); + + String sql = "SELECT data FROM blob_test WHERE _id = " + i; + SQLiteStatement stm = mDatabase.compileStatement(sql); + ParcelFileDescriptor fd = stm.simpleQueryForBlobFileDescriptor(); + assertFileDescriptorContent(BLOBS[i], fd); + } + + @Suppress + public void testSimpleQueryForBlobFileDescriptorSuccessParam() throws IOException { + populateBlobTable(); + + String sql = "SELECT data FROM blob_test WHERE _id = ?"; + SQLiteStatement stm = mDatabase.compileStatement(sql); + stm.bindLong(1, 0); + ParcelFileDescriptor fd = stm.simpleQueryForBlobFileDescriptor(); + assertFileDescriptorContent(BLOBS[0], fd); + } + + public void testGetBlobFailureNoParam() throws Exception { + populateBlobTable(); + + String sql = "SELECT data FROM blob_test WHERE _id = 100"; + SQLiteStatement stm = mDatabase.compileStatement(sql); + ParcelFileDescriptor fd = null; + SQLiteDoneException expectedException = null; + try { + fd = stm.simpleQueryForBlobFileDescriptor(); + } catch (SQLiteDoneException ex) { + expectedException = ex; + } finally { + if (fd != null) { + fd.close(); + fd = null; + } + } + assertNotNull("Should have thrown SQLiteDoneException", expectedException); + } + + public void testGetBlobFailureParam() throws Exception { + populateBlobTable(); + + String sql = "SELECT data FROM blob_test WHERE _id = ?"; + SQLiteStatement stm = mDatabase.compileStatement(sql); + stm.bindLong(1, 100); + ParcelFileDescriptor fd = null; + SQLiteDoneException expectedException = null; + try { + fd = stm.simpleQueryForBlobFileDescriptor(); + } catch (SQLiteDoneException ex) { + expectedException = ex; + } finally { + if (fd != null) { + fd.close(); + fd = null; + } + } + assertNotNull("Should have thrown SQLiteDoneException", expectedException); + } + + /* + * Convert string of hex digits to byte array. + * Results are undefined for poorly formed string. + * + * @param src hex string + */ + private static byte[] parseBlob(String src) { + int len = src.length(); + byte[] result = new byte[len / 2]; + + for (int i = 0; i < len/2; i++) { + int val; + char c1 = src.charAt(i*2); + char c2 = src.charAt(i*2+1); + int val1 = Character.digit(c1, 16); + int val2 = Character.digit(c2, 16); + val = (val1 << 4) | val2; + result[i] = (byte)val; + } + return result; + } + + private static void assertFileDescriptorContent(byte[] expected, ParcelFileDescriptor fd) + throws IOException { + assertInputStreamContent(expected, new ParcelFileDescriptor.AutoCloseInputStream(fd)); + } + + private static void assertInputStreamContent(byte[] expected, InputStream is) + throws IOException { + try { + byte[] observed = new byte[expected.length]; + int count = is.read(observed); + assertEquals(expected.length, count); + assertEquals(-1, is.read()); + MoreAsserts.assertEquals(expected, observed); + } finally { + is.close(); + } + } +} ADDED sqlite3/src/main/java/org/sqlite/database/DatabaseUtils.java Index: sqlite3/src/main/java/org/sqlite/database/DatabaseUtils.java ================================================================== --- /dev/null +++ sqlite3/src/main/java/org/sqlite/database/DatabaseUtils.java @@ -0,0 +1,1461 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * 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.sqlite.database; + +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import org.sqlite.database.sqlite.SQLiteAbortException; +import org.sqlite.database.sqlite.SQLiteConstraintException; +import org.sqlite.database.sqlite.SQLiteDatabase; +import org.sqlite.database.sqlite.SQLiteDatabaseCorruptException; +import org.sqlite.database.sqlite.SQLiteDiskIOException; +import org.sqlite.database.sqlite.SQLiteException; +import org.sqlite.database.sqlite.SQLiteFullException; +import org.sqlite.database.sqlite.SQLiteProgram; +import org.sqlite.database.sqlite.SQLiteStatement; + +import android.database.CursorWindow; +import android.os.OperationCanceledException; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.text.TextUtils; +import android.util.Log; + +import android.database.Cursor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.text.Collator; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Static utility methods for dealing with databases and {@link Cursor}s. + */ +public class DatabaseUtils { + private static final String TAG = "DatabaseUtils"; + + private static final boolean DEBUG = false; + + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_SELECT = 1; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UPDATE = 2; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ATTACH = 3; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_BEGIN = 4; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_COMMIT = 5; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ABORT = 6; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_PRAGMA = 7; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_DDL = 8; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UNPREPARED = 9; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_OTHER = 99; + + /** + * Special function for writing an exception result at the header of + * a parcel, to be used when returning an exception from a transaction. + * exception will be re-thrown by the function in another process + * @param reply Parcel to write to + * @param e The Exception to be written. + * @see Parcel#writeNoException + * @see Parcel#writeException + */ + public static final void writeExceptionToParcel(Parcel reply, Exception e) { + int code = 0; + boolean logException = true; + if (e instanceof FileNotFoundException) { + code = 1; + logException = false; + } else if (e instanceof IllegalArgumentException) { + code = 2; + } else if (e instanceof UnsupportedOperationException) { + code = 3; + } else if (e instanceof SQLiteAbortException) { + code = 4; + } else if (e instanceof SQLiteConstraintException) { + code = 5; + } else if (e instanceof SQLiteDatabaseCorruptException) { + code = 6; + } else if (e instanceof SQLiteFullException) { + code = 7; + } else if (e instanceof SQLiteDiskIOException) { + code = 8; + } else if (e instanceof SQLiteException) { + code = 9; + } else if (e instanceof OperationApplicationException) { + code = 10; + } else if (e instanceof OperationCanceledException) { + code = 11; + logException = false; + } else { + reply.writeException(e); + Log.e(TAG, "Writing exception to parcel", e); + return; + } + reply.writeInt(code); + reply.writeString(e.getMessage()); + + if (logException) { + Log.e(TAG, "Writing exception to parcel", e); + } + } + + /** + * Special function for reading an exception result from the header of + * a parcel, to be used after receiving the result of a transaction. This + * will throw the exception for you if it had been written to the Parcel, + * otherwise return and let you read the normal result data from the Parcel. + * @param reply Parcel to read from + * @see Parcel#writeNoException + * @see Parcel#readException + */ +// public static final void readExceptionFromParcel(Parcel reply) { +// int code = reply.readExceptionCode(); +// if (code == 0) return; +// String msg = reply.readString(); +// DatabaseUtils.readExceptionFromParcel(reply, msg, code); +// } +// +// public static void readExceptionWithFileNotFoundExceptionFromParcel( +// Parcel reply) throws FileNotFoundException { +// int code = reply.readExceptionCode(); +// if (code == 0) return; +// String msg = reply.readString(); +// if (code == 1) { +// throw new FileNotFoundException(msg); +// } else { +// DatabaseUtils.readExceptionFromParcel(reply, msg, code); +// } +// } +// +// public static void readExceptionWithOperationApplicationExceptionFromParcel( +// Parcel reply) throws OperationApplicationException { +// int code = reply.readExceptionCode(); +// if (code == 0) return; +// String msg = reply.readString(); +// if (code == 10) { +// throw new OperationApplicationException(msg); +// } else { +// DatabaseUtils.readExceptionFromParcel(reply, msg, code); +// } +// } + + private static final void readExceptionFromParcel(Parcel reply, String msg, int code) { + switch (code) { + case 2: + throw new IllegalArgumentException(msg); + case 3: + throw new UnsupportedOperationException(msg); + case 4: + throw new SQLiteAbortException(msg); + case 5: + throw new SQLiteConstraintException(msg); + case 6: + throw new SQLiteDatabaseCorruptException(msg); + case 7: + throw new SQLiteFullException(msg); + case 8: + throw new SQLiteDiskIOException(msg); + case 9: + throw new SQLiteException(msg); + case 11: + throw new OperationCanceledException(msg); + default: + reply.readException(code, msg); + } + } + + /** + * Binds the given Object to the given SQLiteProgram using the proper + * typing. For example, bind numbers as longs/doubles, and everything else + * as a string by call toString() on it. + * + * @param prog the program to bind the object to + * @param index the 1-based index to bind at + * @param value the value to bind + */ + public static void bindObjectToProgram(SQLiteProgram prog, int index, + Object value) { + if (value == null) { + prog.bindNull(index); + } else if (value instanceof Double || value instanceof Float) { + prog.bindDouble(index, ((Number)value).doubleValue()); + } else if (value instanceof Number) { + prog.bindLong(index, ((Number)value).longValue()); + } else if (value instanceof Boolean) { + Boolean bool = (Boolean)value; + if (bool) { + prog.bindLong(index, 1); + } else { + prog.bindLong(index, 0); + } + } else if (value instanceof byte[]){ + prog.bindBlob(index, (byte[]) value); + } else { + prog.bindString(index, value.toString()); + } + } + + /** + * Returns data type of the given object's value. + *

    + * Returned values are + *

      + *
    • {@link Cursor#FIELD_TYPE_NULL}
    • + *
    • {@link Cursor#FIELD_TYPE_INTEGER}
    • + *
    • {@link Cursor#FIELD_TYPE_FLOAT}
    • + *
    • {@link Cursor#FIELD_TYPE_STRING}
    • + *
    • {@link Cursor#FIELD_TYPE_BLOB}
    • + *
    + *

    + * + * @param obj the object whose value type is to be returned + * @return object value type + * @hide + */ + public static int getTypeOfObject(Object obj) { + if (obj == null) { + return Cursor.FIELD_TYPE_NULL; + } else if (obj instanceof byte[]) { + return Cursor.FIELD_TYPE_BLOB; + } else if (obj instanceof Float || obj instanceof Double) { + return Cursor.FIELD_TYPE_FLOAT; + } else if (obj instanceof Long || obj instanceof Integer + || obj instanceof Short || obj instanceof Byte) { + return Cursor.FIELD_TYPE_INTEGER; + } else { + return Cursor.FIELD_TYPE_STRING; + } + } + + /** + * Fills the specified cursor window by iterating over the contents of the cursor. + * The window is filled until the cursor is exhausted or the window runs out + * of space. + * + * The original position of the cursor is left unchanged by this operation. + * + * @param cursor The cursor that contains the data to put in the window. + * @param position The start position for filling the window. + * @param window The window to fill. + * @hide + */ + public static void cursorFillWindow(final Cursor cursor, + int position, final CursorWindow window) { + if (position < 0 || position >= cursor.getCount()) { + return; + } + final int oldPos = cursor.getPosition(); + final int numColumns = cursor.getColumnCount(); + window.clear(); + window.setStartPosition(position); + window.setNumColumns(numColumns); + if (cursor.moveToPosition(position)) { + rowloop: do { + if (!window.allocRow()) { + break; + } + for (int i = 0; i < numColumns; i++) { + final int type = cursor.getType(i); + final boolean success; + switch (type) { + case Cursor.FIELD_TYPE_NULL: + success = window.putNull(position, i); + break; + + case Cursor.FIELD_TYPE_INTEGER: + success = window.putLong(cursor.getLong(i), position, i); + break; + + case Cursor.FIELD_TYPE_FLOAT: + success = window.putDouble(cursor.getDouble(i), position, i); + break; + + case Cursor.FIELD_TYPE_BLOB: { + final byte[] value = cursor.getBlob(i); + success = value != null ? window.putBlob(value, position, i) + : window.putNull(position, i); + break; + } + + default: // assume value is convertible to String + case Cursor.FIELD_TYPE_STRING: { + final String value = cursor.getString(i); + success = value != null ? window.putString(value, position, i) + : window.putNull(position, i); + break; + } + } + if (!success) { + window.freeLastRow(); + break rowloop; + } + } + position += 1; + } while (cursor.moveToNext()); + } + cursor.moveToPosition(oldPos); + } + + /** + * Appends an SQL string to the given StringBuilder, including the opening + * and closing single quotes. Any single quotes internal to sqlString will + * be escaped. + * + * This method is deprecated because we want to encourage everyone + * to use the "?" binding form. However, when implementing a + * ContentProvider, one may want to add WHERE clauses that were + * not provided by the caller. Since "?" is a positional form, + * using it in this case could break the caller because the + * indexes would be shifted to accomodate the ContentProvider's + * internal bindings. In that case, it may be necessary to + * construct a WHERE clause manually. This method is useful for + * those cases. + * + * @param sb the StringBuilder that the SQL string will be appended to + * @param sqlString the raw string to be appended, which may contain single + * quotes + */ + public static void appendEscapedSQLString(StringBuilder sb, String sqlString) { + sb.append('\''); + if (sqlString.indexOf('\'') != -1) { + int length = sqlString.length(); + for (int i = 0; i < length; i++) { + char c = sqlString.charAt(i); + if (c == '\'') { + sb.append('\''); + } + sb.append(c); + } + } else + sb.append(sqlString); + sb.append('\''); + } + + /** + * SQL-escape a string. + */ + public static String sqlEscapeString(String value) { + StringBuilder escaper = new StringBuilder(); + + DatabaseUtils.appendEscapedSQLString(escaper, value); + + return escaper.toString(); + } + + /** + * Appends an Object to an SQL string with the proper escaping, etc. + */ + public static final void appendValueToSql(StringBuilder sql, Object value) { + if (value == null) { + sql.append("NULL"); + } else if (value instanceof Boolean) { + Boolean bool = (Boolean)value; + if (bool) { + sql.append('1'); + } else { + sql.append('0'); + } + } else { + appendEscapedSQLString(sql, value.toString()); + } + } + + /** + * Concatenates two SQL WHERE clauses, handling empty or null values. + */ + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + /** + * return the collation key + * @param name + * @return the collation key + */ + public static String getCollationKey(String name) { + byte [] arr = getCollationKeyInBytes(name); + try { + return new String(arr, 0, getKeyLen(arr), "ISO8859_1"); + } catch (Exception ex) { + return ""; + } + } + + /** + * return the collation key in hex format + * @param name + * @return the collation key in hex format + */ + public static String getHexCollationKey(String name) { + byte[] arr = getCollationKeyInBytes(name); + char[] keys = encodeHex(arr); + return new String(keys, 0, getKeyLen(arr) * 2); + } + + + /** + * Used building output as Hex + */ + private static final char[] DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static char[] encodeHex(byte[] input) { + int l = input.length; + char[] out = new char[l << 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = DIGITS[(0xF0 & input[i]) >>> 4 ]; + out[j++] = DIGITS[ 0x0F & input[i] ]; + } + + return out; + } + + private static int getKeyLen(byte[] arr) { + if (arr[arr.length - 1] != 0) { + return arr.length; + } else { + // remove zero "termination" + return arr.length-1; + } + } + + private static byte[] getCollationKeyInBytes(String name) { + if (mColl == null) { + mColl = Collator.getInstance(); + mColl.setStrength(Collator.PRIMARY); + } + return mColl.getCollationKey(name).toByteArray(); + } + + private static Collator mColl = null; + /** + * Prints the contents of a Cursor to System.out. The position is restored + * after printing. + * + * @param cursor the cursor to print + */ + public static void dumpCursor(Cursor cursor) { + dumpCursor(cursor, System.out); + } + + /** + * Prints the contents of a Cursor to a PrintSteam. The position is restored + * after printing. + * + * @param cursor the cursor to print + * @param stream the stream to print to + */ + public static void dumpCursor(Cursor cursor, PrintStream stream) { + stream.println(">>>>> Dumping cursor " + cursor); + if (cursor != null) { + int startPos = cursor.getPosition(); + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + dumpCurrentRow(cursor, stream); + } + cursor.moveToPosition(startPos); + } + stream.println("<<<<<"); + } + + /** + * Prints the contents of a Cursor to a StringBuilder. The position + * is restored after printing. + * + * @param cursor the cursor to print + * @param sb the StringBuilder to print to + */ + public static void dumpCursor(Cursor cursor, StringBuilder sb) { + sb.append(">>>>> Dumping cursor " + cursor + "\n"); + if (cursor != null) { + int startPos = cursor.getPosition(); + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + dumpCurrentRow(cursor, sb); + } + cursor.moveToPosition(startPos); + } + sb.append("<<<<<\n"); + } + + /** + * Prints the contents of a Cursor to a String. The position is restored + * after printing. + * + * @param cursor the cursor to print + * @return a String that contains the dumped cursor + */ + public static String dumpCursorToString(Cursor cursor) { + StringBuilder sb = new StringBuilder(); + dumpCursor(cursor, sb); + return sb.toString(); + } + + /** + * Prints the contents of a Cursor's current row to System.out. + * + * @param cursor the cursor to print from + */ + public static void dumpCurrentRow(Cursor cursor) { + dumpCurrentRow(cursor, System.out); + } + + /** + * Prints the contents of a Cursor's current row to a PrintSteam. + * + * @param cursor the cursor to print + * @param stream the stream to print to + */ + public static void dumpCurrentRow(Cursor cursor, PrintStream stream) { + String[] cols = cursor.getColumnNames(); + stream.println("" + cursor.getPosition() + " {"); + int length = cols.length; + for (int i = 0; i< length; i++) { + String value; + try { + value = cursor.getString(i); + } catch (SQLiteException e) { + // assume that if the getString threw this exception then the column is not + // representable by a string, e.g. it is a BLOB. + value = ""; + } + stream.println(" " + cols[i] + '=' + value); + } + stream.println("}"); + } + + /** + * Prints the contents of a Cursor's current row to a StringBuilder. + * + * @param cursor the cursor to print + * @param sb the StringBuilder to print to + */ + public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) { + String[] cols = cursor.getColumnNames(); + sb.append("" + cursor.getPosition() + " {\n"); + int length = cols.length; + for (int i = 0; i < length; i++) { + String value; + try { + value = cursor.getString(i); + } catch (SQLiteException e) { + // assume that if the getString threw this exception then the column is not + // representable by a string, e.g. it is a BLOB. + value = ""; + } + sb.append(" " + cols[i] + '=' + value + "\n"); + } + sb.append("}\n"); + } + + /** + * Dump the contents of a Cursor's current row to a String. + * + * @param cursor the cursor to print + * @return a String that contains the dumped cursor row + */ + public static String dumpCurrentRowToString(Cursor cursor) { + StringBuilder sb = new StringBuilder(); + dumpCurrentRow(cursor, sb); + return sb.toString(); + } + + /** + * Reads a String out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorStringToContentValues(Cursor cursor, String field, + ContentValues values) { + cursorStringToContentValues(cursor, field, values, field); + } + + /** + * Reads a String out of a field in a Cursor and writes it to an InsertHelper. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param inserter The InsertHelper to bind into + * @param index the index of the bind entry in the InsertHelper + */ + public static void cursorStringToInsertHelper(Cursor cursor, String field, + InsertHelper inserter, int index) { + inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field))); + } + + /** + * Reads a String out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + * @param key The key to store the value with in the map + */ + public static void cursorStringToContentValues(Cursor cursor, String field, + ContentValues values, String key) { + values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field))); + } + + /** + * Reads an Integer out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) { + cursorIntToContentValues(cursor, field, values, field); + } + + /** + * Reads a Integer out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + * @param key The key to store the value with in the map + */ + public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values, + String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + values.put(key, cursor.getInt(colIndex)); + } else { + values.put(key, (Integer) null); + } + } + + /** + * Reads a Long out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values) + { + cursorLongToContentValues(cursor, field, values, field); + } + + /** + * Reads a Long out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into + * @param key The key to store the value with in the map + */ + public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values, + String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + Long value = Long.valueOf(cursor.getLong(colIndex)); + values.put(key, value); + } else { + values.put(key, (Long) null); + } + } + + /** + * Reads a Double out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The REAL field to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values) + { + cursorDoubleToContentValues(cursor, field, values, field); + } + + /** + * Reads a Double out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The REAL field to read + * @param values The {@link ContentValues} to put the value into + * @param key The key to store the value with in the map + */ + public static void cursorDoubleToContentValues(Cursor cursor, String field, + ContentValues values, String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + values.put(key, cursor.getDouble(colIndex)); + } else { + values.put(key, (Double) null); + } + } + + /** + * Read the entire contents of a cursor row and store them in a ContentValues. + * + * @param cursor the cursor to read from. + * @param values the {@link ContentValues} to put the row into. + */ + public static void cursorRowToContentValues(Cursor cursor, ContentValues values) { + String[] columns = cursor.getColumnNames(); + int length = columns.length; + for (int i = 0; i < length; i++) { + if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) { + values.put(columns[i], cursor.getBlob(i)); + } else { + values.put(columns[i], cursor.getString(i)); + } + } + } + + /** + * Picks a start position for {@link Cursor#fillWindow} such that the + * window will contain the requested row and a useful range of rows + * around it. + * + * When the data set is too large to fit in a cursor window, seeking the + * cursor can become a very expensive operation since we have to run the + * query again when we move outside the bounds of the current window. + * + * We try to choose a start position for the cursor window such that + * 1/3 of the window's capacity is used to hold rows before the requested + * position and 2/3 of the window's capacity is used to hold rows after the + * requested position. + * + * @param cursorPosition The row index of the row we want to get. + * @param cursorWindowCapacity The estimated number of rows that can fit in + * a cursor window, or 0 if unknown. + * @return The recommended start position, always less than or equal to + * the requested row. + * @hide + */ + public static int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @return the number of rows in the table + */ + public static long queryNumEntries(SQLiteDatabase db, String table) { + return queryNumEntries(db, table, null, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection) { + return queryNumEntries(db, table, selection, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @param selectionArgs You may include ?s in selection, + * which will be replaced by the values from selectionArgs, + * in order that they appear in the selection. + * The values will be bound as Strings. + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection, + String[] selectionArgs) { + String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : ""; + return longForQuery(db, "select count(*) from " + table + s, + selectionArgs); + } + + /** + * Query the table to check whether a table is empty or not + * @param db the database the table is in + * @param table the name of the table to query + * @return True if the table is empty + * @hide + */ + public static boolean queryIsEmpty(SQLiteDatabase db, String table) { + long isEmpty = longForQuery(db, "select exists(select 1 from " + table + ")", null); + return isEmpty == 0; + } + + /** + * Utility method to run the query on the db and return the value in the + * first column of the first row. + */ + public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return longForQuery(prog, selectionArgs); + } finally { + prog.close(); + } + } + + /** + * Utility method to run the pre-compiled query and return the value in the + * first column of the first row. + */ + public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForLong(); + } + + /** + * Utility method to run the query on the db and return the value in the + * first column of the first row. + */ + public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return stringForQuery(prog, selectionArgs); + } finally { + prog.close(); + } + } + + /** + * Utility method to run the pre-compiled query and return the value in the + * first column of the first row. + */ + public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForString(); + } + + /** + * Utility method to run the query on the db and return the blob value in the + * first column of the first row. + * + * @return A read-only file descriptor for a copy of the blob value. + */ + public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db, + String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return blobFileDescriptorForQuery(prog, selectionArgs); + } finally { + prog.close(); + } + } + + /** + * Utility method to run the pre-compiled query and return the blob value in the + * first column of the first row. + * + * @return A read-only file descriptor for a copy of the blob value. + */ + public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog, + String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForBlobFileDescriptor(); + } + + /** + * Reads a String out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getString(index)); + } + } + + /** + * Reads a Long out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getLong(index)); + } + } + + /** + * Reads a Short out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getShort(index)); + } + } + + /** + * Reads a Integer out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getInt(index)); + } + } + + /** + * Reads a Float out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getFloat(index)); + } + } + + /** + * Reads a Double out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getDouble(index)); + } + } + + /** + * This class allows users to do multiple inserts into a table using + * the same statement. + *

    + * This class is not thread-safe. + *

    + * + * @deprecated Use {@link SQLiteStatement} instead. + */ + @Deprecated + public static class InsertHelper { + private final SQLiteDatabase mDb; + private final String mTableName; + private HashMap mColumns; + private String mInsertSQL = null; + private SQLiteStatement mInsertStatement = null; + private SQLiteStatement mReplaceStatement = null; + private SQLiteStatement mPreparedStatement = null; + + /** + * {@hide} + * + * These are the columns returned by sqlite's "PRAGMA + * table_info(...)" command that we depend on. + */ + public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1; + + /** + * This field was accidentally exposed in earlier versions of the platform + * so we can hide it but we can't remove it. + * + * @hide + */ + public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4; + + /** + * @param db the SQLiteDatabase to insert into + * @param tableName the name of the table to insert into + */ + public InsertHelper(SQLiteDatabase db, String tableName) { + mDb = db; + mTableName = tableName; + } + + private void buildSQL() throws SQLException { + StringBuilder sb = new StringBuilder(128); + sb.append("INSERT INTO "); + sb.append(mTableName); + sb.append(" ("); + + StringBuilder sbv = new StringBuilder(128); + sbv.append("VALUES ("); + + int i = 1; + Cursor cur = null; + try { + cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null); + mColumns = new HashMap(cur.getCount()); + while (cur.moveToNext()) { + String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX); + String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX); + + mColumns.put(columnName, i); + sb.append("'"); + sb.append(columnName); + sb.append("'"); + + if (defaultValue == null) { + sbv.append("?"); + } else { + sbv.append("COALESCE(?, "); + sbv.append(defaultValue); + sbv.append(")"); + } + + sb.append(i == cur.getCount() ? ") " : ", "); + sbv.append(i == cur.getCount() ? ");" : ", "); + ++i; + } + } finally { + if (cur != null) cur.close(); + } + + sb.append(sbv); + + mInsertSQL = sb.toString(); + if (DEBUG) Log.v(TAG, "insert statement is " + mInsertSQL); + } + + private SQLiteStatement getStatement(boolean allowReplace) throws SQLException { + if (allowReplace) { + if (mReplaceStatement == null) { + if (mInsertSQL == null) buildSQL(); + // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead. + String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6); + mReplaceStatement = mDb.compileStatement(replaceSQL); + } + return mReplaceStatement; + } else { + if (mInsertStatement == null) { + if (mInsertSQL == null) buildSQL(); + mInsertStatement = mDb.compileStatement(mInsertSQL); + } + return mInsertStatement; + } + } + + /** + * Performs an insert, adding a new row with the given values. + * + * @param values the set of values with which to populate the + * new row + * @param allowReplace if true, the statement does "INSERT OR + * REPLACE" instead of "INSERT", silently deleting any + * previously existing rows that would cause a conflict + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + private long insertInternal(ContentValues values, boolean allowReplace) { + // Start a transaction even though we don't really need one. + // This is to help maintain compatibility with applications that + // access InsertHelper from multiple threads even though they never should have. + // The original code used to lock the InsertHelper itself which was prone + // to deadlocks. Starting a transaction achieves the same mutual exclusion + // effect as grabbing a lock but without the potential for deadlocks. + mDb.beginTransactionNonExclusive(); + try { + SQLiteStatement stmt = getStatement(allowReplace); + stmt.clearBindings(); + if (DEBUG) Log.v(TAG, "--- inserting in table " + mTableName); + for (Map.Entry e: values.valueSet()) { + final String key = e.getKey(); + int i = getColumnIndex(key); + DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue()); + if (DEBUG) { + Log.v(TAG, "binding " + e.getValue() + " to column " + + i + " (" + key + ")"); + } + } + long result = stmt.executeInsert(); + mDb.setTransactionSuccessful(); + return result; + } catch (SQLException e) { + Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e); + return -1; + } finally { + mDb.endTransaction(); + } + } + + /** + * Returns the index of the specified column. This is index is suitagble for use + * in calls to bind(). + * @param key the column name + * @return the index of the column + */ + public int getColumnIndex(String key) { + getStatement(false); + final Integer index = mColumns.get(key); + if (index == null) { + throw new IllegalArgumentException("column '" + key + "' is invalid"); + } + return index; + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, double value) { + mPreparedStatement.bindDouble(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, float value) { + mPreparedStatement.bindDouble(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, long value) { + mPreparedStatement.bindLong(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, int value) { + mPreparedStatement.bindLong(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, boolean value) { + mPreparedStatement.bindLong(index, value ? 1 : 0); + } + + /** + * Bind null to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + */ + public void bindNull(int index) { + mPreparedStatement.bindNull(index); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, byte[] value) { + if (value == null) { + mPreparedStatement.bindNull(index); + } else { + mPreparedStatement.bindBlob(index, value); + } + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, String value) { + if (value == null) { + mPreparedStatement.bindNull(index); + } else { + mPreparedStatement.bindString(index, value); + } + } + + /** + * Performs an insert, adding a new row with the given values. + * If the table contains conflicting rows, an error is + * returned. + * + * @param values the set of values with which to populate the + * new row + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long insert(ContentValues values) { + return insertInternal(values, false); + } + + /** + * Execute the previously prepared insert or replace using the bound values + * since the last call to prepareForInsert or prepareForReplace. + * + *

    Note that calling bind() and then execute() is not thread-safe. The only thread-safe + * way to use this class is to call insert() or replace(). + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long execute() { + if (mPreparedStatement == null) { + throw new IllegalStateException("you must prepare this inserter before calling " + + "execute"); + } + try { + if (DEBUG) Log.v(TAG, "--- doing insert or replace in table " + mTableName); + return mPreparedStatement.executeInsert(); + } catch (SQLException e) { + Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e); + return -1; + } finally { + // you can only call this once per prepare + mPreparedStatement = null; + } + } + + /** + * Prepare the InsertHelper for an insert. The pattern for this is: + *

      + *
    • prepareForInsert() + *
    • bind(index, value); + *
    • bind(index, value); + *
    • ... + *
    • bind(index, value); + *
    • execute(); + *
    + */ + public void prepareForInsert() { + mPreparedStatement = getStatement(false); + mPreparedStatement.clearBindings(); + } + + /** + * Prepare the InsertHelper for a replace. The pattern for this is: + *
      + *
    • prepareForReplace() + *
    • bind(index, value); + *
    • bind(index, value); + *
    • ... + *
    • bind(index, value); + *
    • execute(); + *
    + */ + public void prepareForReplace() { + mPreparedStatement = getStatement(true); + mPreparedStatement.clearBindings(); + } + + /** + * Performs an insert, adding a new row with the given values. + * If the table contains conflicting rows, they are deleted + * and replaced with the new row. + * + * @param values the set of values with which to populate the + * new row + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long replace(ContentValues values) { + return insertInternal(values, true); + } + + /** + * Close this object and release any resources associated with + * it. The behavior of calling insert() after + * calling this method is undefined. + */ + public void close() { + if (mInsertStatement != null) { + mInsertStatement.close(); + mInsertStatement = null; + } + if (mReplaceStatement != null) { + mReplaceStatement.close(); + mReplaceStatement = null; + } + mInsertSQL = null; + mColumns = null; + } + } + + /** + * Creates a db and populates it with the sql statements in sqlStatements. + * + * @param context the context to use to create the db + * @param dbName the name of the db to create + * @param dbVersion the version to set on the db + * @param sqlStatements the statements to use to populate the db. This should be a single string + * of the form returned by sqlite3's .dump command (statements separated by + * semicolons) + */ + static public void createDbFromSqlStatements( + Context context, String dbName, int dbVersion, String sqlStatements) { + + File f = context.getDatabasePath(dbName); + f.getParentFile().mkdirs(); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f, null); + + // TODO: this is not quite safe since it assumes that all semicolons at the end of a line + // terminate statements. It is possible that a text field contains ;\n. We will have to fix + // this if that turns out to be a problem. + String[] statements = TextUtils.split(sqlStatements, ";\n"); + for (String statement : statements) { + if (TextUtils.isEmpty(statement)) continue; + db.execSQL(statement); + } + db.setVersion(dbVersion); + db.close(); + } + + /** + * Returns one of the following which represent the type of the given SQL statement. + *
      + *
    1. {@link #STATEMENT_SELECT}
    2. + *
    3. {@link #STATEMENT_UPDATE}
    4. + *
    5. {@link #STATEMENT_ATTACH}
    6. + *
    7. {@link #STATEMENT_BEGIN}
    8. + *
    9. {@link #STATEMENT_COMMIT}
    10. + *
    11. {@link #STATEMENT_ABORT}
    12. + *
    13. {@link #STATEMENT_OTHER}
    14. + *
    + * @param sql the SQL statement whose type is returned by this method + * @return one of the values listed above + */ + public static int getSqlStatementType(String sql) { + sql = sql.trim(); + if (sql.length() < 3) { + return STATEMENT_OTHER; + } + String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT); + if (prefixSql.equals("SEL")) { + return STATEMENT_SELECT; + } else if (prefixSql.equals("INS") || + prefixSql.equals("UPD") || + prefixSql.equals("REP") || + prefixSql.equals("DEL")) { + return STATEMENT_UPDATE; + } else if (prefixSql.equals("ATT")) { + return STATEMENT_ATTACH; + } else if (prefixSql.equals("COM")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("END")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("ROL")) { + return STATEMENT_ABORT; + } else if (prefixSql.equals("BEG")) { + return STATEMENT_BEGIN; + } else if (prefixSql.equals("PRA")) { + return STATEMENT_PRAGMA; + } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || + prefixSql.equals("ALT")) { + return STATEMENT_DDL; + } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { + return STATEMENT_UNPREPARED; + } + return STATEMENT_OTHER; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } + + /** + * Returns column index of "_id" column, or -1 if not found. + * @hide + */ + public static int findRowIdColumnIndex(String[] columnNames) { + int length = columnNames.length; + for (int i = 0; i < length; i++) { + if (columnNames[i].equals("_id")) { + return i; + } + } + return -1; + } +} Index: sqlite3/src/main/jni/sqlite/sqlite3.c ================================================================== --- sqlite3/src/main/jni/sqlite/sqlite3.c +++ sqlite3/src/main/jni/sqlite/sqlite3.c @@ -943,13 +943,15 @@ ** ** Similar is true for Mac OS X. LFS is only supported on Mac OS X 9 and later. */ #ifndef SQLITE_DISABLE_LFS # define _LARGE_FILE 1 +#if 0 # ifndef _FILE_OFFSET_BITS # define _FILE_OFFSET_BITS 64 # endif +#endif # define _LARGEFILE_SOURCE 1 #endif /* The GCC_VERSION and MSVC_VERSION macros are used to ** conditionally include optimizations for each of these compilers. A Index: sqlite3test/build.gradle ================================================================== --- sqlite3test/build.gradle +++ sqlite3test/build.gradle @@ -1,10 +1,9 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion '25.0.0' defaultConfig { applicationId "org.sqlite.customsqlitetest" minSdkVersion 16 targetSdkVersion 23