mirror of
				https://github.com/KevinMidboe/linguist.git
				synced 2025-10-29 17:50:22 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1373 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			1373 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| /*
 | |
|  *  objsql.m - implementaion simple persistence layer using objcpp.h
 | |
|  *  ========
 | |
|  *
 | |
|  *  Created by John Holdsworth on 01/04/2009.
 | |
|  *  Copyright 2009 John Holdsworth.
 | |
|  *
 | |
|  *  $Id: //depot/4.4/ObjCpp/objsql.mm#11 $
 | |
|  *  $DateTime: 2012/09/05 00:20:47 $
 | |
|  *
 | |
|  *  C++ classes to wrap up XCode classes for operator overload of
 | |
|  *  useful operations such as access to NSArrays and NSDictionary
 | |
|  *  by subscript or NSString operators such as + for concatenation.
 | |
|  *
 | |
|  *  This works as the Apple Objective-C compiler supports source
 | |
|  *  which mixes C++ with objective C. To enable this: for each
 | |
|  *  source file which will include/import this header file, select
 | |
|  *  it in Xcode and open it's "Info". To enable mixed compilation,
 | |
|  *  for the file's "File Type" select: "sourcecode.cpp.objcpp".
 | |
|  *
 | |
|  *  For bugs or ommisions please email objcpp@johnholdsworth.com
 | |
|  *
 | |
|  *  Home page for updates and docs: http://objcpp.johnholdsworth.com
 | |
|  *
 | |
|  *  You may make commercial use of this source in applications without
 | |
|  *  charge but not sell it as source nor can you remove this notice from
 | |
|  *  this source if you redistribute. You can make any changes you like
 | |
|  *  to this code before redistribution but please annotate them below.
 | |
|  *
 | |
|  *  If you find it useful please send a donation via paypal to account
 | |
|  *  objcpp@johnholdsworth.com. Thanks.
 | |
|  *
 | |
|  *  THIS CODE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND EITHER
 | |
|  *  EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 | |
|  *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 | |
|  *
 | |
|  *  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 | |
|  *  WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
 | |
|  *  THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
 | |
|  *  ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT
 | |
|  *  OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
 | |
|  *  TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED 
 | |
|  *  BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH 
 | |
|  *  ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN
 | |
|  *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 | |
|  *
 | |
|  */
 | |
| 
 | |
| #import <objc/runtime.h>
 | |
| #import <sqlite3.h>
 | |
| 
 | |
| #import "objsql.h"
 | |
| 
 | |
| #if 0
 | |
| #ifdef OODEBUG
 | |
| #define OODEBUG_SQL  1
 | |
| #endif
 | |
| #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]; }
 | |
| 
 | |
| // to handle null values for ints, floats etc.
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  import a flat file with column values separated by the delimiter specified into 
 | |
|  the table associated with this class.
 | |
|  */
 | |
| 
 | |
| + (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Export a flat file with all rows in the table associated with the record subclass.
 | |
|  */
 | |
| 
 | |
| + (BOOL)exportTo:(OOFile &)file delimiter:(cOOString)delim {
 | |
| 	return file.save( [OOMetaData export:[self select] delimiter:delim] );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  populate a view with the string values taken from the ivars of the record
 | |
|  */
 | |
| 
 | |
| - (void)bindToView:(OOView *)view delegate:(id)delegate {
 | |
| 	[OOMetaData bindRecord:self toView:view delegate:delegate];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  When the delegate method os sent use this method to update the record's values
 | |
|  */
 | |
| 
 | |
| - (void)updateFromView:(OOView *)view {
 | |
| 	[OOMetaData updateRecord:self fromView:view];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Default description is dictionary containing values of all ivars
 | |
|  */
 | |
| 
 | |
| - (NSString *)description {
 | |
| 	OOMetaData *metaData = [OOMetaData metaDataForClass:[self class]];
 | |
| 	// hack required where record contains a field "description" to avoid recursion
 | |
| 	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
 | |
| 
 | |
| /**
 | |
|  An internal class representing the interface to a particular database, in this case sqlite3.
 | |
|  */
 | |
| 
 | |
| @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;
 | |
| 
 | |
| /**
 | |
|  By default database file is "objsql.db" in the user/application's "Documents" directory
 | |
|  and a single shared OODatabase instance used for all db operations.
 | |
|  */
 | |
| 
 | |
| + (OODatabase *)sharedInstance {
 | |
|     if ( !sharedInstance )
 | |
|         [self sharedInstanceForPath:OODocument("objsql.db").path()]; 
 | |
| 	return sharedInstance;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Shared instance can be switched between any file paths.
 | |
|  */
 | |
|  
 | |
|  + (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]; }
 | |
| 
 | |
| /**
 | |
|  Designated initialiser for OODatabase instances. Generally only the shared instance is used
 | |
|  and the OODatabase class object is messaged instead.
 | |
|  */
 | |
| 
 | |
| - initPath:(cOOString)path {
 | |
| 	if ( self = [super init] )
 | |
| 		OO_RELEASE( adaptor = [[OOAdaptor alloc] initPath:path database:self] );
 | |
| 	return self;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Automatically register all classes which are subclasses of a record abstract superclass (e.g. OORecord).
 | |
|  */
 | |
| 
 | |
| - (OOStringArray)registerSubclassesOf:(Class)recordSuperClass {
 | |
| 	int numClasses = objc_getClassList( NULL, 0 );
 | |
| 	Class *classes = (Class *)malloc( sizeof *classes * numClasses );
 | |
| 	OOArray<Class> viewClasses;
 | |
| 	OOStringArray classNames;
 | |
| 
 | |
| 	// scan all registered classes for relevant subclasses
 | |
| 	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;
 | |
|                 }
 | |
| 	}
 | |
| 
 | |
| 	// delay creation views until after tables
 | |
| 	for ( int c=0 ; c<viewClasses ; c++ ) {
 | |
| 		[[OODatabase sharedInstance] tableMetaDataForClass:viewClasses[c]];
 | |
| 		classNames += class_getName( viewClasses[c] );
 | |
| 	}
 | |
| 
 | |
| 	// return classes in order registered
 | |
| 	free( classes );
 | |
| 	return classNames;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Register a list of classes before using them so OODatabase can determine the relationships between them.
 | |
|  */
 | |
| 
 | |
| - (void)registerTableClassesNamed:(cOOStringArray)classes {
 | |
| 	for ( NSString *tableClass in *classes )
 | |
| 		[self tableMetaDataForClass:[[NSBundle mainBundle] classNamed:tableClass]];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Send any SQL to the database. Sql is a format string so escape any '%' characters using '%%'.
 | |
|  Any results returned are placed as an array of dictionary values in the database->results.
 | |
|  */
 | |
|  
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Return a single value from row 1, column one from sql sent to the database as a string.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Used to initialise new child records automatically from parent in relation.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Build a where clause for the columns specified.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Prepare the sql passed in adding a where clause with bindings for a join to values taken from the parent record.
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Determine a list of the tables which have a natural join to the record passed in.
 | |
|  If the record is a specific instance of from a table this should determine if
 | |
|  there are any record which exist using the join.
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Perform a select from a table on the database using either the sql specified 
 | |
|  orselect all columns from the table associated with the record class passed in.
 | |
|  If a parent is passed in make a natural join from that record.
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Returns sqlite3 row identifier for a record instance.
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Returns sqlite3 row identifier for last inserted record.
 | |
|  */
 | |
| 
 | |
| - (long long)lastInsertRowID {
 | |
| 	return [*adaptor lastInsertRowID];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Insert an array of record objects into the database. This needs to be commited to take effect.
 | |
|  */
 | |
| 
 | |
| - (int)insertArray:(const OOArray<id> &)objects {
 | |
| 	int count = 0;
 | |
| 	for ( id object in *objects )
 | |
| 		count = [self insert:object];
 | |
| 	return count;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Delete an array of record objects from the database. This needs to be commited to take effect.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Insert the values of the record class instance at the time this method was called into the db.
 | |
|  (must be commited to take effect). Returns the total number of outstanding inserts/updates/deletes.
 | |
|  */
 | |
| 
 | |
| - (int)insert:(id)record {
 | |
| 	return transaction += OOValueDictionary( kOOObject, record, kOOInsert, kCFNull, nil );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Use the values of the record instance at the time this method is called in a where clause to 
 | |
|  delete from the database when commit is called. Returns the total number of outstanding
 | |
|  inserts/updates/deletes.
 | |
|  */
 | |
| 
 | |
| - (int)delete:(id)record {
 | |
| 	return transaction += OOValueDictionary( kOOObject, record, nil );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Call this method if you intend to make changes to the record object and save them to the database.
 | |
|  This takes a snapshot of the previous values to use as a key for the update operation when "commit"
 | |
|  is called. Returns the total number of outstanding inserts/updates/deletes.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Inserts a record into the database the deletes any previous record with the same key.
 | |
|  This ensures the record's rowid changes if this is used by child records.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Inserts a record into the database unless another record with the same key column values
 | |
|  exists in which case it will do an update of the previous record (preserving the ROWID.)
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Commit all pending inserts, updates and deletes to the database. Use commitTransaction to perform 
 | |
|  this inside a database transaction.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Commit all pending inserts, updates, deletes to the database inside a transaction.
 | |
|  */
 | |
| 
 | |
| - (int)commitTransaction {
 | |
| 	[self exec:"BEGIN TRANSACTION"];
 | |
| 	int updated = [self commit];
 | |
| 	return [self exec:"COMMIT"] ? updated : 0;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Rollback any outstanding inserts, updates, or deletes. Please note updated values 
 | |
|  are also rolled back inside the actual record in the application as well.
 | |
|  */
 | |
| 
 | |
| - (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];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Find/create an instance of the OOMetaData class which describes a record class and its associated table.
 | |
|  If the table does not exist it will be created along with indexes for columns/ivars which have 
 | |
|  upper case names for use in joins. Details of the tables parameters can be controlled by using
 | |
|  methods in the OOTableCustomisation protool. If it does not exist a meta table class which
 | |
|  prepresents OOMetaData records themselves is also created from which the list of all registered
 | |
|  tables can be selected.
 | |
|  */
 | |
|  
 | |
| - (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 
 | |
| 
 | |
| /**
 | |
|  Connect to/create sqlite3 database
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Prepare a sql statement after which values can be bound and results returned.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Bind parameters from a prepared SQL statement. The "objCType" method is used to determine the type to bind.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Return a dictionary containing the values for a row returned by the database from a select.
 | |
|  These values need to be decoded using a classes metadata to set the ivar values later.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Create instances of the recordClass to store results from a database select or if the record
 | |
|  class is not present return a list of dictionary objects with the raw results.
 | |
|  */
 | |
| 
 | |
| - (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Find the columns shared between two classes and that have upper case names (are indexed).
 | |
|  */
 | |
| 
 | |
| - (OOStringArray)naturalJoinTo:(cOOStringArray)to {
 | |
|     //NSLog( @"%@ -- %@", *columns, *to );
 | |
| 	OOStringArray commonColumns = columns & to;
 | |
| 	for ( int i=0 ; i<commonColumns ; i++ )
 | |
| 		if ( islower( (*commonColumns[i])[0] ) )
 | |
| 			~commonColumns[i--];
 | |
| 	return commonColumns;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Encode values ready for insertion into the database (convert OOString to NSString etc.)
 | |
|  */
 | |
| 
 | |
| - (cOOValueDictionary)encode:(cOOValueDictionary)values {
 | |
|     //NSLog( @"%@ - unbox: %@, dates: %@, archived: %@", *values, *unbox, *dates, *archived );
 | |
| 	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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Decode values taken from the database for use in [record setValuesForKeysWithDictionary:values];
 | |
|  */
 | |
| 
 | |
| #ifdef OO_ARC
 | |
| void ooArcRetain( id value ) {
 | |
|     // hack to retain value encoded as pointer
 | |
|     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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  import values for a list of nodes selected from an XML document.
 | |
|  */
 | |
| 
 | |
| + (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Convert a string taken from a flat file into record instances which can be inserted into the database.
 | |
|  */
 | |
| 
 | |
| + (OOArray<id>)import:(cOOString)string intoClass:(Class)recordClass delimiter:(cOOString)delim {
 | |
| 	OOMetaData *metaData = [self metaDataForClass:recordClass];
 | |
| 	// remove escaped newlines then split by newline
 | |
| 	OOStringArray lines = (string - @"\\\\\n") / @"\n";
 | |
| 	lines--; // pop last empty line
 | |
| 
 | |
| 	OOArray<id> out;
 | |
| 	for ( int l=0 ; l<lines ; l++ ) {
 | |
| 		OODictionary<NSString *> values;
 | |
| 		values[metaData->columns] = *lines[l] / delim;
 | |
| 
 | |
| 		// empty columns are taken as null values
 | |
| 		for ( NSString *key in *metaData->columns )
 | |
| 			if ( [*values[key] isEqualToString:@""] )
 | |
| 				values[key] = OONull;
 | |
| 
 | |
| 		// convert description strings to NSData
 | |
| 		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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Convert a set of records selected from the database into a string which can be saved to disk.
 | |
|  */
 | |
| 
 | |
| + (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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  Bind a record to a view containing elements which are to display values from the record.
 | |
|  The ivar number is selected by the subview's tag value and it's ".text" property if set to
 | |
|  the value returned record value "stringValue" for the ivar. Supports images stored as
 | |
|  NSData objects, UISwitches bound to boolean valuea and UITextField for alll other values.
 | |
|  */
 | |
| 
 | |
| + (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
 | |
| }
 | |
| 
 | |
| /**
 | |
|  When the delegate method fires this method should be called to update
 | |
|  the record with the modified value before updating the database.
 | |
|  */
 | |
| 
 | |
| + (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
 | |
| 
 |