0

I've completely given up on this, so if a moderator happens to come by, deleting would be great.
It doesn't hurt to leave this question up, but CoreData is so much nicer, you know?


I have a sqlite database for handling the contents of a table. It's great and everything (much easier than other options I looked at), but I'm having trouble with ints. The first time I go to edit an item after launching the app, the field for the int is empty. Re-entering works fine, it saves and appears in the table, but the next edit (without reopening the app) sets the second item's int to that of the first.

i.e., A(1) resets to A(0). I fix it (A(1)), but then B(2) becomes B(1) as soon as I load the edit view. Fix it (B(2)) or not (B(1)), C(3) will then have the same (#) as B.

I still can't figure out what's causing it. Changing the int to a string (edit database column and every relevant file in the app) would certainly work, but that's a whole lot of unnecessary work just to make it slower and easier to break.

edit:

CREATE TABLE "items" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" VARCHAR, "need" INTEGER DEFAULT 0, "notes" TEXT)

- (void)updateItemAtIndexPath:(NSIndexPath *)path {
    Item *i = (Item *)[items objectAtIndex:path.row];
    int ret;
    const char *sql = "update items set name = ?, need = ?, notes = ? where id = ?;";

    if (!updStmt) { // build update statement
        if ((ret = sqlite3_prepare_v2(database, sql, -1, &updStmt, NULL)) != SQLITE_OK) {
            NSAssert1(0, @"Error building statement to update items [%s]", sqlite3_errmsg(database));
        }
    }

    // bind values to statement
    NSString *s = i.name;
    if (s == NULL) s = @"";
    sqlite3_bind_text(updStmt, 1, [s UTF8String], -1, SQLITE_TRANSIENT);
    NSInteger n = i.need;
    sqlite3_bind_int(updStmt, 2, n);
    s = i.notes;
    if (s == NULL) s = @"";
    sqlite3_bind_text(updStmt, 3, [s UTF8String], -1, SQLITE_TRANSIENT);
    n = i.itemid;
    sqlite3_bind_int(updStmt, 4, n);

    // now execute sql statement
    if (sqlite3_step(updStmt) != SQLITE_DONE) {
        NSAssert1(0, @"Error updating values [%s]", sqlite3_errmsg(database));
    }

    // now reset bound statement to original state
    sqlite3_reset(updStmt);
}

- (void)insertItem:(Item *)item {
    int ret;
    const char *sql = "insert into items (name, need, notes) values (?, ?, ?);";

    if (!insStmt) { // first insert - build statement
        if ((ret = sqlite3_prepare_v2(database, sql, -1, &insStmt, NULL)) != SQLITE_OK) {
            NSAssert1(0, @"Error building statement to insert item [%s]", sqlite3_errmsg(database));
        }
    }

    // bind values
    NSString *s = item.name;
    if (s == NULL) s = @"";
    sqlite3_bind_text(insStmt, 1, [s UTF8String], -1, SQLITE_TRANSIENT);
    NSInteger n = item.need;
    sqlite3_bind_int(insStmt, 2, n);
    s = item.notes;
    if (s == NULL) s = @"";
    sqlite3_bind_text(insStmt, 3, [s UTF8String], -1, SQLITE_TRANSIENT);

    // execute sql statement
    if (sqlite3_step(insStmt) != SQLITE_DONE) {
        NSAssert1(0, @"Error inserting item [%s]", sqlite3_errmsg(database));
    }

    // reset bound statement to original state
    sqlite3_reset(insStmt);

    [self readItems]; // refresh array
}
Thromordyn
  • 1,581
  • 4
  • 17
  • 45
  • Some code might be required to tackle this, as well as a table definition. Issue might not be in SQLite per se. – MPelletier Mar 10 '11 at 15:52
  • I kinda figured, but I don't know what to put up. Pasting everything in seems kinda silly, but maybe I should... – Thromordyn Mar 10 '11 at 15:55
  • Start with the table definition and your update routine. – MPelletier Mar 10 '11 at 15:55
  • I think both your update and insert routines might be required. – MPelletier Mar 10 '11 at 16:06
  • Did you take into account that binding to columns use 1 based indexes and reading from columns use 0 based indexes? (May be the other way round, I'm not sure anymore) – Alfonso Mar 10 '11 at 16:23
  • @frenetisch That would explain things... What I don't get is why use binding in the first place. – MPelletier Mar 10 '11 at 16:42
  • I was following a tutorial without much knowledge of any variety of SQL. (I know more now, but not nearly enough. Bleh.) I'll try MP's suggestion as soon as I can figure out how. – Thromordyn Mar 10 '11 at 16:45
  • @MPelletier: Bindings provide a way to 1) reuse a statement (not needed in the above code) and 2) to validate the inputs (most probably needed). Therefore I would stick with the bindings. – Alfonso Mar 10 '11 at 17:32
  • Also, sanity check: create two items by hand and force their insertion in the database with your function, see what's present in sqlite. Yes, that will require you to refactor some code. You're better off sending a ready `item` to your function than a path... – MPelletier Mar 22 '11 at 12:36
  • Only masochists use the SQLite C API directly in Objective-C. [Use FMDB](http://github.com/ccgus/fmdb) (a SQLite wrapper) or [CoreData](http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreData/cdProgrammingGuide.html) (an object graph manager) instead. – Dave DeLong Apr 15 '11 at 15:11
  • Call me what you will; my skill lies in expansion, not creation. (That is to say, it's easier to make a simple SQLite app into something useful than it is to start a new project on my own, regardless of how much (or how little) I want to do with it.) // Core Data is certainly useful, though. I'm attempting to learn how to use it now. – Thromordyn Apr 15 '11 at 15:21

1 Answers1

1

Instead of using sqlite3_bind_text and sqlite3_bind_int, I would try to construct the query string from the various values and use sqlite3_exec to run it. Let's call that a tryout towards a solution.

Example (warning, untested!!):

- (void)updateItemAtIndexPath:(NSIndexPath *)path {
    Item *i = (Item *)[items objectAtIndex:path.row];

    // validate values
    NSString *name = i.name;
    if (name == NULL) name = @"";
    [name stringByReplacingOccurrencesOfString:@"'"
                                    withString:@"''"];
    NSInteger need = i.need;
    NSString *notes = i.notes;
    if (notes == NULL) notes = @"";
    [notes stringByReplacingOccurrencesOfString:@"'"
                                     withString:@"''"];
    NSInteger itemid = i.itemid;

    NSString *sql = [NSString stringWithFormat: 
                    @"update items set name = '%@', need = %@, notes = '%@' where id = %@;", 
                    name, need, notes, itemid];

    // now execute sql statement
    if (sqlite3_exec(database, [sql UTF8String], NULL, NULL, NULL) != SQLITE_DONE) {
        NSAssert1(0, @"Error updating values [%s]", sqlite3_errmsg(database));
    }
}
MPelletier
  • 16,256
  • 15
  • 86
  • 137
  • I don't actually know how to do that off the top of my head... Time for more Google abuse! (Maybe someone could throw something at me? My Google Fu does not exist.) – Thromordyn Mar 10 '11 at 16:31
  • This I wouldn't do unless you know exactly what's in that items name. Otherwise a misplaced ' will screw you badly. – Alfonso Mar 10 '11 at 17:30
  • @frenetisch: Excellent point, always sanitize strings. Forgot that here. – MPelletier Mar 10 '11 at 17:40
  • That does look quite a bit cleaner, but there are 2 errors and 1 warning. (baseString undeclared ; Too few arguments to function 'sqlite3_exec' ; Passing argument 1 of 'sqlite3_exec' from incompatible pointer type (I assume this warning is a result of the first error)) // Is there a good sqlite3 book I could get? Something not all full of errors (Cocos2d for iPhone 0.99 (Pablo Ruiz)) or obsolete (iPhone SDK Development (Dudney Adamson)) would certainly be preferable. – Thromordyn Mar 11 '11 at 14:06
  • Sorry @Thromordyn, the syntax I used for `sqlite3_exec` is all wrong. Gonna fix it. In the mean time, [documentation](http://www.sqlite.org/c3ref/exec.html). There are lots on the sqlite site, all up to date. – MPelletier Mar 11 '11 at 15:00
  • And like frenetisch said, my method is flawed because it doesn't check for single quotes, so you'll need to check for that and replace them, i.e. sanitize. – MPelletier Mar 11 '11 at 15:02
  • @Thromordyn: Tweaked a bit, added sanitizing. Untested, I don't have xcode on hand. – MPelletier Mar 11 '11 at 15:15
  • @Thromordyn: Fixed the formating on `sql` string. That oughta fix the "baseString undeclared" warning. I thought that was a system constant, not an example of a variable name. Sorry, my objective-c fu is weak. – MPelletier Mar 11 '11 at 15:46
  • @MPelletier: It's quite alright. Late response due to installation of Xcode 4. (It's awesome, by the way. The new live error/warning thing is my very best friend.) // And the code is... still with one warning. "Incompatible pointer types passing 'NSString *' to parameter of type 'const char *'" it says. [Searching for a fix...] – Thromordyn Mar 11 '11 at 17:17
  • @Thromordyn: how about `NString * const finalsql = sql;` Then use `finalsql` in `sqlite3_exec`? – MPelletier Mar 11 '11 at 17:59
  • Look at [this](http://stackoverflow.com/questions/753755/using-a-constant-nsstring-as-the-key-for-nsuserdefaults). – MPelletier Mar 11 '11 at 18:04
  • Essentially the exact same warning. With that edit, it's `'NSString *const'` instead of `'NSString *'` – Thromordyn Mar 14 '11 at 13:06
  • @Thromordyn: Didn't think of this for all weekend, and I've got no XCode on hand. I hacked together a fix, basically casting `NSString` into `const char*`. There definitely is something simpler for this. – MPelletier Mar 14 '11 at 13:40
  • There probably is. I think my best bet might be a new book. (Nothing current with consistently good reviews, though. Hmm...) – Thromordyn Mar 15 '11 at 13:09
  • @Thromordyn: Did you try the latest changes? – MPelletier Mar 15 '11 at 13:20
  • @MPelletier I think I misread your comment. All I'm getting is another `Incompatible pointer types` warning. – Thromordyn Mar 15 '11 at 13:33
  • @Thromordyn My latest version from yesterday is in the answer, please forget the comment. – MPelletier Mar 15 '11 at 13:37
  • @MPelletier Ah, alright. The best (read: only useful) book for sqlite I can find has a negative review mentioning excessive theory and not enough examples, so I think I'll pass. (Also it's O'Reilly, and from what I've seen, they kinda suck at what they do. Just a bit.) // So... What now? There must be someone who knows sqlite well enough to help... – Thromordyn Mar 15 '11 at 13:57
  • @Thromordyn: At this point it's mostly an objective-c problem, no? – MPelletier Mar 15 '11 at 14:13
  • @MPelletier Nothing new. `NSString` and `const char` can't be thrown around as though they're the same thing, surprisingly enough. // I think I read something about casting `NSString` to `const char` at some point... Maybe that's what I want? – Thromordyn Mar 15 '11 at 14:30
  • @Thromordyn: Found an example, some uses of `sqlite3_exec` in there. See if you get similar mileage: http://www.switchonthecode.com/tutorials/using-sqlite-on-the-iphone – MPelletier Mar 15 '11 at 14:46
  • @MPelletier It looks like a somewhat useful resource, but I don't think I can deconstruct and use it. Bookmarked anyway, just in case. – Thromordyn Mar 15 '11 at 15:15
  • @Thromordyn I found these tutorials: http://www.icodeblog.com/category/tutorials/?s=sqlite – MPelletier Mar 16 '11 at 03:08
  • @MPelletier I'll take a look. iCB's SQLite tutorial series broke badly for me (still no idea how), but I'll give it a shot. Maybe I'll somehow find what's causing the int to be handled improperly. Wouldn't that be nice... – Thromordyn Mar 16 '11 at 03:22
  • @MPelletier A-ha! I found it! [This post](http://stackoverflow.com/questions/3731894/iphone-development-sqlite3-bind-int-not-working) has an answer by Toro, and that removed both warnings from my project. Instead of `const char *finalsql = sql;` it should be `const char *finalsql = [sql UTF8String];`. – Thromordyn Mar 17 '11 at 13:04
  • Still not quite there. Now I'm getting `EXC_BAD_ACCESS` on the insert statement. It's probably completely wrong, though, so I'll go hunting again. // Edit: Same on update. This is not working. – Thromordyn Mar 17 '11 at 13:51
  • It's a bit faster now (tweaked some things here and there) but the "need" column is still giving me trouble. Maybe I should just go and make it a string... It seems like I'd be cheating, but at least it'd be harder to break, right? If nobody can come up with anything by the end of the month, I'll do it. – Thromordyn Mar 21 '11 at 14:29
  • @Thromordyn I'm not done with this yet. I'm slowly learning objective-c to nail this question. Please bear with me :) – MPelletier Mar 21 '11 at 14:33
  • @Thromordyn: By the way, maybe you should look into CoreData before changing that int to string. – MPelletier Mar 22 '11 at 11:17
  • @MPelletier I'll go ahead and do that if I can find a good resource for it. The only thing I've found so far was a really bad tutorial. CoreData is a little intimidating, but so was sqlite before I figured it out, so maybe that would be a good direction to head in. – Thromordyn Mar 22 '11 at 12:34
  • Just got a really nice iOS database programming book. 48 pages in (most of which have some code) and there isn't a single warning. Link to said book on amazon [here](http://www.amazon.com/gp/product/0470636173/), for anyone interested. This is so much better than what I've been doing... – Thromordyn Mar 22 '11 at 18:06
  • I think I'll just rework another app I wrote to do this. Kinda defeats the purpose of asking and waiting for an answer, but Core Data instead of direct SQLite will probably be a nice change of pace. – Thromordyn Apr 12 '11 at 13:47
  • Alright. Sometimes, letting go is difficult, I know :) I had completely forgotten about this, and did not advance the solution further, sorry. – MPelletier Apr 12 '11 at 13:49