sqlite techniques

143
SQLite Techniques Ben Scheirman @subdigital Code / Slides: github.com/subdigital/iphonedevcon- sandiego Tuesday, September 28, 2010

Upload: joaopmaia

Post on 15-May-2015

1.822 views

Category:

Technology


1 download

DESCRIPTION

Presentation by Ben Scheirman give at the september meeting of the Houston iPhone Developers Meetup (http://houstoniphone.com)

TRANSCRIPT

Page 1: SQLite Techniques

SQLite Techniques

Ben Scheirman@subdigital

Code / Slides: github.com/subdigital/iphonedevcon-

sandiego

Tuesday, September 28, 2010

Page 2: SQLite Techniques

SQLite is....

Single-user

File-based

Simple

Cross Platform

A subset of full ANSI SQL

Tuesday, September 28, 2010

Page 3: SQLite Techniques

SQLite API

Pure C API

Lots of strange-looking, hard to debug code

Best choice is to abstract it away

Tuesday, September 28, 2010

Page 4: SQLite Techniques

SQLite Tools

SQLite Manager Add-in for Firefox

sqlite3 command line utility

Tuesday, September 28, 2010

Page 5: SQLite Techniques

Writable Databases

Your app bundle is signed!

(That means the contents can't change)

Tuesday, September 28, 2010

Page 6: SQLite Techniques

Your App Bundle

Resources Documents

Your App Sandbox

Tuesday, September 28, 2010

Page 7: SQLite Techniques

Your App Bundle

Resources Documents

Your App Sandbox

Tuesday, September 28, 2010

Page 8: SQLite Techniques

Creating the database

Tuesday, September 28, 2010

Page 9: SQLite Techniques

Getting Resource Paths

Tuesday, September 28, 2010

Page 10: SQLite Techniques

Getting Resource Paths

//fetches  path  for  foo.db

Tuesday, September 28, 2010

Page 11: SQLite Techniques

Getting Resource Paths

//fetches  path  for  foo.dbNSString  *resourcePath  =  [[NSBundle  mainBundle]

Tuesday, September 28, 2010

Page 12: SQLite Techniques

Getting Resource Paths

//fetches  path  for  foo.dbNSString  *resourcePath  =  [[NSBundle  mainBundle]                                                          pathForResource:@"foo"

Tuesday, September 28, 2010

Page 13: SQLite Techniques

Getting Resource Paths

//fetches  path  for  foo.dbNSString  *resourcePath  =  [[NSBundle  mainBundle]                                                          pathForResource:@"foo"                                                          ofType:@"db"];

Tuesday, September 28, 2010

Page 14: SQLite Techniques

Finding the Documents Directory

Tuesday, September 28, 2010

Page 15: SQLite Techniques

Finding the Documents Directory

NSArray  *paths  =  NSSearchPathForDirectoriesInDomains(  

Tuesday, September 28, 2010

Page 16: SQLite Techniques

Finding the Documents Directory

NSArray  *paths  =  NSSearchPathForDirectoriesInDomains(                                                        NSDocumentDirectory,  

Tuesday, September 28, 2010

Page 17: SQLite Techniques

Finding the Documents Directory

NSArray  *paths  =  NSSearchPathForDirectoriesInDomains(                                                        NSDocumentDirectory,                                                        NSUserDomainMask,  

Tuesday, September 28, 2010

Page 18: SQLite Techniques

Finding the Documents Directory

NSArray  *paths  =  NSSearchPathForDirectoriesInDomains(                                                        NSDocumentDirectory,                                                        NSUserDomainMask,                                                        YES);

Tuesday, September 28, 2010

Page 19: SQLite Techniques

Finding the Documents Directory

NSArray  *paths  =  NSSearchPathForDirectoriesInDomains(                                                        NSDocumentDirectory,                                                        NSUserDomainMask,                                                        YES);

NSString  *documentsDirectory  =  [paths  objectAtIndex:0];

Tuesday, September 28, 2010

Page 20: SQLite Techniques

Copy a default database on startup

Tuesday, September 28, 2010

Page 21: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 22: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 23: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 24: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 25: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 26: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 27: SQLite Techniques

Copy a default database on startup

//in applicationDidFinishLaunching

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"app" ofType:@"db"];

NSString *targetPath = ... NSFileManager *fm = [NSFileManager defaultManager]; if(![fm fileExistsAtPath:targetPath]) { [fm copyItemAtPath:sourcePath toPath:targetPath error:&error]; }

Tuesday, September 28, 2010

Page 28: SQLite Techniques

SQLite API Basics

Tuesday, September 28, 2010

Page 29: SQLite Techniques

Add the Framework

Tuesday, September 28, 2010

Page 30: SQLite Techniques

Add the Framework

Tuesday, September 28, 2010

Page 31: SQLite Techniques

Add the Framework

Tuesday, September 28, 2010

Page 32: SQLite Techniques

Add the Framework

Tuesday, September 28, 2010

Page 33: SQLite Techniques

SQLite API - Opening a connection

Tuesday, September 28, 2010

Page 34: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

Tuesday, September 28, 2010

Page 35: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

Tuesday, September 28, 2010

Page 36: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

Tuesday, September 28, 2010

Page 37: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) {

Tuesday, September 28, 2010

Page 38: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) { [NSException raise:@"SQLITE ERROR"

Tuesday, September 28, 2010

Page 39: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) { [NSException raise:@"SQLITE ERROR" format:@"Error %d", result];

Tuesday, September 28, 2010

Page 40: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) { [NSException raise:@"SQLITE ERROR" format:@"Error %d", result];}

Tuesday, September 28, 2010

Page 41: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) { [NSException raise:@"SQLITE ERROR" format:@"Error %d", result];}

//later

Tuesday, September 28, 2010

Page 42: SQLite Techniques

SQLite API - Opening a connection

#import "sqlite3.h"

sqlite3 *db;

int result = sqlite3_open(PATH, &db);

if (result != SQLITE_OK) { [NSException raise:@"SQLITE ERROR" format:@"Error %d", result];}

//latersqlite3_close(db);

Tuesday, September 28, 2010

Page 43: SQLite Techniques

SQLite API - executing queries

Tuesday, September 28, 2010

Page 44: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(

Tuesday, September 28, 2010

Page 45: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,

Tuesday, September 28, 2010

Page 46: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,

Tuesday, September 28, 2010

Page 47: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,

Tuesday, September 28, 2010

Page 48: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,

Tuesday, September 28, 2010

Page 49: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage

Tuesday, September 28, 2010

Page 50: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

Tuesday, September 28, 2010

Page 51: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

//callback signature

Tuesday, September 28, 2010

Page 52: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

//callback signatureint RowCallback(void * context,

Tuesday, September 28, 2010

Page 53: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

//callback signatureint RowCallback(void * context,

int numColumns,

Tuesday, September 28, 2010

Page 54: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

//callback signatureint RowCallback(void * context,

int numColumns, char** colValues,

Tuesday, September 28, 2010

Page 55: SQLite Techniques

SQLite API - executing queries

int sqlite3_exec(sqlite3 *db,char *sql,int (*callback)(void *, int, char**, char**) callbackFunc,void *context,char **errorMessage);

//callback signatureint RowCallback(void * context,

int numColumns, char** colValues, char ** colNames);

Tuesday, September 28, 2010

Page 56: SQLite Techniques

SQLite API -Prepared Statements

More Powerful

Unfortunately much more code!

Tuesday, September 28, 2010

Page 57: SQLite Techniques

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 58: SQLite Techniques

sqlite3 *db;

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 59: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 60: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 61: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 62: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error }

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 63: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error }

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 64: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt;

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 65: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 66: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 67: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 68: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error }

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 69: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error }

//...

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 70: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error }

//...

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 71: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error }

//...

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 72: SQLite Techniques

sqlite3 *db; int result = sqlite3_open(DB_PATH, &db); if(result != SQLITE_OK) { //handle error } sqlite3_stmt *stmt; result = sqlite3_prepare_v2(db, sql_cstring, -1, &stmt, NULL); if(result != SQLITE_OK) { //handle error }

//...

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 73: SQLite Techniques

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 74: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

Tuesday, September 28, 2010

Page 75: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX!

Tuesday, September 28, 2010

Page 76: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5);

Tuesday, September 28, 2010

Page 77: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT);

Tuesday, September 28, 2010

Page 78: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT);

Tuesday, September 28, 2010

Page 79: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt);

Tuesday, September 28, 2010

Page 80: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE)

Tuesday, September 28, 2010

Page 81: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do!

Tuesday, September 28, 2010

Page 82: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do!

Tuesday, September 28, 2010

Page 83: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array];

Tuesday, September 28, 2010

Page 84: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) {

Tuesday, September 28, 2010

Page 85: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary];

Tuesday, September 28, 2010

Page 86: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary];

Tuesday, September 28, 2010

Page 87: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

Tuesday, September 28, 2010

Page 88: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row];

Tuesday, September 28, 2010

Page 89: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row]; result = sqlite3_step(stmt);

Tuesday, September 28, 2010

Page 90: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row]; result = sqlite3_step(stmt); }

Tuesday, September 28, 2010

Page 91: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row]; result = sqlite3_step(stmt); }

sqlite_finalize(stmt);

Tuesday, September 28, 2010

Page 92: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row]; result = sqlite3_step(stmt); }

sqlite_finalize(stmt);

Tuesday, September 28, 2010

Page 93: SQLite Techniques

SQLite API-Prepared Statements //INSERT INTO CARS(year, make) VALUES(?, ?)

//PARAMETERS HAVE A 1-BASED INDEX! sqlite3_bind_int(stmt, 1 /* idx */, 5); sqlite3_bind_text(stmt, 2 /* idx */,"value", -1, SQLITE_TRANSIENT); result = sqlite3_step(stmt); if(result == SQLITE_DONE) return [NSArray array]; //nothing to do! NSMutableArray *results = [NSMutableArray array]; while(result == SQLITE_ROW) { NSMutableDictionary *row = [NSMutableDictionary dictionary]; [self processRow:row forStatement:stmt];

[results addObject:row]; result = sqlite3_step(stmt); }

sqlite_finalize(stmt); return results;

Tuesday, September 28, 2010

Page 94: SQLite Techniques

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 95: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 96: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 97: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 98: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index!

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 99: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 100: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 101: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null];

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 102: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 103: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)];

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 104: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 105: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i);

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 106: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text];

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 107: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else {

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 108: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 109: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling }

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 110: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling } [row setObject:value forKey:

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 111: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling } [row setObject:value forKey: [NSString stringWithUTF8String:colName]];

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 112: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling } [row setObject:value forKey: [NSString stringWithUTF8String:colName]]; }

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 113: SQLite Techniques

-(void)processRow:(NSMutableDictionary *)row forStatement:(sqlite3_stmt*)stmt { int columnCount = sqlite3_column_count(stmt); for(int i=0; i<columnCount; i++) { //0-based index! const char * colName = sqlite3_column_name(stmt, i); int type = sqlite3_column_type(stmt, i);

id value = [NSNull null]; if(type == SQLITE_INTEGER) { value = [NSNumber numberWithInt:sqlite3_column_int(stmt, i)]; } else if (type == SQLITE_TEXT) { const unsigned char * text = sqlite3_column_text(stmt, i); value = [NSString stringWithFormat:@"%s", text]; } else { // more type handling } [row setObject:value forKey: [NSString stringWithUTF8String:colName]]; }}

SQLite API-Prepared Statements

Tuesday, September 28, 2010

Page 114: SQLite Techniques

FMDB

http://github.com/ccgus/fmdb

Tuesday, September 28, 2010

Page 115: SQLite Techniques

FMDB Usage

Tuesday, September 28, 2010

Page 116: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];

Tuesday, September 28, 2010

Page 117: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])

Tuesday, September 28, 2010

Page 118: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

Tuesday, September 28, 2010

Page 119: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];

Tuesday, September 28, 2010

Page 120: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];    

Tuesday, September 28, 2010

Page 121: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])

Tuesday, September 28, 2010

Page 122: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode],

Tuesday, September 28, 2010

Page 123: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

Tuesday, September 28, 2010

Page 124: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

Tuesday, September 28, 2010

Page 125: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];

Tuesday, September 28, 2010

Page 126: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;

Tuesday, September 28, 2010

Page 127: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {

Tuesday, September 28, 2010

Page 128: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {   [db executeUpdate:@"insert into test (a, b) values (?, ?)" ,

Tuesday, September 28, 2010

Page 129: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {   [db executeUpdate:@"insert into test (a, b) values (?, ?)" ,       @"hi'", // look! I put in a ', and I'm not escaping it!

Tuesday, September 28, 2010

Page 130: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {   [db executeUpdate:@"insert into test (a, b) values (?, ?)" ,       @"hi'", // look! I put in a ', and I'm not escaping it!       [NSNumber numberWithInt:i]];

Tuesday, September 28, 2010

Page 131: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {   [db executeUpdate:@"insert into test (a, b) values (?, ?)" ,       @"hi'", // look! I put in a ', and I'm not escaping it!       [NSNumber numberWithInt:i]];  }

Tuesday, September 28, 2010

Page 132: SQLite Techniques

FMDB Usage  FMDatabase* db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];  if (![db open])     NSLog(@"Could not open db.");

  [db executeUpdate:@"blah blah blah"];      if ([db hadError])   NSLog(@"Err %d: %@", [db lastErrorCode], [db lastErrorMessage]);

  [db executeUpdate:@"create table test (a text, b integer)"];

  [db beginTransaction];  int i = 0;  while (i++ < 20) {   [db executeUpdate:@"insert into test (a, b) values (?, ?)" ,       @"hi'", // look! I put in a ', and I'm not escaping it!       [NSNumber numberWithInt:i]];  }  [db commit];

Tuesday, September 28, 2010

Page 133: SQLite Techniques

What about updates?

Database is never copied over again

If we did, your customers would lose data

Tuesday, September 28, 2010

Page 134: SQLite Techniques

Migrations

Concept from Ruby on Rails

Evolutionary database design

Each database has a "version"

App runs all migrations to the latest version

Tuesday, September 28, 2010

Page 135: SQLite Techniques

Migrations

number name

1 initial_schema

2 update_foo

3 implement_blah

Tuesday, September 28, 2010

Page 136: SQLite Techniques

Migrations

number name

1 initial_schema

2 update_foo

3 implement_blah

Tuesday, September 28, 2010

Page 137: SQLite Techniques

Hand-rolled Database Migrator

DEMO

Tuesday, September 28, 2010

Page 139: SQLite Techniques

FMDB Migration Manager

NSArray *migrations = [NSArray arrayWithObjects: [CreateStudents migration], // 1 [CreateStaffMembers migration], // 2 [AddStudentNumberToStudents migration], // 3 nil ];[FmdbMigrationManager executeForDatabasePath:@"/tmp/tmp.db" withMigrations:migrations];

Tuesday, September 28, 2010

Page 140: SQLite Techniques

FMDB Migration Manager

@interface CreateStudents : FmdbMigration{}@end

@implementation CreateStudents- (void)up { [self createTable:@"students" withColumns:[NSArray arrayWithObjects: [FmdbMigrationColumn columnWithColumnName:@"first_name" columnType:@"string"], [FmdbMigrationColumn columnWithColumnName:@"age" columnType:@"integer" defaultValue:21], nil];}

- (void)down { [self dropTable:@"students"];}

@end

Tuesday, September 28, 2010

Page 141: SQLite Techniques

ActiveRecord

Person *p = [[Person alloc] init];p.name = @"Joe";p.occupation = @"Dishwasher";

[p save];

Person *p = [Person personWithId:5];==> Person {id:5, name:Joe, occupation:Dishwasher}

Tuesday, September 28, 2010

Page 142: SQLite Techniques

Aptiva's ActiveRecord

http://github.com/aptiva/activerecord

(YMMV)

Tuesday, September 28, 2010

Page 143: SQLite Techniques

Questions?

Tuesday, September 28, 2010