Your issue is not the maximum size of a BLOB, rather it is the maximum size of a Cursor Window, which is specific to the Android SQLite API/implementation and the limit varies from 1Mb to 4Mb (if I recall correctly) depending upon the version of Android (later Versions having the larger size).
The Cursor Window is wrapped within the Cursor implementation which for Room is wrapped inside the convenience methods, so determining the size, although probably possible would likely be a difficult/complex task.
It is possible to get around the issue, which is that the Cursor Window MUST be able to store a full row (so other columns impact upon how big the Blob itself can be). You can extract parts (chunks) of the Blob and then combine them. Using chunks that are less than 1Mb (probably much less) would cover all limitations.
- However, for such large amounts of data, it is recommended to store the data as a file and then store enough of the file path to be able to retrieve the file.
Here's two answers (not using Room though) that retrieve data from a Blob that is greater than the Cursor Window limit.
Do you know how I can determine the limit size for a particular version of Android (or at runtime for the version my app is on)? I found config_cursorWindowSize = 2048 in AOSP code for Android 12 (which is what I'm testing on at the moment), but that's inconsistent with the fact that I've had queries that worked up to about ~4MB per blob (or really, per row, to your point).
The following, from limited testing, (API 28 and 31) is one way BUT I suspect that this may alter (both show 2MB i.e. 2097152) depending upon other uses as CursorWindow can be constructed using a constructor where the windowSize is passed.
Here's the basic/limited java method which
- creates a database and a table with just the 1 column into which an oversized BLOB (4Mb) is inserted as the only row,
- then progressively tries to extract a smaller part (all at first) of the BLOB until the data can be extracted without the exception
- finally it clears up the database
:-
long checkCursorWindowSize(Context context) {
final String TEST_TABLE_NAME = "tcw";
final String TEST_DATABASE_NAME = "tcw";
final String TEST_COLUMN_NAME = "testblob";
final int reducer = 1024 * 1024 / 16; /* size to reduce BLOB by per iteration */
final int allowance = 1; /* size to reduce BLOB by for all extractions (reason for use probably best to not be on a power of 2 boundary????) */
byte[] ba = new byte[1024 * 1024 * 4]; /* initial size of BLOB when storing*/
Cursor csr;
String tcwDbPath = context.getDatabasePath(TEST_DATABASE_NAME).getPath();
File dbFile = context.getDatabasePath(TEST_DATABASE_NAME);
File dbDirectory = dbFile.getParentFile();
/* If the database exists then delete it = should never exist */
if (dbFile.exists()) {
dbFile.delete();
}
/* Ensure that the databases directory exists - otherwise openOrCreate fails */
if (!dbDirectory.exists()) {
Log.d("DEBUGINFO", "Database directory " + dbDirectory.getPath() + " does not exist - Creating");
dbDirectory.mkdirs();
}
/* Finally create the database */
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(tcwDbPath,null,null);
/* Do everything inside a transaction to reduce writes */
db.beginTransaction();
/* Create the table into which the BLOB will be loaded */
db.execSQL("CREATE TABLE IF NOT EXISTS " + TEST_TABLE_NAME + " (" + TEST_COLUMN_NAME + " BLOB)");
/* Insert the over-sized BLOB */
ContentValues cv = new ContentValues();
cv.put(TEST_COLUMN_NAME,ba);
db.insert(TEST_TABLE_NAME,null,cv);
/* Prepare to repeatedly extract an eve decreasing part of the blob */
int retrieveSize = ba.length; /* first try to retrieve entire BLOB */
int caught = 0; /* the number of caught exceptions */
/* Try 1024 attempts at getting smaller BLOB */
for (int i=0; i <= 1024; i++ ) {
Log.d("DEBUG","Iteration is " + (i + 1) + ". Retrieve Size is " + retrieveSize);
try {
/* Build the query to extract part of the blob (using the SQLite substr function) according to
the current retrieve size less the allowance
only getting 1 row (should only be 1 row)
NOTE!! retrieving an oversize BLOB does not fail here, rather the failure is when attempting to read the data (getBlob method)
*/
csr = db.query(TEST_TABLE_NAME, new String[]{"substr(" + TEST_COLUMN_NAME + ",1," + (retrieveSize - allowance) + ") AS " + TEST_COLUMN_NAME},null,null,null,null,null,"1");
while (csr.moveToNext()) {
byte[] test = csr.getBlob(0); /* get the Blob from the 1st (only) column */
}
break; /* if no Exception then all done */
} catch (SQLException e) {
retrieveSize = retrieveSize - reducer; /* reduce the retrieveSize ready for the next iteration (if one )*/
caught++;
//e.printStackTrace();
}
}
Log.d("DEBUGINFO","Caught " + caught + " SQlite Exceptions");
/* Done with the database so clean up */
db.endTransaction();
db.close();
new File(tcwDbPath).delete();
/* adjust the retrieve size as it has been adjusted in preparation for the next iteration */
return retrieveSize + reducer;
}
The limited testing used the following in MainActivity :-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("DEBUGINFO","MaxCursorSize = " + checkCursorWindowSize(this));
}
On API 28 :- D/DEBUGINFO: MaxCursorSize = 2097152
With the whole log being:-
2022-08-29 10:58:06.798 D/DEBUG: Iteration is 16. Retrieve Size is 3211264
2022-08-29 10:58:06.802 W/CursorWindow: Window is full: requested allocation 3211263 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.802 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,3211263) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.802 D/DEBUG: Iteration is 17. Retrieve Size is 3145728
2022-08-29 10:58:06.806 W/CursorWindow: Window is full: requested allocation 3145727 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.807 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,3145727) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.807 D/DEBUG: Iteration is 18. Retrieve Size is 3080192
2022-08-29 10:58:06.810 W/CursorWindow: Window is full: requested allocation 3080191 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.810 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,3080191) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.810 D/DEBUG: Iteration is 19. Retrieve Size is 3014656
2022-08-29 10:58:06.813 W/CursorWindow: Window is full: requested allocation 3014655 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.813 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,3014655) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.813 D/DEBUG: Iteration is 20. Retrieve Size is 2949120
2022-08-29 10:58:06.818 W/CursorWindow: Window is full: requested allocation 2949119 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.819 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2949119) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.819 D/DEBUG: Iteration is 21. Retrieve Size is 2883584
2022-08-29 10:58:06.824 W/CursorWindow: Window is full: requested allocation 2883583 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.824 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2883583) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.825 D/DEBUG: Iteration is 22. Retrieve Size is 2818048
2022-08-29 10:58:06.829 W/CursorWindow: Window is full: requested allocation 2818047 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.829 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2818047) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.829 D/DEBUG: Iteration is 23. Retrieve Size is 2752512
2022-08-29 10:58:06.832 W/CursorWindow: Window is full: requested allocation 2752511 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.832 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2752511) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.832 D/DEBUG: Iteration is 24. Retrieve Size is 2686976
2022-08-29 10:58:06.837 W/CursorWindow: Window is full: requested allocation 2686975 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.838 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2686975) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.838 D/DEBUG: Iteration is 25. Retrieve Size is 2621440
2022-08-29 10:58:06.842 W/CursorWindow: Window is full: requested allocation 2621439 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.842 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2621439) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.842 D/DEBUG: Iteration is 26. Retrieve Size is 2555904
2022-08-29 10:58:06.846 W/CursorWindow: Window is full: requested allocation 2555903 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.846 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2555903) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.846 D/DEBUG: Iteration is 27. Retrieve Size is 2490368
2022-08-29 10:58:06.849 W/CursorWindow: Window is full: requested allocation 2490367 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.849 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2490367) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.849 D/DEBUG: Iteration is 28. Retrieve Size is 2424832
2022-08-29 10:58:06.853 W/CursorWindow: Window is full: requested allocation 2424831 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.853 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2424831) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.854 D/DEBUG: Iteration is 29. Retrieve Size is 2359296
2022-08-29 10:58:06.857 W/CursorWindow: Window is full: requested allocation 2359295 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.858 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2359295) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.858 D/DEBUG: Iteration is 30. Retrieve Size is 2293760
2022-08-29 10:58:06.862 W/CursorWindow: Window is full: requested allocation 2293759 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.862 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2293759) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.862 D/DEBUG: Iteration is 31. Retrieve Size is 2228224
2022-08-29 10:58:06.865 W/CursorWindow: Window is full: requested allocation 2228223 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.865 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2228223) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.865 D/DEBUG: Iteration is 32. Retrieve Size is 2162688
2022-08-29 10:58:06.870 W/CursorWindow: Window is full: requested allocation 2162687 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.870 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2162687) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.870 D/DEBUG: Iteration is 33. Retrieve Size is 2097152
2022-08-29 10:58:06.874 W/CursorWindow: Window is full: requested allocation 2097151 bytes, free space 2096720 bytes, window size 2097152 bytes
2022-08-29 10:58:06.875 E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=1; query: SELECT substr(testblob,1,2097151) AS testblob FROM tcw LIMIT 1
2022-08-29 10:58:06.875 D/DEBUG: Iteration is 34. Retrieve Size is 2031616
2022-08-29 10:58:06.881 D/DEBUGINFO: Caught 33 SQlite Exceptions
2022-08-29 10:58:06.904 D/DEBUGINFO: MaxCursorSize = 2097152
On API 31 the result is exactly the same bar the time/timings i.e. MaxCursorSize is calculated returned as 2097512
However, add another column for example and then other data has to be loaded into the CursorWindow and the result is then not a true reflection of how big the Blob can be.
- hence why it could well just lead to issues if any reliance were made on the limit of what a CursorWindow can hold in total.