Thursday, September 15, 2011

Compiling Your Own Version of SQLite for iOS and applying some fixes

Adding your own version of sqlite for iOS is fairly easy task.
  1. Go to http://www.sqlite.org/ and download the latest sqlite amalgamation archive
  2. Extract it somewhere and add it to your Xcode project. You have several options here. You could copy it to your project folder, or link to it. You could also decide, to put it as a static library target and link it to your main target. It's up to you. You will need only sqlite.c and sqlite.h files.
  3. Apply some #defines you might need. For example:
#define SQLITE_ENABLE_FTS3
#define SQLITE_DISABLE_LFS
#define SQLITE_THREADSAFE 0
Then you can compile!

If you need more detail about this part of the process, you can find more detailed description here and here.

And then I hit a problem. I was testing on different simulator versions and found out that for some reason SQLite functions returned strange errors ( SQLITE_CORRUPT with number code 11 ) with some versions of the simulator, while with others, everything worked perfectly fine. After hours of debugging, I've found that the problem is in wrong file size, returned by a function called unixFileSize. Here is how it looks in my sqlite version:
/*
** Determine the current size of a file in bytes
*/
static int unixFileSize(sqlite3_file *id, i64 *pSize){
  int rc;
  struct stat buf;
  assert( id );
  rc = osFstat(((unixFile*)id)->h, &buf);
  SimulateIOError( rc=1 );
  if( rc!=0 ){
    ((unixFile*)id)->lastErrno = errno;
    return SQLITE_IOERR_FSTAT;
  }
  *pSize = buf.st_size;

  /* When opening a zero-size database, the findInodeInfo() procedure
  ** writes a single byte into that file in order to work around a bug
  ** in the OS-X msdos filesystem.  In order to avoid problems with upper
  ** layers, we need to report this file size as zero even though it is
  ** really 1.   Ticket #3260.
  */
  if( *pSize==1 ) *pSize = 0;


  return SQLITE_OK;
}
Here after a successful call to osFstat, buf.st_size contained wrong file size. This is clearly not a bug in SQLite. There might be several reasons for this to happen. Probably struct stat is different in terms of packaging or field sizes from the one compiled with some of the simulators. I don't know and since I cannot fix it in iOS Simulator (the power of closed source in action), I don't really care. What I can fix, is not to use fstat() and struct stat in this function. Here is how I changed the function using other Unix ways and do the job:
/*
** Determine the current size of a file in bytes
*/
static int unixFileSize(sqlite3_file *id, i64 *pSize){
  off_t pos, size;
  pos = lseek(((unixFile*)id)->h, 0L, SEEK_CUR);
  if (-1 == pos){
    ((unixFile*)id)->lastErrno = errno;
    return SQLITE_IOERR_SEEK;
  }
  size = lseek(((unixFile*)id)->h, 0L, SEEK_END);
  if (-1 == size) {
    ((unixFile*)id)->lastErrno = errno;
    return SQLITE_IOERR_SEEK;
  }
  if (-1 == lseek(((unixFile*)id)->h, pos, SEEK_SET)) {
    ((unixFile*)id)->lastErrno = errno;
    return SQLITE_IOERR_SEEK;
  }
  *pSize = size;

  /* When opening a zero-size database, the findInodeInfo() procedure
  ** writes a single byte into that file in order to work around a bug
  ** in the OS-X msdos filesystem.  In order to avoid problems with upper
  ** layers, we need to report this file size as zero even though it is
  ** really 1.   Ticket #3260.
  */
  if( *pSize==1 ) *pSize = 0;


  return SQLITE_OK;
}
As you can see I'm using lseek to get the file size and thus avoid the struct stat and fstat() usage. This fixed the problem for me and I hope this could spare some time to others too. Note that this fix is not portable and will not work in non Unix environment.