49 #import <objc/runtime.h>
62 static NSString *kOOObject =
@"__OOOBJECT__", *kOOInsert =
@"__ISINSERT__", *kOOUpdate =
@"__ISUPDATE__", *kOOExecSQL =
@"__OOEXEC__";
64 #pragma mark OORecord abstract superclass for records
78 + (id)insertWithParent:(
id)parent {
93 - (void)setNilValueForKey:(NSString *)key {
96 zeroForNull = [NSNumber numberWithInt:0];
97 [
self setValue:zeroForNull forKey:key];
108 + (
OOArray<id>)selectRecordsRelatedTo:(
id)parent {
139 - (void)bindToView:(
OOView *)view delegate:(
id)delegate {
159 return [*[metaData
encode:[
self dictionaryWithValuesForKeys:ivars]] description];
164 #pragma mark OOAdaptor - all methods required by objsql to access a database
174 struct _str_link *next;
char str[1];
183 - (
OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(
OOMetaData *)metaData;
184 - (sqlite_int64)lastInsertRowID;
188 @interface NSData(OOExtras)
189 - initWithDescription:(NSString *)description;
192 #pragma mark OODatabase is the low level interface to a particular database
222 va_list argp; va_start(argp, fmt);
223 NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
258 if (
self = [super init] )
268 int numClasses = objc_getClassList( NULL, 0 );
269 Class *classes = (Class *)malloc(
sizeof *classes * numClasses );
274 numClasses = objc_getClassList( classes, numClasses );
275 for (
int c=0 ; c<numClasses ; c++ ) {
276 Class superClass = classes[c];
277 if ( class_getName( superClass )[0] !=
'_' )
278 while ( (superClass = class_getSuperclass( superClass )) )
279 if ( superClass == recordSuperClass ) {
280 if ( [classes[c] respondsToSelector:
@selector(ooTableSql)] )
281 viewClasses += classes[c];
284 classNames += class_getName( classes[c] );
291 for (
int c=0 ; c<viewClasses ; c++ ) {
293 classNames += class_getName( viewClasses[c] );
306 for ( NSString *tableClass in *classes )
316 va_list argp; va_start(argp, fmt);
317 NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
319 results = [
self select:sql intoClass:NULL joinFrom:nil];
329 va_list argp; va_start(argp, fmt);
330 NSString *sql =
OO_AUTORELEASE( [[NSString alloc] initWithFormat:fmt arguments:argp] );
332 if( [
self exec:
"%@", sql] &&
results > 0 ) {
333 NSString *aColumnName = [[**
results[0] allKeys] objectAtIndex:0];
334 return [(NSNumber *)(*results[0])[aColumnName] stringValue];
344 - (id)copyJoinKeysFrom:(
id)parent to:(
id)newChild {
349 [newChild setValuesForKeysWithDictionary:[childMetaData decode:keyValues]];
359 for (
int i=0 ; i<columns ; i++ ) {
360 NSString *name = *columns[i];
361 const char *prefix = i==0 ?
"\nwhere":
" and";
363 id value = values[name];
364 if ( !value || value ==
OONull )
365 out += qualifyNulls ?
OOFormat(
@"%s\n\t%@ is NULL", prefix, name ) :
@"";
366 else if ( !qualifyNulls && [value isKindOfClass:[NSString
class]] &&
367 [value rangeOfString:
@"%"].location != NSNotFound )
368 out +=
OOFormat(
@"%s\n\t%@ LIKE ?", prefix, name );
370 out +=
OOFormat(
@"%s\n\t%@ = ?", prefix, name );
385 sharedColumns = [parentMetaData
naturalJoinTo:metaData->joinableColumns];
386 joinValues = [parentMetaData
encode:[parent dictionaryWithValuesForKeys:sharedColumns]];
391 if ( [metaData->
recordClass respondsToSelector:
@selector(ooOrderBy)] )
395 NSLog(
@"-[OOMetaData prepareSql:] %@\n%@", *sql, *joinValues );
401 return !parent || [*
adaptor bindCols:sharedColumns values:joinValues startingAt:1 bindNulls:NO];
417 if ( record && record != metaData )
418 for (
int i=0 ; i<tablesWithNaturalJoin ; i++ ) {
419 OOString sql =
OOFormat(
@"select count(*) as result from %@", **tablesWithNaturalJoin[i] );
425 if ( ![(*tmpResults[0])[
"result"] intValue] )
426 ~tablesWithNaturalJoin[i--];
438 - (
OOArray<id>)select:(
cOOString)select intoClass:(Class)recordClass joinFrom:(
id)parent {
439 OOMetaData *metaData = [
self tableMetaDataForClass:recordClass ? recordClass : [parent class]];
443 if ( ![
self prepareSql:sql joinFrom:parent toTable:metaData] )
446 return [*adaptor bindResultsIntoInstancesOfClass:recordClass metaData:metaData];
450 return [
self select:select intoClass:recordClass joinFrom:nil];
454 return [
self select:select intoClass:nil joinFrom:nil];
461 - (
long long)rowIDForRecord:(
id)record {
465 return [*(*idResults[0])[@"rowid"] longLongValue];
473 return [*
adaptor lastInsertRowID];
480 - (int)insertArray:(const
OOArray<
id> &)objects {
482 for (
id object in *objects )
483 count = [
self insert:object];
491 - (int)deleteArray:(const
OOArray<
id> &)objects {
493 for (
id object in *objects )
494 if ( ![
object respondsToSelector:
@selector(
delete)] )
495 count = [
self delete:object];
508 - (int)insert:(
id)record {
518 - (int)delete:(
id)record {
528 - (int)update:(
id)record {
529 OOMetaData *metaData = [
self tableMetaDataForClass:[record class]];
531 for ( NSString *key in *metaData->
tocopy )
532 OO_RELEASE( oldValues[key] = [oldValues[key] copy] );
533 oldValues[kOOUpdate] = (id)kOOUpdate;
534 oldValues[kOOObject] = record;
543 - (int)indate:(
id)record {
544 OOMetaData *metaData = [
self tableMetaDataForClass:[record class]];
546 OOArray<id> existing = [
self select:sql intoClass:nil joinFrom:record];
547 int count = [
self insert:record];
548 for ( NSDictionary *exist in *existing ) {
550 (
long)[[exist objectForKey:
@"rowid"] longLongValue] );
561 - (int)upsert:(
id)record {
562 OOArray<id> existing = [
self select:nil intoClass:[record class] joinFrom:record];
564 OOWarn(
@"-[ODatabase upsert:] Duplicate record for upsert: %@", record );
565 if ( existing > 0 ) {
566 [
self update:existing[0]];
571 return [
self insert:record];
584 OOString exec = (NSMutableString *)~values[kOOExecSQL];
586 if ( ![
self exec:
@"%@", *exec] )
587 OOWarn(
@"-[ODatabase commit] Error in transaction exec: %@ - %s", *exec,
errmsg );
591 OORef<NSObject *>
object = *values[kOOObject]; values -= kOOObject;
592 BOOL isInsert = !!~values[kOOInsert], isUpdate = !!~values[kOOUpdate];
594 OOMetaData *metaData = [
self tableMetaDataForClass:[object class]];
599 for ( NSString *name in *metaData->
columns )
600 if ( ![*newValues[name] isEqual:values[name]] )
608 OOFormat( isUpdate ?
@"update %@ set" :
@"delete from %@", *metaData->tableName );
610 int nchanged = changedCols;
611 if ( isUpdate && nchanged == 0 ) {
612 OOWarn(
@"%s %@ (%@)",
errmsg = (
char *)
"-[ODatabase commit:] Update of unchanged record", *
object, *(
lastSQL = sql) );
616 for (
int i=0 ; i<nchanged ; i++ )
617 sql += OOFormat(
@"%s\n\t%@ = ?", i==0 ? "" : ",", **changedCols[i] );
620 OOString quote =
"?", commaQuote =
", ?";
621 for (
int i=0 ; i<metaData->
columns ; i++ )
622 sql += i==0 ? quote : commaQuote;
626 sql += [self whereClauseFor:metaData->columns values:values qualifyNulls:YES];
629 NSLog(
@"-[OODatabase commit]: %@ %@", *sql, *values );
636 [*adaptor bindCols:changedCols values:newValues startingAt:1 bindNulls:YES];
637 [*adaptor bindCols:metaData->columns values:values startingAt:1+nchanged bindNulls:isInsert];
639 [*adaptor bindResultsIntoInstancesOfClass:nil metaData:metaData];
652 [
self exec:"BEGIN TRANSACTION"];
653 int updated = [
self commit];
654 return [
self exec:"COMMIT"] ? updated : 0;
663 for ( NSMutableDictionary *d in *transaction ) {
666 if ( !!~values[kOOUpdate] ) {
667 OORef<OORecord *> record = ~values[kOOObject];
668 OOMetaData *metaData = [
self tableMetaDataForClass:[*record class]];
671 for ( NSString *name in *metaData->
boxed )
672 OO_RELEASE( (
id)[[*record valueForKey:name] pointerValue] );
675 [*record setValuesForKeysWithDictionary:[metaData
decode:values]];
678 return (
int)[*~transaction count];
691 if ( !recordClass || recordClass == [
OOMetaData class] )
694 OOString className = class_getName( recordClass );
705 [
self stringForSql:
"select count(*) from sqlite_master where name = '%@'",
708 for ( NSString *idx in *metaData->
indexes )
709 if ( ![
self exec:idx] )
710 OOWarn(
@"-[OOMetaData tableMetaDataForClass:] Error creating index: %@", idx );
720 #pragma mark OOAdaptor - implements all access to a particular database
729 if (
self = [super init] ) {
732 if ( (
owner->
errcode = sqlite3_open( path, &
db )) != SQLITE_OK ) {
733 OOWarn(
@"-[OOAdaptor initPath:database:] Error opening database at path: %@", *path );
750 - (int)bindValue:(
id)value asParameter:(
int)pno {
752 NSLog(
@"-[OOAdaptor bindValue:bindValue:] bind parameter #%d as: %@", pno, value );
754 if ( !value || value ==
OONull )
755 return sqlite3_bind_null(
stmt, pno );
756 #if OOSQL_THREAD_SAFE_BUT_USES_MORE_MEMORY
757 else if ( [value isKindOfClass:[NSString
class]] )
758 return sqlite3_bind_text(
stmt, pno, [value UTF8String], -1, SQLITE_STATIC );
760 else if ( [value isKindOfClass:[NSString
class]] ) {
761 int len = (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
762 struct _str_link *str = (
struct _str_link *)malloc(
sizeof *str + len );
765 [value getCString:str->str maxLength:len+1 encoding:NSUTF8StringEncoding];
766 return sqlite3_bind_text( stmt, pno, str->str, len, SQLITE_STATIC );
769 else if ( [value isKindOfClass:[NSData
class]] )
770 return sqlite3_bind_blob( stmt, pno, [value bytes], (
int)[value length], SQLITE_STATIC );
772 const char *type = [value objCType];
775 case 'c':
case 's':
case 'i':
case 'l':
776 case 'C':
case 'S':
case 'I':
case 'L':
777 return sqlite3_bind_int( stmt, pno, [value intValue] );
779 return sqlite3_bind_int64( stmt, pno, [value longLongValue] );
781 return sqlite3_bind_double( stmt, pno, [value doubleValue] );
784 OOWarn(
@"-[OOAdaptor bindValue:bindValue:] Undefined type in bind of parameter #%d: %s, value: %@", pno, type, value );
794 for ( NSString *name in *columns )
795 if ( bindNulls || *values[name] !=
OONull )
796 if ( (errcode = [
self bindValue:values[name] asParameter:pno++]) != SQLITE_OK )
797 OOWarn(
@"-[OOAdaptor bindCols:...] Bind failed column: %@ - %s (%d)", name, owner->errmsg = (
char *)sqlite3_errmsg( db ), owner->errcode = errcode );
798 return owner->errcode == SQLITE_OK;
807 int ncols = sqlite3_column_count( stmt );
810 for (
int i=0 ; i<ncols ; i++ ) {
811 OOString name = sqlite3_column_name( stmt, i );
814 switch ( sqlite3_column_type( stmt, i ) ) {
819 value = [[NSNumber alloc] initWithLongLong:sqlite3_column_int64( stmt, i )];
822 value = [[NSNumber alloc] initWithDouble:sqlite3_column_double( stmt, i )];
825 const unsigned char *bytes = sqlite3_column_text( stmt, i );
826 value = [[NSMutableString alloc] initWithBytes:bytes
827 length:sqlite3_column_bytes( stmt, i)
828 encoding:NSUTF8StringEncoding];
832 const void *bytes = sqlite3_column_blob( stmt, i );
833 value = [[NSData alloc] initWithBytes:bytes length:sqlite3_column_bytes( stmt, i )];
837 OOWarn(
@"-[OOAdaptor valuesForNextRow:] Invalid type on bind of ivar %@: %d", *name, sqlite3_column_type( stmt, i ) );
840 values[name] = value;
852 - (
OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(
OOMetaData *)metaData {
854 BOOL awakeFromDB = [recordClass instancesRespondToSelector:@selector(awakeFromDB)];
856 while( (owner->errcode = sqlite3_step( stmt )) == SQLITE_ROW ) {
859 id record = [[recordClass alloc] init];
860 [record setValuesForKeysWithDictionary:[metaData
decode:values]];
863 [record awakeFromDB];
872 if ( owner->errcode != SQLITE_DONE )
873 OOWarn(
@"-[OOAdaptor bindResultsIntoInstancesOfClass:metaData:] Not done (bind) stmt: %@ - %s", *owner->lastSQL, owner->errmsg = (
char *)sqlite3_errmsg( db ) );
875 owner->errcode = SQLITE_OK;
879 while ( strs != NULL ) {
880 struct _str_link *next = strs->next;
884 owner->updateCount = sqlite3_changes( db );
885 sqlite3_finalize( stmt );
889 - (sqlite_int64)lastInsertRowID {
890 return sqlite3_last_insert_rowid( db );
900 #pragma mark OOMetaData instances represent a table in the database and it's record class
909 + (
OOMetaData *)metaDataForClass:(Class)recordClass OO_RETURNS {
910 if ( !tableOfTables )
912 OOMetaData *metaData = metaDataByClass[recordClass];
914 OO_RELEASE( metaData = [[OOMetaData alloc] initClass:recordClass] );
918 + (
OOArray<id>)selectRecordsRelatedTo:(
id)record {
922 - initClass:(Class)aClass {
923 if ( !(
self = [super init]) )
926 metaDataByClass[recordClass] =
self;
935 "tableTitle tableName recordClassName keyColumns ivars columns outcols";
944 while ( (aClass = [aClass superclass]) && aClass != [NSObject
class] );
946 for (
int h=(
int)hierarchy-1 ; h>=0 ; h-- ) {
947 aClass = (Class)hierarchy[h];
948 Ivar *ivarInfo = class_copyIvarList( aClass, NULL );
952 for (
int in=0 ; ivarInfo[in] ; in++ ) {
953 OOString columnName = ivar_getName( ivarInfo[in] );
956 OOString type =
types[columnName] = ivar_getTypeEncoding( ivarInfo[in] ), dbtype =
"";
958 SEL columnSel = sel_getUid(columnName);
960 case 'c':
case 's':
case 'i':
case 'l':
961 case 'C':
case 'S':
case 'I':
case 'L':
969 if( !type[
"=\"ref\"@\"NS"] )
970 OOWarn(
@"-[OOMetaData initClass:] Invalid structure type for ivar %@ in class %@: %@", *columnName, *
recordClassName, *type );
972 if ( ![
recordClass instancesRespondToSelector:columnSel] ) {
974 if ( [[
recordClass superclass] instancesRespondToSelector:columnSel] )
975 OOWarn(
@"-[OOMetaData initClass:] Superclass of class %@ is providing method for column: %@", *
recordClassName, *columnName );
978 if ( !!type[
"NS(Mutable)?String\""] )
980 else if ( !!type[
"\"NSDate\""] ) {
985 if ( !type[
"NS(Mutable)?Data\""] )
992 OOWarn(
@"-[OOMetaData initClass:] Unknown data type '%@' in class %@", *type, *
tableName );
999 if ( dbtype ==
@"text" )
1002 if ( columnName ==
@"rowid" || columnName ==
@"ROWID" ||
1003 columnName ==
@"OID" || columnName ==
@"_ROWID_" ) {
1009 !
columns?
"":
",", *columnName, *dbtype, *type );
1011 if ( iswupper( columnName[columnName[0] !=
'_' ? 0 : 1] ) )
1016 if ( class_getName( [aClass superclass] )[0] !=
'_' ) {
1026 if ( [
recordClass respondsToSelector:
@selector(ooTableKey)] )
1030 if ( [
recordClass respondsToSelector:
@selector(ooConstraints)] )
1035 if ( [
recordClass respondsToSelector:
@selector(ooTableSql)] ) {
1043 for( Class other in [*metaDataByClass allKeys] ) {
1044 OOMetaData *otherMetaData = metaDataByClass[other];
1045 if ( other ==
recordClass || otherMetaData == tableOfTables )
1064 for (
int i=0 ; i<commonColumns ; i++ )
1065 if ( islower( (*commonColumns[i])[0] ) )
1066 ~commonColumns[i--];
1067 return commonColumns;
1076 for ( NSString *key in *
unbox ) {
1077 id value = (id)[*values[key] pointerValue];
1078 values[key] = value ? value :
OONull;
1080 for ( NSString *key in *
dates )
1081 values[key] = [NSNumber numberWithDouble:[(id)values[key] timeIntervalSince1970]];
1083 values[key] = (NSValue *)[NSKeyedArchiver archivedDataWithRootObject:values[key]];
1094 if ( (value = values[key]) )
1095 values[key] = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)value];
1096 for ( NSString *key in *
dates )
1097 if ( (value = values[key]) )
1098 values[key] = (
id)[NSDate dateWithTimeIntervalSince1970:[value doubleValue]];
1099 for ( NSString *key in *
boxed ) {
1100 if ( (value = values[key]) ) {
1106 OO_RELEASE( values[key] = [[NSValue alloc] initWithBytes:&value objCType:@encode(
id)] );
1120 for ( NSMutableDictionary *dict in *nodes ) {
1122 for ( NSString *ivar in *metaData->
columns )
1124 values[ivar] = node[ivar];
1126 id record = [[recordClass alloc] init];
1127 [record setValuesForKeysWithDictionary:[metaData
decode:values]];
1146 for (
int l=0 ; l<lines ; l++ ) {
1148 values[metaData->
columns] = *lines[l] / delim;
1151 for ( NSString *key in *metaData->
columns )
1152 if ( [*values[key] isEqualToString:
@""] )
1156 for ( NSString *key in *metaData->
blobs )
1157 OO_RELEASE( values[key] = (NSString *)[[NSData alloc] initWithDescription:values[key]] );
1160 [record setValuesForKeysWithDictionary:[metaData
decode:values]];
1176 for (
id record in *array ) {
1178 metaData = [record isKindOfClass:[NSDictionary class]] ?
1182 *[metaData
encode:[record dictionaryWithValuesForKeys:metaData->columns]];
1185 NSString *blank =
@"";
1186 for ( NSString *key in *metaData->
columns )
1187 line += *values[key] !=
OONull ? [values[key] stringValue] : blank;
1189 out += line/delim+
"\n";
1202 + (void)bindRecord:(
id)record toView:(
OOView *)view delegate:(
id)delegate {
1203 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1207 for (
int i=0 ; i<metaData->
ivars ; i++ ) {
1208 UILabel *label = (UILabel *)[view viewWithTag:1+i];
1209 id value = values[metaData->ivars[i]];
1211 if ( [label isKindOfClass:[UIImageView
class]] )
1212 ((UIImageView *)label).image = value !=
OONull ? [UIImage imageWithData:(NSData *)value] : nil;
1213 else if ( [label isKindOfClass:[UISwitch
class]] ) {
1214 UISwitch *uiSwitch = (UISwitch *)label;
1215 uiSwitch.on = value !=
OONull ? [value charValue] : 0;
1217 [uiSwitch addTarget:delegate action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged];
1219 else if ( [label isKindOfClass:[UIWebView
class]] )
1220 [(UIWebView *)label loadHTMLString:value != OONull ? value : @"" baseURL:nil];
1222 label.text = value !=
OONull ? [value stringValue] :
@"";
1223 if ( [label isKindOfClass:[UITextView
class]] ) {
1224 [(UITextView *)label setContentOffset:CGPointMake(0,0) animated:NO];
1225 [(UITextView *)label scrollRangeToVisible:NSMakeRange(0,0)];
1229 if ( [label respondsToSelector:
@selector(delegate)] )
1230 ((UITextField *)label).delegate = delegate;
1233 if ( (label = (UILabel *)[view viewWithTag:-1-i]) ) {
1234 label.text = **metaData->
ivars[i];
1240 for (
int i=metaData->
ivars ; (subView = [view viewWithTag:1+i]) ; i++ ) {
1241 subView.hidden = YES;
1242 if ( (subView = [view viewWithTag:-1-i]) )
1243 subView.hidden =YES;
1253 + (void)updateRecord:(
id)record fromView:(
OOView *)view {
1254 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1255 if ( view.tag > 0 && [view respondsToSelector:
@selector(text)] ) {
1257 NSString *name = *metaData->
ivars[view.tag-1];
1259 id value =
OO_RETAIN(((UITextField *)view).text );
1261 if ( type[0] ==
'{' ) {
1266 value = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)];
1268 OO_RELEASE( (
id)[[record valueForKey:name] pointerValue] );
1272 [record setValue:value forKey:name];
1275 for (
OOView *subview in [view subviews] )
1285 NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:self];
1286 OOView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archived];
1287 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1288 copy.frame = CGRectMake(0.0, 0.0,
self.frame.size.width,
self.frame.size.height);
1290 copy.frame = NSMakeRect(0.0, 0.0,
self.frame.size.width,
self.frame.size.height);
1297 @implementation NSData(OOExtras)
1299 static int unhex (
unsigned char ch ) {
1300 return ch >=
'a' ? 10 + ch - 'a' : ch >= 'A' ? 10 + ch - 'A' : ch - '0';
1303 - initWithDescription:(NSString *)description {
1304 NSInteger len = [description length]/2, lin = [description lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1305 char *bytes = (
char *)malloc( len ), *optr = bytes, *hex = (
char *)malloc( lin+1 );
1306 [description getCString:hex maxLength:lin+1 encoding:NSUTF8StringEncoding];
1308 for (
char *iptr = hex ; *iptr ; iptr+=2 ) {
1309 if ( *iptr ==
'<' || *iptr ==
' ' || *iptr ==
'>' )
1312 *optr++ = unhex( *iptr )*16 + unhex( *(iptr+1) );
1316 return [
self initWithBytesNoCopy:bytes length:optr-bytes freeWhenDone:YES];
1319 - (NSString *)stringValue {
return [
self description]; }
1323 @interface NSString(OOExtras)
1325 @implementation NSString(OOExtras)
1326 - (char)charValue {
return [
self intValue]; }
1327 - (char)shortValue {
return [
self intValue]; }
1328 - (NSString *)stringValue {
return self; }
1331 @interface NSArray(OOExtras)
1333 @implementation NSArray(OOExtras)
1334 - (NSString *)stringValue {
1335 static OOReplace reformat(
"/(\\s)\\s+|^\\(|\\)$|\"/$1/" );
1336 return &([
self description] | reformat);
1340 @interface NSDictionary(OOExtras)
1342 @implementation NSDictionary(OOExtras)
1343 - (NSString *)stringValue {
1344 static OOReplace reformat(
"/(\\s)\\s+|^\\{|\\}$|\"/$1/" );
1345 return &([
self description] | reformat);
1349 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1353 - (NSString *)
text {
return self.on ?
@"1" :
@"0"; }