mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +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
|
|
|