// // Log.m // Geopher Lite // // Created by Branden Russell on 8/16/08. // Copyright 2008 Stone Software and Branden Russell. All rights reserved. // #import "Log.h" // Static variables for compiled SQL queries. This implementation choice is to be able to share a one time // compilation of each query across all instances of the class. Each time a query is used, variables may be bound // to it, it will be "stepped", and then reset for the next usage. When the application begins to terminate, // a class method will be invoked to "finalize" (delete) the compiled queries - this must happen before the database // can be closed. static sqlite3_stmt *insert_statement = nil; static sqlite3_stmt *init_statement = nil; static sqlite3_stmt *delete_statement = nil; static sqlite3_stmt *hydrate_statement = nil; static sqlite3_stmt *dehydrate_statement = nil; @implementation Log @synthesize tableName; // Finalize (delete) all of the SQLite compiled queries. + (void)finalizeStatements { if (insert_statement) sqlite3_finalize(insert_statement); if (init_statement) sqlite3_finalize(init_statement); if (delete_statement) sqlite3_finalize(delete_statement); if (hydrate_statement) sqlite3_finalize(hydrate_statement); if (dehydrate_statement) sqlite3_finalize(dehydrate_statement); } // Creates the object with primary key and title is brought into memory. - (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db tableName:(NSString *)tn { if ( (self = [super init]) ) { primaryKey = pk; database = db; tableName = tn; // Compile the query for retrieving log data. See insertIntoDatabase: for more detail. if (init_statement == nil) { // Note the '?' at the end of the query. This is a parameter which can be replaced by a bound variable. // This is a great way to optimize because frequently used queries can be compiled once, then with each // use new variable values can be bound to placeholders. strInit = [NSString stringWithFormat:@"SELECT date FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strInit UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &init_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // For this query, we bind the primary key to the first (and only) placeholder in the statement. // Note that the parameters are numbered from 1, not from 0. sqlite3_bind_int(init_statement, 1, primaryKey); if (sqlite3_step(init_statement) == SQLITE_ROW) { self.date = [NSDate dateWithTimeIntervalSince1970:sqlite3_column_double(init_statement, 0)]; } else { self.date = [NSDate date]; } // Reset the statement for future reuse. sqlite3_reset(init_statement); dirty = NO; } return self; } - (void)insertIntoDatabase:(sqlite3 *)db { database = db; // This query may be performed many times during the run of the application. As an optimization, a static // variable is used to store the SQLite compiled byte-code for the query, which is generated one time - the first // time the method is executed by any log object. if (insert_statement == nil) { strInsertIntoDatabase = [NSString stringWithFormat:@"INSERT INTO %@ (date) VALUES(?)", self.tableName]; const char *sql = [strInsertIntoDatabase UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &insert_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } sqlite3_bind_double(insert_statement, 1, [date timeIntervalSince1970]); int success = sqlite3_step(insert_statement); // Because we want to reuse the statement, we "reset" it instead of "finalizing" it. sqlite3_reset(insert_statement); if (success == SQLITE_ERROR) { NSAssert1(0, @"Error: failed to insert into the database with message '%s'.", sqlite3_errmsg(database)); } else { // SQLite provides a method which retrieves the value of the most recently auto-generated primary key sequence // in the database. To access this functionality, the table should have a column declared of type // "INTEGER PRIMARY KEY" primaryKey = sqlite3_last_insert_rowid(database); } // All data for the log is already in memory, but has not be written to the database // Mark as hydrated to prevent empty/default values from overwriting what is in memory hydrated = YES; } - (void)deleteFromDatabase { // Compile the delete statement if needed. if (delete_statement == nil) { strDeleteFromDatabase = [NSString stringWithFormat:@"DELETE FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strDeleteFromDatabase UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &delete_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(delete_statement, 1, primaryKey); // Execute the query. int success = sqlite3_step(delete_statement); // Reset the statement for future use. sqlite3_reset(delete_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to delete from database with message '%s'.", sqlite3_errmsg(database)); } } // Brings the rest of the object data into memory. If already in memory, no action is taken (harmless no-op). - (void)hydrate { // Check if action is necessary. if (hydrated) return; // Compile the hydration statement, if needed. if (hydrate_statement == nil) { // TODO: Missing time after date. Is it needed or does date track time? strHydrate = [NSString stringWithFormat:@"SELECT logId, type, finderId, finderName, text FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strHydrate UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &hydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(hydrate_statement, 1, primaryKey); // Execute the query. int success =sqlite3_step(hydrate_statement); if (success == SQLITE_ROW) { int idx = 0; self.logId = sqlite3_column_int(hydrate_statement, idx++); self.type = sqlite3_column_int(hydrate_statement, idx++); self.finderId = sqlite3_column_int(hydrate_statement, idx++); char *str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.finderName = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.text = (str) ? [NSString stringWithUTF8String:str] : @""; } else { // The query did not return self.logId = 0; self.type = 0; self.finderId = 0; self.finderName = @"None"; self.text = @""; } // Reset the query for the next use. sqlite3_reset(hydrate_statement); // Update object state with respect to hydration. hydrated = YES; } // Flushes all but the primary key and name out to the database. - (void)dehydrate { if (dirty) { // Write any changes to the database. // First, if needed, compile the dehydrate query. if (dehydrate_statement == nil) { // TODO: Missing time after date. Is it needed or does date track time? strDehydrate = [NSString stringWithFormat:@"UPDATE %@ SET date=?, date=?, logId=?, type=?, finderId=?, finderName=?, text=? WHERE pk=?", self.tableName]; const char *sql = [strDehydrate UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &dehydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } int idx = 1; sqlite3_bind_double(dehydrate_statement, idx++, [date timeIntervalSince1970]); sqlite3_bind_int(dehydrate_statement, idx++, logId); sqlite3_bind_int(dehydrate_statement, idx++, type); sqlite3_bind_int(dehydrate_statement, idx++, finderId); sqlite3_bind_text(dehydrate_statement, idx++, [finderName UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [text UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, primaryKey); // Execute the query. int success = sqlite3_step(dehydrate_statement); // Reset the query for the next use. sqlite3_reset(dehydrate_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to dehydrate with message '%s'.", sqlite3_errmsg(database)); } // Update the object state with respect to unwritten changes. dirty = NO; } // Release member variables to reclaim memory. Set to nil to avoid over-releasing them // if dehydrate is called multiple times. [finderName release]; finderName = nil; [text release]; text = nil; [data release]; data = nil; // Update the object state with respect to hydration. hydrated = NO; } - (void)dealloc { [date release]; [super dealloc]; } #pragma mark Properties // Accessors implemented below. All the "get" accessors simply return the value directly, with no additional // logic or steps for synchronization. The "set" accessors attempt to verify that the new value is definitely // different from the old value, to minimize the amount of work done. Any "set" which actually results in changing // data will mark the object as "dirty" - i.e., possessing data that has not been written to the database. // All the "set" accessors copy data, rather than retain it. This is common for value objects - strings, numbers, // dates, data buffers, etc. This ensures that subsequent changes to either the original or the copy don't violate // the encapsulation of the owning object. - (NSInteger)primaryKey { return primaryKey; } - (NSString *)name { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateStyle:NSDateFormatterMediumStyle]; [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; return [dateFormatter stringFromDate:date];; } - (NSDate *)date { return date; } - (void)setDate:(NSDate *)aDate { if ((!date && !aDate) || (date && aDate && [date isEqualToDate:aDate])) return; dirty = YES; [date release]; date = [aDate copy]; } - (int)logId { return logId; } - (void)setLogId:(int)val { if ((!logId && !val) || (logId && val && logId == val)) return; dirty = YES; logId = val; } - (int)type { return type; } - (void)setType:(int)val { if ((!type && !val) || (type && val && type == val)) return; dirty = YES; type = val; } - (int)finderId { return finderId; } - (void)setFinderId:(int)val { if ((!finderId && !val) || (finderId && val && finderId == val)) return; dirty = YES; finderId = val; } - (NSString *)finderName { return finderName; } - (void)setFinderName:(NSString *)val { if ((!finderName && !val) || (finderName && val && [finderName isEqualToString:finderName])) return; dirty = YES; [finderName release]; finderName = [val copy]; } - (NSString *)text { return text; } - (void)setText:(NSString *)val { if ((!text && !val) || (text && val && [text isEqualToString:text])) return; dirty = YES; [text release]; text = [val copy]; } @end