#import <objc/runtime.h>
#import <sqlite3.h>
#import "objsql.h"
#if 0#endif
OOOODatabase OODB;
static NSString *kOOObject = @"__OOOBJECT__", *kOOInsert = @"__ISINSERT__", *kOOUpdate = @"__ISUPDATE__", *kOOExecSQL = @"__OOEXEC__";
#pragma mark OORecord abstract superclass for records
@implementation OORecord
+ (id)record OO_AUTORETURNS {
return OO_AUTORELEASE( [[self alloc] init] );
}
+ (id)insert OO_AUTORETURNS {
OORecord *record = [self record];
[record insert];
return record;
}
+ (id)insertWithParent:(id)parent {
return [[OODatabase sharedInstance] copyJoinKeysFrom:parent to:[self insert]];
}
- (id)insert { [[OODatabase sharedInstance] insert:self]; return self; }
- (id)delete { [[OODatabase sharedInstance] delete:self]; return self; }
- (void)update { [[OODatabase sharedInstance] update:self]; }
- (void)indate { [[OODatabase sharedInstance] indate:self]; }
- (void)upsert { [[OODatabase sharedInstance] upsert:self]; }
- (int)commit { return [[OODatabase sharedInstance] commit]; }
- (int)rollback { return [[OODatabase sharedInstance] rollback]; }
- (void)setNilValueForKey:(NSString *)key {
static OOReference<NSValue *> zeroForNull;
if ( !zeroForNull )
zeroForNull = [NSNumber numberWithInt:0];
[self setValue:zeroForNull forKey:key];
}
+ (OOArray<id>)select {
return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:nil];
}
+ (OOArray<id>)select:(cOOString)sql {
return [[OODatabase sharedInstance] select:sql intoClass:self joinFrom:nil];
}
+ (OOArray<id>)selectRecordsRelatedTo:(id)parent {
return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:parent];
}
- (OOArray<id>)select {
return [[OODatabase sharedInstance] select:nil intoClass:[self class] joinFrom:self];
}
+ (int)importFrom:(OOFile &)file delimiter:(cOOString)delim {
OOArray<id> rows = [OOMetaData import:file.string() intoClass:self delimiter:delim];
[[OODatabase sharedInstance] insertArray:rows];
return [OODatabase commit];
}
+ (BOOL)exportTo:(OOFile &)file delimiter:(cOOString)delim {
return file.save( [OOMetaData export:[self select] delimiter:delim] );
}
- (void)bindToView:(OOView *)view delegate:(id)delegate {
[OOMetaData bindRecord:self toView:view delegate:delegate];
}
- (void)updateFromView:(OOView *)view {
[OOMetaData updateRecord:self fromView:view];
}
- (NSString *)description {
OOMetaData *metaData = [OOMetaData metaDataForClass:[self class]];
OOStringArray ivars; ivars <<= *metaData->ivars; ivars -= "description";
return [*[metaData encode:[self dictionaryWithValuesForKeys:ivars]] description];
}
@end
#pragma mark OOAdaptor - all methods required by objsql to access a database
@interface OOAdaptor : NSObject {
sqlite3 *db;
sqlite3_stmt *stmt;
struct _str_link {
struct _str_link *next; char str[1];
} *strs;
OO_UNSAFE OODatabase *owner;
}
- initPath:(cOOString)path database:(OODatabase *)database;
- (BOOL)prepare:(cOOString)sql;
- (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls;
- (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData;
- (sqlite_int64)lastInsertRowID;
@end
@interface NSData(OOExtras)
- initWithDescription:(NSString *)description;
@end
#pragma mark OODatabase is the low level interface to a particular database
@implementation OODatabase
static OOReference<OODatabase *> sharedInstance;
+ (OODatabase *)sharedInstance {
if ( !sharedInstance )
[self sharedInstanceForPath:OODocument("objsql.db").path()];
return sharedInstance;
}
+ (OODatabase *)sharedInstanceForPath:(cOOString)path {
if ( !!sharedInstance )
sharedInstance = OONil;
if ( !!path )
OO_RELEASE( sharedInstance = [[OODatabase alloc] initPath:path] );
return sharedInstance;
}
+ (BOOL)exec:(cOOString)fmt, ... {
va_list argp; va_start(argp, fmt);
NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
va_end( argp );
return [[self sharedInstance] exec:OO_AUTORELEASE( sql )];
}
+ (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
return [[self sharedInstance] select:select intoClass:recordClass joinFrom:parent];
}
+ (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
return [[self sharedInstance] select:select intoClass:recordClass joinFrom:nil];
}
+ (OOArray<id>)select:(cOOString)select {
return [[self sharedInstance] select:select intoClass:nil joinFrom:nil];
}
+ (int)insertArray:(const OOArray<id> &)objects { return [[self sharedInstance] insertArray:objects]; }
+ (int)deleteArray:(const OOArray<id> &)objects { return [[self sharedInstance] deleteArray:objects]; }
+ (int)insert:(id)object { return [[self sharedInstance] insert:object]; }
+ (int)delete:(id)object { return [[self sharedInstance] delete:object]; }
+ (int)update:(id)object { return [[self sharedInstance] update:object]; }
+ (int)indate:(id)object { return [[self sharedInstance] indate:object]; }
+ (int)upsert:(id)object { return [[self sharedInstance] upsert:object]; }
+ (int)commit { return [[self sharedInstance] commit]; }
+ (int)rollback { return [[self sharedInstance] rollback]; }
+ (int)commitTransaction { return [[OODatabase sharedInstance] commitTransaction]; }
- initPath:(cOOString)path {
if ( self = [super init] )
OO_RELEASE( adaptor = [[OOAdaptor alloc] initPath:path database:self] );
return self;
}
- (OOStringArray)registerSubclassesOf:(Class)recordSuperClass {
int numClasses = objc_getClassList( NULL, 0 );
Class *classes = (Class *)malloc( sizeof *classes * numClasses );
OOArray<Class> viewClasses;
OOStringArray classNames;
numClasses = objc_getClassList( classes, numClasses );
for ( int c=0 ; c<numClasses ; c++ ) {
Class superClass = classes[c];
if ( class_getName( superClass )[0] != '_' )
while ( (superClass = class_getSuperclass( superClass )) )
if ( superClass == recordSuperClass ) {
if ( [classes[c] respondsToSelector:@selector(ooTableSql)] )
viewClasses += classes[c];
else {
[[OODatabase sharedInstance] tableMetaDataForClass:classes[c]];
classNames += class_getName( classes[c] );
}
break;
}
}
for ( int c=0 ; c<viewClasses ; c++ ) {
[[OODatabase sharedInstance] tableMetaDataForClass:viewClasses[c]];
classNames += class_getName( viewClasses[c] );
}
free( classes );
return classNames;
}
- (void)registerTableClassesNamed:(cOOStringArray)classes {
for ( NSString *tableClass in *classes )
[self tableMetaDataForClass:[[NSBundle mainBundle] classNamed:tableClass]];
}
- (BOOL)exec:(cOOString)fmt, ... {
va_list argp; va_start(argp, fmt);
NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
va_end( argp );
results = [self select:sql intoClass:NULL joinFrom:nil];
OO_RELEASE( sql );
return !errcode;
}
- (OOString)stringForSql:(cOOString)fmt, ... {
va_list argp; va_start(argp, fmt);
NSString *sql = OO_AUTORELEASE( [[NSString alloc] initWithFormat:fmt arguments:argp] );
va_end( argp );
if( [self exec:"%@", sql] && results > 0 ) {
NSString *aColumnName = [[**results[0] allKeys] objectAtIndex:0];
return [(NSNumber *)(*results[0])[aColumnName] stringValue];
}
else
return nil;
}
- (id)copyJoinKeysFrom:(id)parent to:(id)newChild {
OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]],
*childMetaData = [self tableMetaDataForClass:[newChild class]];
OOStringArray commonColumns = [parentMetaData naturalJoinTo:childMetaData->columns]; OOValueDictionary keyValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:commonColumns]];
[newChild setValuesForKeysWithDictionary:[childMetaData decode:keyValues]];
return newChild;
}
- (OOString)whereClauseFor:(cOOStringArray)columns values:(cOOValueDictionary)values qualifyNulls:(BOOL)qualifyNulls {
OOString out;
for ( int i=0 ; i<columns ; i++ ) {
NSString *name = *columns[i];
const char *prefix = i==0 ?"\nwhere":" and";
id value = values[name];
if ( value == OONull )
out += qualifyNulls ? OOFormat( @"%s\n\t%@ is NULL", prefix, name ) : @"";
else if ( !qualifyNulls && [value isKindOfClass:[NSString class]] &&
[value rangeOfString:@"%"].location != NSNotFound )
out += OOFormat( @"%s\n\t%@ LIKE ?", prefix, name );
else
out += OOFormat( @"%s\n\t%@ = ?", prefix, name );
}
return out;
}
- (BOOL)prepareSql:(OOString &)sql joinFrom:(id)parent toTable:(OOMetaData *)metaData {
OOValueDictionary joinValues;
OOStringArray sharedColumns;
if ( parent ) {
OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]];
sharedColumns = [parentMetaData naturalJoinTo:metaData->joinableColumns];
joinValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:sharedColumns]];
sql += [self whereClauseFor:sharedColumns values:joinValues qualifyNulls:NO];
}
if ( [metaData->recordClass respondsToSelector:@selector(ooOrderBy)] )
sql += OOFormat( @"\norder by %@", [metaData->recordClass ooOrderBy] );
#ifdef OODEBUG_SQL
NSLog( @"-[OOMetaData prepareSql:] %@\n%@", *sql, *joinValues );
#endif
if ( ![*adaptor prepare:sql] )
return NO;
return !parent || [*adaptor bindCols:sharedColumns values:joinValues startingAt:1 bindNulls:NO];
}
- (OOArray<OOMetaData *>)tablesRelatedByNaturalJoinFrom:(id)record {
OOMetaData *metaData = [record class] == [OOMetaData class] ?
record : [self tableMetaDataForClass:[record class]];
OOStringArray tablesWithNaturalJoin;
tablesWithNaturalJoin <<= metaData->tablesWithNaturalJoin;
if ( record && record != metaData )
for ( int i=0 ; i<tablesWithNaturalJoin ; i++ ) {
OOString sql = OOFormat( @"select count(*) as result from %@", **tablesWithNaturalJoin[i] );
OOMetaData *childMetaData = tableMetaDataByClassName[tablesWithNaturalJoin[i]];
[self prepareSql:sql joinFrom:record toTable:childMetaData];
OOArray<OODictionary<NSNumber *> > tmpResults = [*adaptor bindResultsIntoInstancesOfClass:NULL metaData:nil];
if ( ![(*tmpResults[0])["result"] intValue] )
~tablesWithNaturalJoin[i--];
}
return tableMetaDataByClassName[+tablesWithNaturalJoin];
}
- (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
OOMetaData *metaData = [self tableMetaDataForClass:recordClass ? recordClass : [parent class]];
OOString sql = !select ?
OOFormat( @"select %@\nfrom %@", *(metaData->outcols/", "), *metaData->tableName ) : *select;
if ( ![self prepareSql:sql joinFrom:parent toTable:metaData] )
return nil;
return [*adaptor bindResultsIntoInstancesOfClass:recordClass metaData:metaData];
}
- (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
return [self select:select intoClass:recordClass joinFrom:nil];
}
- (OOArray<id>)select:(cOOString)select {
return [self select:select intoClass:nil joinFrom:nil];
}
- (long long)rowIDForRecord:(id)record {
OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
OOString sql = OOFormat( @"select ROWID from %@", *metaData->tableName );
OOArray<OODictionary<NSNumber *> > idResults = [self select:sql intoClass:nil joinFrom:record];
return [*(*idResults[0])[@"rowid"] longLongValue];
}
- (long long)lastInsertRowID {
return [*adaptor lastInsertRowID];
}
- (int)insertArray:(const OOArray<id> &)objects {
int count = 0;
for ( id object in *objects )
count = [self insert:object];
return count;
}
- (int)deleteArray:(const OOArray<id> &)objects {
int count = 0;
for ( id object in *objects )
if ( ![object respondsToSelector:@selector(delete)] )
count = [self delete:object];
else {
[object delete];
count++;
}
return count;
}
- (int)insert:(id)record {
return transaction += OOValueDictionary( kOOObject, record, kOOInsert, kCFNull, nil );
}
- (int)delete:(id)record {
return transaction += OOValueDictionary( kOOObject, record, nil );
}
- (int)update:(id)record {
OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
OOValueDictionary oldValues = [metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
for ( NSString *key in *metaData->tocopy )
OO_RELEASE( oldValues[key] = [oldValues[key] copy] );
oldValues[kOOUpdate] = OONull;
oldValues[kOOObject] = record;
return transaction += oldValues;
}
- (int)indate:(id)record {
OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
OOString sql = OOFormat( @"select rowid from %@", *metaData->tableName );
OOArray<id> existing = [self select:sql intoClass:nil joinFrom:record];
int count = [self insert:record];
for ( NSDictionary *exist in *existing ) {
OOString sql = OOFormat( @"delete from %@ where rowid = %ld", *metaData->tableName,
(long)[[exist objectForKey:@"rowid"] longLongValue] );
transaction += OOValueDictionary( kOOExecSQL, *sql, nil );
}
return count;
}
- (int)upsert:(id)record {
OOArray<id> existing = [self select:nil intoClass:[record class] joinFrom:record];
if ( existing > 1 )
OOWarn( @"-[ODatabase upsert:] Duplicate record for upsert: %@", record );
if ( existing > 0 ) {
[self update:existing[0]];
(*transaction[-1])[kOOObject] = record;
return transaction;
}
else
return [self insert:record];
}
- (int)commit {
int commited = 0;
for ( int i=0 ; i<transaction ; i++ ) {
OOValueDictionary values = transaction[i];
OOString exec = (NSMutableString *)~values[kOOExecSQL];
if ( !!exec ) {
if ( ![self exec:@"%@", *exec] )
OOWarn( @"-[ODatabase commit] Error in transaction exec: %@ - %s", *exec, errmsg );
continue;
}
OORef<NSObject *> object = *values[kOOObject]; values -= kOOObject;
BOOL isInsert = !!~values[kOOInsert], isUpdate = !!~values[kOOUpdate];
OOMetaData *metaData = [self tableMetaDataForClass:[object class]];
OOValueDictionary newValues = [metaData encode:[object dictionaryWithValuesForKeys:metaData->columns]];
OOStringArray changedCols;
if ( isUpdate ) {
for ( NSString *name in *metaData->columns )
if ( ![*newValues[name] isEqual:values[name]] )
changedCols += name;
}
else
values = newValues;
OOString sql = isInsert ?
OOFormat( @"insert into %@ (%@) values (", *metaData->tableName, *(metaData->columns/", ") ) :
OOFormat( isUpdate ? @"update %@ set" : @"delete from %@", *metaData->tableName );
int nchanged = changedCols;
if ( isUpdate && nchanged == 0 ) {
OOWarn( @"%s %@ (%@)", errmsg = (char *)"-[ODatabase commit:] Update of unchanged record", *object, *(lastSQL = sql) );
continue;
}
for ( int i=0 ; i<nchanged ; i++ )
sql += OOFormat( @"%s\n\t%@ = ?", i==0 ? "" : ",", **changedCols[i] );
if ( isInsert ) {
OOString quote = "?", commaQuote = ", ?";
for ( int i=0 ; i<metaData->columns ; i++ )
sql += i==0 ? quote : commaQuote;
sql += ")";
}
else
sql += [self whereClauseFor:metaData->columns values:values qualifyNulls:YES];
#ifdef OODEBUG_SQL
NSLog( @"-[OODatabase commit]: %@ %@", *sql, *values );
#endif
if ( ![*adaptor prepare:sql] )
continue;
if ( isUpdate )
[*adaptor bindCols:changedCols values:newValues startingAt:1 bindNulls:YES];
[*adaptor bindCols:metaData->columns values:values startingAt:1+nchanged bindNulls:isInsert];
[*adaptor bindResultsIntoInstancesOfClass:nil metaData:metaData];
commited += updateCount;
}
transaction = nil;
return commited;
}
- (int)commitTransaction {
[self exec:"BEGIN TRANSACTION"];
int updated = [self commit];
return [self exec:"COMMIT"] ? updated : 0;
}
- (int)rollback {
for ( NSMutableDictionary *d in *transaction ) {
OODictionary<id> values = d;
if ( !!~values[kOOUpdate] ) {
OORef<OORecord *> record = ~values[kOOObject];
OOMetaData *metaData = [self tableMetaDataForClass:[*record class]];
#ifndef OO_ARC
for ( NSString *name in *metaData->boxed )
OO_RELEASE( (id)[[*record valueForKey:name] pointerValue] );
#endif
[*record setValuesForKeysWithDictionary:[metaData decode:values]];
}
}
return (int)[*~transaction count];
}
- (OOMetaData *)tableMetaDataForClass:(Class)recordClass {
if ( !recordClass || recordClass == [OOMetaData class] )
return [OOMetaData metaDataForClass:[OOMetaData class]];
OOString className = class_getName( recordClass );
OOMetaData *metaData = tableMetaDataByClassName[className];
if ( !metaData ) {
metaData = [OOMetaData metaDataForClass:recordClass];
#ifdef OODEBUG_SQL
NSLog(@"\n%@", *metaData->createTableSQL);
#endif
if ( metaData->tableName[0] != '_' &&
[self stringForSql:"select count(*) from sqlite_master where name = '%@'",
*metaData->tableName] == "0" )
if ( [self exec:"%@", *metaData->createTableSQL] )
for ( NSString *idx in *metaData->indexes )
if ( ![self exec:idx] )
OOWarn( @"-[OOMetaData tableMetaDataForClass:] Error creating index: %@", idx );
tableMetaDataByClassName[className] = metaData;
}
return metaData;
}
@end
#pragma mark OOAdaptor - implements all access to a particular database
@implementation OOAdaptor
- (OOAdaptor *)initPath:(cOOString)path database:(OODatabase *)database {
if ( self = [super init] ) {
owner = database;
OOFile( OOFile( path ).directory() ).mkdir();
if ( (owner->errcode = sqlite3_open( path, &db )) != SQLITE_OK ) {
OOWarn( @"-[OOAdaptor initPath:database:] Error opening database at path: %@", *path );
return nil;
}
}
return self;
}
- (BOOL)prepare:(cOOString)sql {
if ( (owner->errcode = sqlite3_prepare_v2( db, owner->lastSQL = sql, -1, &stmt, 0 )) != SQLITE_OK )
OOWarn(@"-[OOAdaptor prepare:] Could not prepare sql: \"%@\" - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
return owner->errcode == SQLITE_OK;
}
- (int)bindValue:(id)value asParameter:(int)pno {
#ifdef OODEBUG_BIND
NSLog( @"-[OOAdaptor bindValue:bindValue:] bind parameter #%d as: %@", pno, value );
#endif
if ( !value || value == OONull )
return sqlite3_bind_null( stmt, pno );
#if OOSQL_THREAD_SAFE_BUT_USES_MORE_MEMORY
else if ( [value isKindOfClass:[NSString class]] )
return sqlite3_bind_text( stmt, pno, [value UTF8String], -1, SQLITE_STATIC );
#else
else if ( [value isKindOfClass:[NSString class]] ) {
int len = (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
struct _str_link *str = (struct _str_link *)malloc( sizeof *str + len );
str->next = strs;
strs = str;
[value getCString:str->str maxLength:len+1 encoding:NSUTF8StringEncoding];
return sqlite3_bind_text( stmt, pno, str->str, len, SQLITE_STATIC );
}
#endif
else if ( [value isKindOfClass:[NSData class]] )
return sqlite3_bind_blob( stmt, pno, [value bytes], (int)[value length], SQLITE_STATIC );
const char *type = [value objCType];
if ( type )
switch ( type[0] ) {
case 'c': case 's': case 'i': case 'l':
case 'C': case 'S': case 'I': case 'L':
return sqlite3_bind_int( stmt, pno, [value intValue] );
case 'q': case 'Q':
return sqlite3_bind_int64( stmt, pno, [value longLongValue] );
case 'f': case 'd':
return sqlite3_bind_double( stmt, pno, [value doubleValue] );
}
OOWarn( @"-[OOAdaptor bindValue:bindValue:] Undefined type in bind of parameter #%d: %s, value: %@", pno, type, value );
return -1;
}
- (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls {
int errcode;
for ( NSString *name in *columns )
if ( bindNulls || *values[name] != OONull )
if ( (errcode = [self bindValue:values[name] asParameter:pno++]) != SQLITE_OK )
OOWarn( @"-[OOAdaptor bindCols:...] Bind failed column: %@ - %s (%d)", name, owner->errmsg = (char *)sqlite3_errmsg( db ), owner->errcode = errcode );
return owner->errcode == SQLITE_OK;
}
- (OOValueDictionary)valuesForNextRow {
int ncols = sqlite3_column_count( stmt );
OOValueDictionary values;
for ( int i=0 ; i<ncols ; i++ ) {
OOString name = sqlite3_column_name( stmt, i );
id value = nil;
switch ( sqlite3_column_type( stmt, i ) ) {
case SQLITE_NULL:
value = OONull;
break;
case SQLITE_INTEGER:
value = [[NSNumber alloc] initWithLongLong:sqlite3_column_int64( stmt, i )];
break;
case SQLITE_FLOAT:
value = [[NSNumber alloc] initWithDouble:sqlite3_column_double( stmt, i )];
break;
case SQLITE_TEXT: {
const unsigned char *bytes = sqlite3_column_text( stmt, i );
value = [[NSMutableString alloc] initWithBytes:bytes
length:sqlite3_column_bytes( stmt, i)
encoding:NSUTF8StringEncoding];
}
break;
case SQLITE_BLOB: {
const void *bytes = sqlite3_column_blob( stmt, i );
value = [[NSData alloc] initWithBytes:bytes length:sqlite3_column_bytes( stmt, i )];
}
break;
default:
OOWarn( @"-[OOAdaptor valuesForNextRow:] Invalid type on bind of ivar %@: %d", *name, sqlite3_column_type( stmt, i ) );
}
values[name] = value;
OO_RELEASE( value );
}
return values;
}
- (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData {
OOArray<id> out;
BOOL awakeFromDB = [recordClass instancesRespondToSelector:@selector(awakeFromDB)];
while( (owner->errcode = sqlite3_step( stmt )) == SQLITE_ROW ) {
OOValueDictionary values = [self valuesForNextRow];
if ( recordClass ) {
id record = [[recordClass alloc] init];
[record setValuesForKeysWithDictionary:[metaData decode:values]];
if ( awakeFromDB )
[record awakeFromDB];
out += record;
OO_RELEASE( record );
}
else
out += values;
}
if ( owner->errcode != SQLITE_DONE )
OOWarn(@"-[OOAdaptor bindResultsIntoInstancesOfClass:metaData:] Not done (bind) stmt: %@ - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
else {
owner->errcode = SQLITE_OK;
out.alloc();
}
while ( strs != NULL ) {
struct _str_link *next = strs->next;
free( strs );
strs = next;
}
owner->updateCount = sqlite3_changes( db );
sqlite3_finalize( stmt );
return out;
}
- (sqlite_int64)lastInsertRowID {
return sqlite3_last_insert_rowid( db );
}
- (void) dealloc {
sqlite3_close( db );
OO_DEALLOC( super );
}
@end
#pragma mark OOMetaData instances represent a table in the database and it's record class
@implementation OOMetaData
static OODictionary<OOMetaData *> metaDataByClass;
static OOMetaData *tableOfTables;
+ (NSString *)ooTableTitle { return @"Table MetaData"; }
+ (OOMetaData *)metaDataForClass:(Class)recordClass OO_RETURNS {
if ( !tableOfTables )
OO_RELEASE( tableOfTables = [[OOMetaData alloc] initClass:[OOMetaData class]] );
OOMetaData *metaData = metaDataByClass[recordClass];
if ( !metaData )
OO_RELEASE( metaData = [[OOMetaData alloc] initClass:recordClass] );
return metaData;
}
+ (OOArray<id>)selectRecordsRelatedTo:(id)record {
return [[OODatabase sharedInstance] tablesRelatedByNaturalJoinFrom:record];
}
- initClass:(Class)aClass {
if ( !(self = [super init]) )
return self;
recordClass = aClass;
metaDataByClass[recordClass] = self;
recordClassName = class_getName( recordClass );
tableTitle = [recordClass respondsToSelector:@selector(ooTableTitle)] ?
[recordClass ooTableTitle] : *recordClassName;
tableName = [recordClass respondsToSelector:@selector(ooTableName)] ?
[recordClass ooTableName] : *recordClassName;
if ( aClass == [OOMetaData class] ) {
ivars = columns = outcols = boxed = unbox =
"tableTitle tableName recordClassName keyColumns ivars columns outcols";
return self;
}
createTableSQL = OOFormat( @"create table %@ (", *tableName );
OOArray<Class> hierarchy;
do
hierarchy += aClass;
while ( (aClass = [aClass superclass]) && aClass != [NSObject class] );
for ( int h=(int)hierarchy-1 ; h>=0 ; h-- ) {
aClass = (Class)hierarchy[h]; Ivar *ivarInfo = class_copyIvarList( aClass, NULL );
if ( !ivarInfo )
continue;
for ( int in=0 ; ivarInfo[in] ; in++ ) {
OOString columnName = ivar_getName( ivarInfo[in] );
ivars += columnName;
OOString type = types[columnName] = ivar_getTypeEncoding( ivarInfo[in] ), dbtype = "";
SEL columnSel = sel_getUid(columnName);
switch ( type[0] ) {
case 'c': case 's': case 'i': case 'l':
case 'C': case 'S': case 'I': case 'L':
case 'q': case 'Q':
dbtype = @"int";
break;
case 'f': case 'd':
dbtype = @"real";
break;
case '{':
static OOPattern isOORef( "=\"ref\"@\"NS" );
if( !(type & isOORef) )
OOWarn( @"-[OOMetaData initClass:] Invalid structure type for ivar %@ in class %@: %@", *columnName, *recordClassName, *type );
boxed += columnName;
if ( ![recordClass instancesRespondToSelector:columnSel] ) {
unbox += columnName;
if ( [[recordClass superclass] instancesRespondToSelector:columnSel] )
OOWarn( @"-[OOMetaData initClass:] Superclass of class %@ is providing method for column: %@", *recordClassName, *columnName );
}
case '@':
static OOPattern isNSString( "NS(Mutable)?String\"" ),
isNSDate( "\"NSDate\"" ), isNSData( "NS(Mutable)?Data\"" );
if ( type & isNSString )
dbtype = @"text";
else if ( type & isNSDate ) {
dbtype = @"real";
dates += columnName;
}
else {
if ( !(type & isNSData) )
archived += columnName;
blobs += columnName;
dbtype = @"blob";
}
break;
default:
OOWarn( @"-[OOMetaData initClass:] Unknown data type '%@' in class %@", *type, *tableName );
archived += columnName;
blobs += columnName;
dbtype = @"blob";
break;
}
if ( dbtype == @"text" )
tocopy += columnName;
if ( columnName == @"rowid" || columnName == @"ROWID" ||
columnName == @"OID" || columnName == @"_ROWID_" ) {
outcols += columnName;
continue;
}
createTableSQL += OOFormat(@"%s\n\t%@ %@ /* %@ */",
!columns?"":",", *columnName, *dbtype, *type );
if ( iswupper( columnName[columnName[0] != '_' ? 0 : 1] ) )
indexes += OOFormat(@"create index %@_%@ on %@ (%@)\n",
*tableName, *columnName,
*tableName, *columnName);
if ( class_getName( [aClass superclass] )[0] != '_' ) {
columns += columnName;
outcols += columnName;
joinableColumns += columnName;
}
}
free( ivarInfo );
}
if ( [recordClass respondsToSelector:@selector(ooTableKey)] )
createTableSQL += OOFormat( @",\n\tprimary key (%@)",
*(keyColumns = [recordClass ooTableKey]) );
if ( [recordClass respondsToSelector:@selector(ooConstraints)] )
createTableSQL += OOFormat( @",\n\t%@", [recordClass ooConstraints] );
createTableSQL += "\n)\n";
if ( [recordClass respondsToSelector:@selector(ooTableSql)] ) {
createTableSQL = [recordClass ooTableSql];
indexes = nil;
}
tableOfTables->tablesWithNaturalJoin += recordClassName;
tablesWithNaturalJoin += recordClassName;
for( Class other in [*metaDataByClass allKeys] ) {
OOMetaData *otherMetaData = metaDataByClass[other];
if ( other == recordClass || otherMetaData == tableOfTables )
continue;
if ( [self naturalJoinTo:otherMetaData->joinableColumns] > 0 )
tablesWithNaturalJoin += otherMetaData->recordClassName;
if ( [otherMetaData naturalJoinTo:joinableColumns] > 0 )
otherMetaData->tablesWithNaturalJoin += recordClassName;
}
return self;
}
- (OOStringArray)naturalJoinTo:(cOOStringArray)to {
OOStringArray commonColumns = columns & to;
for ( int i=0 ; i<commonColumns ; i++ )
if ( islower( (*commonColumns[i])[0] ) )
~commonColumns[i--];
return commonColumns;
}
- (cOOValueDictionary)encode:(cOOValueDictionary)values {
for ( NSString *key in *unbox ) {
id value = (id)[*values[key] pointerValue];
values[key] = value ? value : OONull;
}
for ( NSString *key in *dates )
values[key] = [NSNumber numberWithDouble:[(id)values[key] timeIntervalSince1970]];
for ( NSString *key in *archived )
values[key] = (NSValue *)[NSKeyedArchiver archivedDataWithRootObject:values[key]];
return values;
}
#ifdef OO_ARC
void ooArcRetain( id value ) {
if ( value && value != OONull ) {
static IMP retainIMP;
static SEL retainSEL;
if ( !retainIMP ) {
retainSEL = sel_getUid( "retain" );
Method method = class_getInstanceMethod( [value class], retainSEL );
retainIMP = method_getImplementation( method );
}
retainIMP( value, retainSEL );
}
}
#endif
- (cOOValueDictionary)decode:(cOOValueDictionary)values {
id value;
for ( NSString *key in *archived )
if ( (value = values[key]) )
values[key] = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)value];
for ( NSString *key in *dates )
if ( (value = values[key]) )
values[key] = [NSDate dateWithTimeIntervalSince1970:[value doubleValue]];
for ( NSString *key in *boxed ) {
if ( (value = values[key]) ) {
value = value != OONull ? OO_RETAIN( value ) : nil;
#ifdef OO_ARC
ooArcRetain( value );
#endif
OO_RELEASE( values[key] = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)] );
}
}
return values;
}
+ (OOArray<id>)import:(const OOArray<OODictionary<OOString> > &)nodes intoClass:(Class)recordClass {
OOMetaData *metaData = [self metaDataForClass:recordClass];
OOArray<id> out;
for ( NSMutableDictionary *dict in *nodes ) {
OOStringDictionary node = dict, values;
for ( NSString *ivar in *metaData->columns )
values[ivar] = node[ivar];
id record = [[recordClass alloc] init];
[record setValuesForKeysWithDictionary:[metaData decode:values]];
out += record;
OO_RELEASE( record );
}
return out;
}
+ (OOArray<id>)import:(cOOString)string intoClass:(Class)recordClass delimiter:(cOOString)delim {
OOMetaData *metaData = [self metaDataForClass:recordClass];
OOStringArray lines = (string - @"\\\\\n") / @"\n";
lines--;
OOArray<id> out;
for ( int l=0 ; l<lines ; l++ ) {
OODictionary<NSString *> values;
values[metaData->columns] = *lines[l] / delim;
for ( NSString *key in *metaData->columns )
if ( [*values[key] isEqualToString:@""] )
values[key] = OONull;
for ( NSString *key in *metaData->blobs )
OO_RELEASE( values[key] = (NSString *)[[NSData alloc] initWithDescription:values[key]] );
id record = [[recordClass alloc] init];
[record setValuesForKeysWithDictionary:[metaData decode:values]];
out += record;
OO_RELEASE( record );
}
return out;
}
+ (OOString)export:(const OOArray<id> &)array delimiter:(cOOString)delim {
OOMetaData *metaData = nil;
OOString out;
for ( id record in *array ) {
if ( !metaData )
metaData = [record isKindOfClass:[NSDictionary class]] ?
OONull : [self metaDataForClass:[record class]];
OODictionary<NSNumber *> values = metaData == OONull ? record :
*[metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
OOStringArray line;
NSString *blank = @"";
for ( NSString *key in *metaData->columns )
line += *values[key] != OONull ? [values[key] stringValue] : blank;
out += line/delim+"\n";
}
return out;
}
+ (void)bindRecord:(id)record toView:(OOView *)view delegate:(id)delegate {
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
OOMetaData *metaData = [self metaDataForClass:[record class]];
OOValueDictionary values = [metaData encode:[record dictionaryWithValuesForKeys:metaData->ivars]];
for ( int i=0 ; i<metaData->ivars ; i++ ) {
UILabel *label = (UILabel *)[view viewWithTag:1+i];
id value = values[metaData->ivars[i]];
if ( [label isKindOfClass:[UIImageView class]] )
((UIImageView *)label).image = value != OONull ? [UIImage imageWithData:(NSData *)value] : nil;
else if ( [label isKindOfClass:[UISwitch class]] ) {
UISwitch *uiSwitch = (UISwitch *)label;
uiSwitch.on = value != OONull ? [value charValue] : 0;
if ( delegate )
[uiSwitch addTarget:delegate action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged];
}
else if ( [label isKindOfClass:[UIWebView class]] )
[(UIWebView *)label loadHTMLString:value != OONull ? value : @"" baseURL:nil];
else if ( label ) {
label.text = value != OONull ? [value stringValue] : @"";
if ( [label isKindOfClass:[UITextView class]] ) {
[(UITextView *)label setContentOffset:CGPointMake(0,0) animated:NO];
[(UITextView *)label scrollRangeToVisible:NSMakeRange(0,0)];
}
}
if ( [label respondsToSelector:@selector(delegate)] )
((UITextField *)label).delegate = delegate;
label.hidden = NO;
if ( (label = (UILabel *)[view viewWithTag:-1-i]) ) {
label.text = **metaData->ivars[i];
label.hidden = NO;
}
}
OOView *subView;
for ( int i=metaData->ivars ; (subView = [view viewWithTag:1+i]) ; i++ ) {
subView.hidden = YES;
if ( (subView = [view viewWithTag:-1-i]) )
subView.hidden =YES;
}
#endif
}
+ (void)updateRecord:(id)record fromView:(OOView *)view {
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
if ( view.tag > 0 && [view respondsToSelector:@selector(text)] ) {
OOMetaData *metaData = [self metaDataForClass:[record class]];
NSString *name = *metaData->ivars[view.tag-1];
OOString type = metaData->types[name];
id value = OO_RETAIN(((UITextField *)view).text );
if ( type[0] == '{' ) {
#ifdef OO_ARC
ooArcRetain( value );
#endif
value = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)];
#ifndef OO_ARC
OO_RELEASE( (id)[[record valueForKey:name] pointerValue] );
#endif
}
[record setValue:value forKey:name];
OO_RELEASE( value );
}
for ( OOView *subview in [view subviews] )
[self updateRecord:record fromView:subview];
#endif
}
@end
@implementation OOView(OOExtras)
- copyView {
NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:self];
OOView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archived];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
copy.frame = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);
#else
copy.frame = NSMakeRect(0.0, 0.0, self.frame.size.width, self.frame.size.height);
#endif
return copy;
}
@end
@implementation NSData(OOExtras)
static int unhex ( unsigned char ch ) {
return ch >= 'a' ? 10 + ch - 'a' : ch >= 'A' ? 10 + ch - 'A' : ch - '0';
}
- initWithDescription:(NSString *)description {
NSInteger len = [description length]/2, lin = [description lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
char *bytes = (char *)malloc( len ), *optr = bytes, *hex = (char *)malloc( lin+1 );
[description getCString:hex maxLength:lin+1 encoding:NSUTF8StringEncoding];
for ( char *iptr = hex ; *iptr ; iptr+=2 ) {
if ( *iptr == '<' || *iptr == ' ' || *iptr == '>' )
iptr--;
else
*optr++ = unhex( *iptr )*16 + unhex( *(iptr+1) );
}
free( hex );
return [self initWithBytesNoCopy:bytes length:optr-bytes freeWhenDone:YES];
}
- (NSString *)stringValue { return [self description]; }
@end
@interface NSString(OOExtras)
@end
@implementation NSString(OOExtras)
- (char)charValue { return [self intValue]; }
- (char)shortValue { return [self intValue]; }
- (NSString *)stringValue { return self; }
@end
@interface NSArray(OOExtras)
@end
@implementation NSArray(OOExtras)
- (NSString *)stringValue {
static OOReplace reformat( "/(\\s)\\s+|^\\(|\\)$|\"/$1/" );
return &([self description] | reformat);
}
@end
@interface NSDictionary(OOExtras)
@end
@implementation NSDictionary(OOExtras)
- (NSString *)stringValue {
static OOReplace reformat( "/(\\s)\\s+|^\\{|\\}$|\"/$1/" );
return &([self description] | reformat);
}
@end
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
@interface UISwitch(OOExtras)
@end
@implementation UISwitch(OOExtras)
- (NSString *)text { return self.on ? @"1" : @"0"; }
@end
#endif