// // GTMSQLite.m // // Convenience wrapper for SQLite storage see the header for details. // // Copyright 2007-2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // #import #import "GTMSQLite.h" #import "GTMMethodCheck.h" #import "GTMDefines.h" #include typedef struct { BOOL upperCase; int textRep; } UpperLowerUserArgs; typedef struct { BOOL reverse; CFOptionFlags compareOptions; int textRep; } CollateUserArgs; typedef struct { CFOptionFlags *compareOptionPtr; int textRep; } LikeGlobUserArgs; // Helper inline for SQLite text type to CF endcoding GTM_INLINE CFStringEncoding SqliteTextEncodingToCFStringEncoding(int enc) { // Default should never happen, but assume UTF 8 CFStringEncoding encoding = kCFStringEncodingUTF8; _GTMDevAssert(enc == SQLITE_UTF16BE || enc == SQLITE_UTF16LE, @"Passed in encoding was not a UTF16 encoding: %d", enc); switch(enc) { case SQLITE_UTF16BE: encoding = kCFStringEncodingUTF16BE; break; case SQLITE_UTF16LE: encoding = kCFStringEncodingUTF16LE; break; } return encoding; } // Helper inline for filtering CFStringCompareFlags GTM_INLINE CFOptionFlags FilteredStringCompareFlags(CFOptionFlags inOptions) { CFOptionFlags outOptions = 0; if (inOptions & kCFCompareCaseInsensitive) { outOptions |= kCFCompareCaseInsensitive; } if (inOptions & kCFCompareNonliteral) outOptions |= kCFCompareNonliteral; if (inOptions & kCFCompareLocalized) outOptions |= kCFCompareLocalized; if (inOptions & kCFCompareNumerically) outOptions |= kCFCompareNumerically; if (inOptions & kCFCompareDiacriticInsensitive) { outOptions |= kCFCompareDiacriticInsensitive; } if (inOptions & kCFCompareWidthInsensitive) { outOptions |= kCFCompareWidthInsensitive; } return outOptions; } // Function prototypes for our custom implementations of UPPER/LOWER using // CFString so that we handle Unicode and localization more cleanly than // native SQLite. static void UpperLower8(sqlite3_context *context, int argc, sqlite3_value **argv); static void UpperLower16(sqlite3_context *context, int argc, sqlite3_value **argv); // Function prototypes for CFString-based collation sequences static void CollateNeeded(void *userContext, sqlite3 *db, int textRep, const char *name); static int Collate8(void *userContext, int length1, const void *str1, int length2, const void *str2); static int Collate16(void *userContext, int length1, const void *str1, int length2, const void *str2); // Function prototypes for CFString LIKE and GLOB static void Like8(sqlite3_context *context, int argc, sqlite3_value **argv); static void Like16(sqlite3_context *context, int argc, sqlite3_value **argv); static void Glob8(sqlite3_context *context, int argc, sqlite3_value **argv); static void Glob16(sqlite3_context *context, int argc, sqlite3_value **argv); // The CFLocale of the current user at process start static CFLocaleRef gCurrentLocale = NULL; // Private methods @interface GTMSQLiteDatabase (PrivateMethods) - (int)installCFAdditions; - (void)collationArgumentRetain:(NSData *)collationArgs; // Convenience method to clean up resources. Called from both // dealloc & finalize // - (void)cleanupDB; @end @implementation GTMSQLiteDatabase + (void)initialize { // Need the locale for some CFString enhancements gCurrentLocale = CFLocaleCopyCurrent(); } + (int)sqliteVersionNumber { return sqlite3_libversion_number(); } + (NSString *)sqliteVersionString { return [NSString stringWithUTF8String:sqlite3_libversion()]; } - (id)initWithPath:(NSString *)path withCFAdditions:(BOOL)additions utf8:(BOOL)useUTF8 flags:(int)flags errorCode:(int *)err { int rc = SQLITE_INTERNAL; if ((self = [super init])) { path_ = [path copy]; if (useUTF8) { rc = sqlite3_open_v2([path_ fileSystemRepresentation], &db_, flags, NULL); } else { CFStringEncoding cfEncoding; #if TARGET_RT_BIG_ENDIAN cfEncoding = kCFStringEncodingUTF16BE; #else cfEncoding = kCFStringEncodingUTF16LE; #endif NSStringEncoding nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); NSData *data = [path dataUsingEncoding:nsEncoding]; // Using -[NSString cStringUsingEncoding] causes sqlite3_open16 // to fail because it expects 2 null-terminating bytes and // cStringUsingEncoding only has 1 NSMutableData *mutable = [NSMutableData dataWithData:data]; [mutable increaseLengthBy:2]; rc = sqlite3_open16([mutable bytes], &db_); } if ((rc == SQLITE_OK) && db_) { if (additions) { userArgDataPool_ = [[NSMutableArray array] retain]; if (!userArgDataPool_) { // Leave *err as internal err // COV_NF_START - not sure how to fail Cocoa initializers [self release]; return nil; // COV_NF_END } rc = [self installCFAdditions]; } } if (err) *err = rc; if (rc != SQLITE_OK) { // COV_NF_START [self release]; self = nil; // COV_NF_END } } return self; } - (id)initWithPath:(NSString *)path withCFAdditions:(BOOL)additions utf8:(BOOL)useUTF8 errorCode:(int *)err { return [self initWithPath:path withCFAdditions:additions utf8:useUTF8 flags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE errorCode:err]; } - (id)initInMemoryWithCFAdditions:(BOOL)additions utf8:(BOOL)useUTF8 errorCode:(int *)err { return [self initWithPath:@":memory:" withCFAdditions:additions utf8:useUTF8 errorCode:err]; } - (void)dealloc { [self cleanupDB]; [super dealloc]; } - (void)cleanupDB { if (db_) { int rc = sqlite3_close(db_); if (rc != SQLITE_OK) { _GTMDevLog(@"Unable to close \"%@\", error code: %d\r" @"Did you forget to call -[GTMSQLiteStatement" @" finalizeStatement] on one of your statements?", self, rc); } } [path_ release]; [userArgDataPool_ release]; } // Private method to install our custom CoreFoundation additions to SQLite // behavior - (int)installCFAdditions { int rc = SQLITE_OK; // Install our custom functions for improved text handling // UPPER/LOWER static const struct { const char *sqlName; UpperLowerUserArgs userArgs; void *function; } customUpperLower[] = { { "upper", { YES, SQLITE_UTF8 }, &UpperLower8 }, { "upper", { YES, SQLITE_UTF16 }, &UpperLower16 }, { "upper", { YES, SQLITE_UTF16BE }, &UpperLower16 }, { "upper", { YES, SQLITE_UTF16LE }, &UpperLower16 }, { "lower", { NO, SQLITE_UTF8 }, &UpperLower8 }, { "lower", { NO, SQLITE_UTF16 }, &UpperLower16 }, { "lower", { NO, SQLITE_UTF16BE }, &UpperLower16 }, { "lower", { NO, SQLITE_UTF16LE }, &UpperLower16 }, }; for (size_t i = 0; i < (sizeof(customUpperLower) / sizeof(customUpperLower[0])); i++) { rc = sqlite3_create_function(db_, customUpperLower[i].sqlName, 1, customUpperLower[i].userArgs.textRep, (void *)&customUpperLower[i].userArgs, customUpperLower[i].function, NULL, NULL); if (rc != SQLITE_OK) return rc; // COV_NF_LINE because sqlite3_create_function is // called with input defined at compile-time } // Fixed collation sequences static const struct { const char *sqlName; CollateUserArgs userArgs; void *function; } customCollationSequence[] = { { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF8 }, &Collate8 }, { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16 }, &Collate16 }, { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16BE }, &Collate16 }, { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16LE }, &Collate16 }, }; for (size_t i = 0; i < (sizeof(customCollationSequence) / sizeof(customCollationSequence[0])); i++) { rc = sqlite3_create_collation(db_, customCollationSequence[i].sqlName, customCollationSequence[i].userArgs.textRep, (void *)&customCollationSequence[i].userArgs, customCollationSequence[i].function); if (rc != SQLITE_OK) return rc; // COV_NF_LINE because the input to // sqlite3_create_collation is set at compile time } // Install handler for dynamic collation sequences static const struct { const char *sqlName; int numArgs; int textRep; void *function; } customLike[] = { { "like", 2, SQLITE_UTF8, &Like8 }, { "like", 2, SQLITE_UTF16, &Like16 }, { "like", 2, SQLITE_UTF16BE, &Like16 }, { "like", 2, SQLITE_UTF16LE, &Like16 }, { "like", 3, SQLITE_UTF8, &Like8 }, { "like", 3, SQLITE_UTF16, &Like16 }, { "like", 3, SQLITE_UTF16BE, &Like16 }, { "like", 3, SQLITE_UTF16LE, &Like16 }, }; rc = sqlite3_collation_needed(db_, self, &CollateNeeded); if (rc != SQLITE_OK) return rc; // COV_NF_LINE because input to // sqlite3_collation_needed is static // Start LIKE as case-insensitive and non-literal // (sqlite defaults LIKE to case-insensitive) likeOptions_ = kCFCompareCaseInsensitive | kCFCompareNonliteral; for (size_t i = 0; i < (sizeof(customLike) / sizeof(customLike[0])); i++) { // Each implementation gets its own user args NSMutableData *argsData = [NSMutableData dataWithLength:sizeof(LikeGlobUserArgs)]; if (!argsData) return SQLITE_INTERNAL; [userArgDataPool_ addObject:argsData]; LikeGlobUserArgs *args = (LikeGlobUserArgs *)[argsData bytes]; args->compareOptionPtr = &likeOptions_; args->textRep = customLike[i].textRep; rc = sqlite3_create_function(db_, customLike[i].sqlName, customLike[i].numArgs, customLike[i].textRep, args, customLike[i].function, NULL, NULL); if (rc != SQLITE_OK) return rc; // COV_NF_LINE because input to // sqlite3_create_function is static } // Start GLOB just non-literal but case-sensitive (same as SQLite defaults) static const struct { const char *sqlName; int textRep; void *function; } customGlob[] = { { "glob", SQLITE_UTF8, &Glob8 }, { "glob", SQLITE_UTF16, &Glob16 }, { "glob", SQLITE_UTF16BE, &Glob16 }, { "glob", SQLITE_UTF16LE, &Glob16 }, }; globOptions_ = kCFCompareNonliteral; for (size_t i = 0; i < (sizeof(customGlob) / sizeof(customGlob[0])); i++) { // Each implementation gets its own user args NSMutableData *argsData = [NSMutableData dataWithLength:sizeof(LikeGlobUserArgs)]; if (!argsData) return SQLITE_INTERNAL; [userArgDataPool_ addObject:argsData]; LikeGlobUserArgs *args = (LikeGlobUserArgs *)[argsData bytes]; args->compareOptionPtr = &globOptions_; args->textRep = customGlob[i].textRep; rc = sqlite3_create_function(db_, customGlob[i].sqlName, 2, customGlob[i].textRep, args, customGlob[i].function, NULL, NULL); if (rc != SQLITE_OK) return rc; // COV_NF_LINE because input to // sqlite3_create_function is static } hasCFAdditions_ = YES; return SQLITE_OK; } // Private method used by collation creation callback - (void)collationArgumentRetain:(NSData *)collationArgs { [userArgDataPool_ addObject:collationArgs]; } - (sqlite3 *)sqlite3DB { return db_; } - (void)synchronousMode:(BOOL)enable { if (enable) { [self executeSQL:@"PRAGMA synchronous = NORMAL;"]; [self executeSQL:@"PRAGMA fullfsync = 1;"]; } else { [self executeSQL:@"PRAGMA fullfsync = 0;"]; [self executeSQL:@"PRAGMA synchronous = OFF;"]; } } - (BOOL)hasCFAdditions { return hasCFAdditions_; } - (void)setLikeComparisonOptions:(CFOptionFlags)options { if (hasCFAdditions_) likeOptions_ = FilteredStringCompareFlags(options); } - (CFOptionFlags)likeComparisonOptions { CFOptionFlags flags = 0; if (hasCFAdditions_) flags = likeOptions_; return flags; } - (void)setGlobComparisonOptions:(CFOptionFlags)options { if (hasCFAdditions_) globOptions_ = FilteredStringCompareFlags(options); } - (CFOptionFlags)globComparisonOptions { CFOptionFlags globOptions = 0; if (hasCFAdditions_) globOptions = globOptions_; return globOptions; } - (int)lastErrorCode { return sqlite3_errcode(db_); } - (NSString *)lastErrorString { const char *errMsg = sqlite3_errmsg(db_); if (!errMsg) return nil; return [NSString stringWithCString:errMsg encoding:NSUTF8StringEncoding]; } - (int)lastChangeCount { return sqlite3_changes(db_); } - (int)totalChangeCount { return sqlite3_total_changes(db_); } - (unsigned long long)lastInsertRowID { return sqlite3_last_insert_rowid(db_); } - (void)interrupt { sqlite3_interrupt(db_); } - (int)setBusyTimeoutMS:(int)timeoutMS { int rc = sqlite3_busy_timeout(db_, timeoutMS); if (rc == SQLITE_OK) { timeoutMS_ = timeoutMS; } return rc; } - (int)busyTimeoutMS { return timeoutMS_; } - (int)executeSQL:(NSString *)sql { int rc; // Sanity if (!sql) { rc = SQLITE_MISUSE; // Reasonable return for this case } else { if (hasCFAdditions_) { rc = sqlite3_exec(db_, [[sql precomposedStringWithCanonicalMapping] UTF8String], NULL, NULL, NULL); } else { rc = sqlite3_exec(db_, [sql UTF8String], NULL, NULL, NULL); } } return rc; } - (BOOL)beginDeferredTransaction { int err; err = [self executeSQL:@"BEGIN DEFERRED TRANSACTION;"]; return (err == SQLITE_OK) ? YES : NO; } - (BOOL)rollback { int err = [self executeSQL:@"ROLLBACK TRANSACTION;"]; return (err == SQLITE_OK) ? YES : NO; } - (BOOL)commit { int err = [self executeSQL:@"COMMIT TRANSACTION;"]; return (err == SQLITE_OK) ? YES : NO; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p - %@>", [self class], self, path_]; } @end #pragma mark Upper/Lower // Private helper to handle upper/lower conversions for UTF8 static void UpperLower8(sqlite3_context *context, int argc, sqlite3_value **argv) { // Args if ((argc < 1) || (sqlite3_value_type(argv[0]) == SQLITE_NULL)) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation got bad args", -1); return; // COV_NF_END } const char *sqlText8 = (void *)sqlite3_value_text(argv[0]); if (!sqlText8) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation no input UTF8", -1); return; // COV_NF_END } // Get user data UpperLowerUserArgs *userArgs = sqlite3_user_data(context); if (!userArgs) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation no user args", -1); return; // COV_NF_END } _GTMDevAssert(userArgs->textRep == SQLITE_UTF8, @"Received non UTF8 encoding in UpperLower8: %d", userArgs->textRep); // Worker string, must be mutable for case conversion so order our calls // to only copy once CFMutableStringRef workerString = CFStringCreateMutable(kCFAllocatorDefault, 0); GTMCFAutorelease(workerString); if (!workerString) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation failed " \ "to allocate CFMutableStringRef", -1); return; // COV_NF_END } CFStringAppendCString(workerString, sqlText8, kCFStringEncodingUTF8); // Perform the upper/lower if (userArgs->upperCase) { CFStringUppercase(workerString, gCurrentLocale); } else { CFStringLowercase(workerString, gCurrentLocale); } // Convert to our canonical composition CFStringNormalize(workerString, kCFStringNormalizationFormC); // Get the bytes we will return, using the more efficient accessor if we can const char *returnString = CFStringGetCStringPtr(workerString, kCFStringEncodingUTF8); if (returnString) { // COV_NF_START // Direct buffer, but have SQLite copy it sqlite3_result_text(context, returnString, -1, SQLITE_TRANSIENT); // COV_NF_END } else { // Need to get a copy CFIndex workerLength = CFStringGetLength(workerString); CFIndex bufferSize = CFStringGetMaximumSizeForEncoding(workerLength, kCFStringEncodingUTF8); void *returnBuffer = malloc(bufferSize); if (!returnBuffer) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER failed to allocate return buffer", -1); return; // COV_NF_END } CFIndex convertedBytes = 0; CFIndex convertedChars = CFStringGetBytes(workerString, CFRangeMake(0, workerLength), kCFStringEncodingUTF8, 0, false, returnBuffer, bufferSize, &convertedBytes); if (convertedChars != workerLength) { // COV_NF_START free(returnBuffer); sqlite3_result_error(context, "CFStringGetBytes() failed to " \ "convert all characters", -1); // COV_NF_END } else { // Set the result, letting SQLite take ownership and using free() as // the destructor // We cast the 3rd parameter to an int because sqlite3 doesn't appear // to support 64-bit mode. sqlite3_result_text(context, returnBuffer, (int)convertedBytes, &free); } } } // Private helper to handle upper/lower conversions for UTF16 variants static void UpperLower16(sqlite3_context *context, int argc, sqlite3_value **argv) { // Args if ((argc < 1) || (sqlite3_value_type(argv[0]) == SQLITE_NULL)) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation got bad args", -1); return; // COV_NF_END } // For UTF16 variants we want our working string to be in native-endian // UTF16. This gives us the fewest number of copies (since SQLite converts // in-place). There is no advantage to breaking out the string construction // to use UTF16BE or UTF16LE because all that does is move the conversion // work into the CFString constructor, so just use simple code. int sqlText16ByteCount = sqlite3_value_bytes16(argv[0]); const UniChar *sqlText16 = (void *)sqlite3_value_text16(argv[0]); if (!sqlText16ByteCount || !sqlText16) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation no input UTF16", -1); return; // COV_NF_END } // Get user data UpperLowerUserArgs *userArgs = sqlite3_user_data(context); if (!userArgs) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation no user args", -1); return; // COV_NF_END } CFStringEncoding encoding = SqliteTextEncodingToCFStringEncoding(userArgs->textRep); // Mutable worker for upper/lower CFMutableStringRef workerString = CFStringCreateMutable(kCFAllocatorDefault, 0); GTMCFAutorelease(workerString); if (!workerString) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation failed " \ "to allocate CFMutableStringRef", -1); return; // COV_NF_END } CFStringAppendCharacters(workerString, sqlText16, sqlText16ByteCount / sizeof(UniChar)); // Perform the upper/lower if (userArgs->upperCase) { CFStringUppercase(workerString, gCurrentLocale); } else { CFStringLowercase(workerString, gCurrentLocale); } // Convert to our canonical composition CFStringNormalize(workerString, kCFStringNormalizationFormC); // Length after normalization matters CFIndex workerLength = CFStringGetLength(workerString); // If we can give direct byte access use it const UniChar *returnString = CFStringGetCharactersPtr(workerString); if (returnString) { // COV_NF_START details of whether cfstringgetcharactersptr returns // a buffer or NULL are internal; not something we can depend on. // When building for Leopard+, CFIndex is a 64-bit type, which is // why we cast it to an int when we call the sqlite api. _GTMDevAssert((workerLength * sizeof(UniChar) <= INT_MAX), @"sqlite methods do not support buffers greater " @"than 32 bit sizes"); // Direct access to the internal buffer, hand it to sqlite for copy and // conversion sqlite3_result_text16(context, returnString, (int)(workerLength * sizeof(UniChar)), SQLITE_TRANSIENT); // COV_NF_END } else { // Need to get a copy since we can't get direct access CFIndex bufferSize = CFStringGetMaximumSizeForEncoding(workerLength, encoding); void *returnBuffer = malloc(bufferSize); if (!returnBuffer) { // COV_NF_START sqlite3_result_error(context, "LOWER/UPPER CF implementation failed " \ "to allocate return buffer", -1); return; // COV_NF_END } CFIndex convertedBytes = 0; CFIndex convertedChars = CFStringGetBytes(workerString, CFRangeMake(0, workerLength), encoding, 0, false, returnBuffer, bufferSize, &convertedBytes); if (convertedChars != workerLength) { // COV_NF_START free(returnBuffer); sqlite3_result_error(context, "LOWER/UPPER CF implementation CFStringGetBytes() " \ "failed to convert all characters", -1); // COV_NF_END } else { // When building for Leopard+, CFIndex is a 64-bit type, but // sqlite3's functions all take ints. Assert the error for dev // builds and cast down. _GTMDevAssert((convertedBytes <= INT_MAX), @"sqlite methods do not support buffers greater " @"than 32-bit sizes"); int convertedBytesForSQLite = (int)convertedBytes; // Set the result, letting SQLite take ownership and using free() as // the destructor. For output since we're copying out the bytes anyway // we might as well use the preferred encoding of the original call. _GTMDevAssert(userArgs->textRep == SQLITE_UTF16BE || userArgs->textRep == SQLITE_UTF16LE, @"Received non UTF16 encoding in UpperLower16: %d", userArgs->textRep); switch (userArgs->textRep) { case SQLITE_UTF16BE: sqlite3_result_text16be(context, returnBuffer, convertedBytesForSQLite, &free); break; case SQLITE_UTF16LE: sqlite3_result_text16le(context, returnBuffer, convertedBytesForSQLite, &free); break; default: free(returnBuffer); // COV_NF_START no way to tell sqlite to not use utf8 or utf16? sqlite3_result_error(context, "LOWER/UPPER CF implementation " \ "had unhandled encoding", -1); // COV_NF_END } } } } #pragma mark Collations static void CollateNeeded(void *userContext, sqlite3 *db, int textRep, const char *name) { // Cast GTMSQLiteDatabase *gtmdb = (GTMSQLiteDatabase *)userContext; _GTMDevAssert(gtmdb, @"Invalid database parameter from sqlite"); // Create space for the collation args NSMutableData *collationArgsData = [NSMutableData dataWithLength:sizeof(CollateUserArgs)]; CollateUserArgs *userArgs = (CollateUserArgs *)[collationArgsData bytes]; bzero(userArgs, sizeof(CollateUserArgs)); userArgs->textRep = textRep; // Parse the name into the flags we need NSString *collationName = [[NSString stringWithUTF8String:name] lowercaseString]; NSArray *collationComponents = [collationName componentsSeparatedByString:@"_"]; NSString *collationFlag = nil; BOOL atLeastOneValidFlag = NO; for (collationFlag in collationComponents) { if ([collationFlag isEqualToString:@"reverse"]) { userArgs->reverse = YES; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"nocase"]) { userArgs->compareOptions |= kCFCompareCaseInsensitive; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"nonliteral"]) { userArgs->compareOptions |= kCFCompareNonliteral; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"localized"]) { userArgs->compareOptions |= kCFCompareLocalized; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"numeric"]) { userArgs->compareOptions |= kCFCompareNumerically; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"nodiacritic"]) { userArgs->compareOptions |= kCFCompareDiacriticInsensitive; atLeastOneValidFlag = YES; } else if ([collationFlag isEqualToString:@"widthinsensitive"]) { userArgs->compareOptions |= kCFCompareWidthInsensitive; atLeastOneValidFlag = YES; } } // No valid tokens means nothing to do if (!atLeastOneValidFlag) return; int err; // Add the collation switch (textRep) { case SQLITE_UTF8: err = sqlite3_create_collation([gtmdb sqlite3DB], name, textRep, userArgs, &Collate8); if (err != SQLITE_OK) return; break; case SQLITE_UTF16: case SQLITE_UTF16BE: case SQLITE_UTF16LE: err = sqlite3_create_collation([gtmdb sqlite3DB], name, textRep, userArgs, &Collate16); if (err != SQLITE_OK) return; break; default: return; } // Have the db retain our collate function args [gtmdb collationArgumentRetain:collationArgsData]; } static int Collate8(void *userContext, int length1, const void *str1, int length2, const void *str2) { // User args CollateUserArgs *userArgs = (CollateUserArgs *)userContext; _GTMDevAssert(userArgs, @"Invalid user arguments from sqlite"); // Sanity and zero-lengths if (!(str1 && str2) || (!length1 && !length2)) { return kCFCompareEqualTo; // Best we can do and stable sort } if (!length1 && length2) { if (userArgs->reverse) { return kCFCompareGreaterThan; } else { return kCFCompareLessThan; } } else if (length1 && !length2) { if (userArgs->reverse) { return kCFCompareLessThan; } else { return kCFCompareGreaterThan; } } // We have UTF8 strings with no terminating null, we want to compare // with as few copies as possible. Leopard introduced a no-copy string // creation function, we'll use it when we can but we want to stay compatible // with Tiger. CFStringRef string1 = NULL, string2 = NULL; string1 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, str1, length1, kCFStringEncodingUTF8, false, kCFAllocatorNull); string2 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, str2, length2, kCFStringEncodingUTF8, false, kCFAllocatorNull); GTMCFAutorelease(string1); GTMCFAutorelease(string2); // Allocation failures can't really be sanely handled from a collator int sqliteResult; if (!(string1 && string2)) { // COV_NF_START sqliteResult = (int)kCFCompareEqualTo; // COV_NF_END } else { // Compare // We have to cast to int because SQLite takes functions that // return an int, but when compiling for Leopard+, // CFComparisonResult is a signed long, but on Tiger it's an int CFComparisonResult result; result = CFStringCompare(string1, string2, userArgs->compareOptions); sqliteResult = (int)result; // Reverse if (userArgs->reverse && sqliteResult) { sqliteResult = -sqliteResult; } } return sqliteResult; } static int Collate16(void *userContext, int length1, const void *str1, int length2, const void *str2) { // User args CollateUserArgs *userArgs = (CollateUserArgs *)userContext; _GTMDevAssert(userArgs, @"Invalid user arguments from sqlite"); // Sanity and zero-lengths if (!(str1 && str2) || (!length1 && !length2)) { return kCFCompareEqualTo; // Best we can do and stable sort } if (!length1 && length2) { if (userArgs->reverse) { return kCFCompareGreaterThan; } else { return kCFCompareLessThan; } } else if (length1 && !length2) { if (userArgs->reverse) { return kCFCompareLessThan; } else { return kCFCompareGreaterThan; } } // Target encoding CFStringEncoding encoding = SqliteTextEncodingToCFStringEncoding(userArgs->textRep); // We have UTF16 strings, we want to compare with as few copies as // possible. Since endianness matters we want to use no-copy // variants where possible and copy (and endian convert) only when // we must. CFStringRef string1 = NULL, string2 = NULL; if ((userArgs->textRep == SQLITE_UTF16) || #if TARGET_RT_BIG_ENDIAN (userArgs->textRep == SQLITE_UTF16BE) #else (userArgs->textRep == SQLITE_UTF16LE) #endif ) { string1 = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, str1, length1 / sizeof(UniChar), kCFAllocatorNull); string2 = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, str2, length2 / sizeof(UniChar), kCFAllocatorNull); } else { // No point in using the "no copy" version of the call here. If the // bytes were in the native order we'd be in the other part of this // conditional, so we know we have to copy the string to endian convert // it. string1 = CFStringCreateWithBytes(kCFAllocatorDefault, str1, length1, encoding, false); string2 = CFStringCreateWithBytes(kCFAllocatorDefault, str2, length2, encoding, false); } GTMCFAutorelease(string1); GTMCFAutorelease(string2); int sqliteResult; // Allocation failures can't really be sanely handled from a collator if (!(string1 && string2)) { // COV_NF_START sqliteResult = (int)kCFCompareEqualTo; // COV_NF_END } else { // Compare // We cast the return value to an int because CFComparisonResult // is a long in Leopard+ builds. I have no idea why we need // 64-bits for a 3-value enum, but that's how it is... CFComparisonResult result; result = CFStringCompare(string1, string2, userArgs->compareOptions); sqliteResult = (int)result; //Reverse if (userArgs->reverse && sqliteResult) { sqliteResult = -sqliteResult; } } return sqliteResult; } #pragma mark Like/Glob // Private helper to handle LIKE and GLOB with different encodings. This // is essentially a reimplementation of patternCompare() in func.c of the // SQLite sources. static void LikeGlobCompare(sqlite3_context *context, CFStringRef pattern, CFStringRef targetString, UniChar matchAll, UniChar matchOne, UniChar escape, BOOL setSupport, CFOptionFlags compareOptions) { // Setup for pattern walk CFIndex patternLength = CFStringGetLength(pattern); CFStringInlineBuffer patternBuffer; CFStringInitInlineBuffer(pattern, &patternBuffer, CFRangeMake(0, patternLength)); UniChar patternChar; CFIndex patternIndex = 0; CFIndex targetStringLength = CFStringGetLength(targetString); CFIndex targetStringIndex = 0; BOOL isAnchored = YES; size_t dataSize = patternLength * sizeof(UniChar); NSMutableData *tempData = [NSMutableData dataWithLength:dataSize]; // Temp string buffer can be no larger than the whole pattern UniChar *findBuffer = [tempData mutableBytes]; if (!findBuffer) { // COV_NF_START sqlite3_result_error(context, "LIKE or GLOB CF implementation failed to " \ "allocate temporary buffer", -1); return; // COV_NF_END } // We'll use a mutable string we can just reset as we wish CFMutableStringRef findString = CFStringCreateMutableWithExternalCharactersNoCopy(kCFAllocatorDefault, NULL, 0, 0, kCFAllocatorNull); GTMCFAutorelease(findString); if (!findString) { // COV_NF_START sqlite3_result_error(context, "LIKE or GLOB CF implementation failed to " \ "allocate temporary CFString", -1); return; // COV_NF_END } // Walk the pattern while (patternIndex < patternLength) { patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); // Match all character has no effect other than to unanchor the search if (patternChar == matchAll) { isAnchored = NO; patternIndex++; continue; } // Match one character pushes the string index forward by one composed // character if (patternChar == matchOne) { // If this single char match would walk us off the end of the string // we're already done, no match if (targetStringIndex >= targetStringLength) { sqlite3_result_int(context, 0); return; } // There's still room in the string, so move the string index forward one // composed character and go back around. CFRange nextCharRange = CFStringGetRangeOfComposedCharactersAtIndex(targetString, targetStringIndex); targetStringIndex = nextCharRange.location + nextCharRange.length; patternIndex++; continue; } // Character set matches require the parsing of the character set if (setSupport && (patternChar == 0x5B)) { // "[" // A character set must match one character, if there's not at least one // character left in the string, don't bother if (targetStringIndex >= targetStringLength) { sqlite3_result_int(context, 0); return; } // There's at least one character, try to match the remainder of the // string using a CFCharacterSet CFMutableCharacterSetRef charSet = CFCharacterSetCreateMutable(kCFAllocatorDefault); GTMCFAutorelease(charSet); if (!charSet) { // COV_NF_START sqlite3_result_error(context, "LIKE or GLOB CF implementation failed to " \ "allocate temporary CFMutableCharacterSet", -1); return; // COV_NF_END } BOOL invert = NO; // Walk one character forward patternIndex++; if (patternIndex >= patternLength) { // Oops, out of room sqlite3_result_error(context, "LIKE or GLOB CF implementation found " \ "unclosed character set", -1); return; } // First character after pattern open is special-case patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); if (patternChar == 0x5E) { // "^" invert = YES; // Bump forward one character, can still be an unescaped "]" after // negation patternIndex++; if (patternIndex >= patternLength) { // Oops, out of room sqlite3_result_error(context, "LIKE or GLOB CF implementation found " \ "unclosed character set after negation", -1); return; } patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); } // First char in set or first char in negation can be a literal "]" not // considered a close if (patternChar == 0x5D) { // "]" CFCharacterSetAddCharactersInRange(charSet, CFRangeMake(patternChar, 1)); patternIndex++; if (patternIndex >= patternLength) { // Oops, out of room sqlite3_result_error(context, "LIKE or GLOB CF implementation found " \ "unclosed character set after escaped ]", -1); return; } patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); } while ((patternIndex < patternLength) && patternChar && (patternChar != 0x5D)) { // "]" // Check for possible character range, for this to be true we // must have a hyphen at the next position and at least 3 // characters of room (for hyphen, range end, and set // close). Hyphens at the end without a trailing range are // treated as literals if (((patternLength - patternIndex) >= 3) && // Second char must be "-" (CFStringGetCharacterFromInlineBuffer(&patternBuffer, // 0x2D is "-" patternIndex + 1) == 0x2D) && // And third char must be anything other than set close in // case the hyphen is at the end of the set and needs to // be treated as a literal (CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex + 2) != 0x5D)) { // "]" // Get the range close UniChar rangeClose = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex + 2); // Add the whole range int rangeLen = rangeClose - patternChar + 1; CFCharacterSetAddCharactersInRange(charSet, CFRangeMake(patternChar, rangeLen)); // Move past the end of the range patternIndex += 3; } else { // Single Raw character CFCharacterSetAddCharactersInRange(charSet, CFRangeMake(patternChar, 1)); patternIndex++; } // Load next char for loop if (patternIndex < patternLength) { patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); } else { patternChar = 0; } } // Check for closure if (patternChar != 0x5D) { // "]" sqlite3_result_error(context, "LIKE or GLOB CF implementation found " \ "unclosed character set", -1); return; } else { // Increment past the end of the set patternIndex++; } // Invert the set if needed if (invert) CFCharacterSetInvert(charSet); // Do the search CFOptionFlags findOptions = 0; if (isAnchored) findOptions |= kCFCompareAnchored; CFRange foundRange; unsigned long rangeLen = targetStringLength - targetStringIndex; BOOL found = CFStringFindCharacterFromSet(targetString, charSet, CFRangeMake(targetStringIndex, rangeLen), findOptions, &foundRange); // If no match then the whole pattern fails if (!found) { sqlite3_result_int(context, 0); return; } // If we did match then we need to push the string index to the // character past the end of the match and then go back around // the loop. targetStringIndex = foundRange.location + foundRange.length; // At this point patternIndex is either at the end of the // string, or at the next special character which will be picked // up and handled at the top of the loop. We do, however, need // to reset the anchor status isAnchored = YES; // End of character sets, back around continue; } // Otherwise the pattern character is a normal or escaped // character we should consume and match with normal string // matching CFIndex findBufferIndex = 0; while ((patternIndex < patternLength) && patternChar && !((patternChar == matchAll) || (patternChar == matchOne) || (setSupport && (patternChar == 0x5B)))) { // "[" if (patternChar == escape) { // No matter what the character follows the escape copy it to the // buffer patternIndex++; if (patternIndex >= patternLength) { // COV_NF_START // Oops, escape came at end of pattern, that's an error sqlite3_result_error(context, "LIKE or GLOB CF implementation found " \ "escape character at end of pattern", -1); return; // COV_NF_END } patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); } // At this point the patternChar is either the escaped character or the // original normal character findBuffer[findBufferIndex++] = patternChar; // Set up for next loop patternIndex++; if (patternIndex < patternLength) { patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex); } else { patternChar = 0; } } // On loop exit we have a string ready for comparision, if that // string is too long then it can't be a match. if (findBufferIndex > (targetStringLength - targetStringIndex)) { sqlite3_result_int(context, 0); return; } // We actually need to do a comparison CFOptionFlags findOptions = compareOptions; if (isAnchored) findOptions |= kCFCompareAnchored; CFStringSetExternalCharactersNoCopy(findString, findBuffer, findBufferIndex, findBufferIndex); CFRange foundRange; unsigned long rangeLen = targetStringLength - targetStringIndex; BOOL found = CFStringFindWithOptions(targetString, findString, CFRangeMake(targetStringIndex, rangeLen), findOptions, &foundRange); // If no match then the whole pattern fails if (!found) { sqlite3_result_int(context, 0); return; } // If we did match then we need to push the string index to the // character past the end of the match and then go back around the // loop. targetStringIndex = foundRange.location + foundRange.length; // At this point patternIndex is either at the end of the string, // or at the next special character which will be picked up and // handled at the top of the loop. We do, however, need to reset // the anchor status isAnchored = YES; } // On loop exit all pattern characters have been considered. If we're still // alive it means that we've matched the entire pattern, except for trailing // wildcards, we need to handle that case. if (isAnchored) { // If we're still anchored there was no trailing matchAll, in which case // we have to have run to exactly the end of the string if (targetStringIndex == targetStringLength) { sqlite3_result_int(context, 1); } else { sqlite3_result_int(context, 0); } } else { // If we're not anchored any remaining characters are OK sqlite3_result_int(context, 1); } } static void Like8(sqlite3_context *context, int argc, sqlite3_value **argv) { // Get our LIKE options LikeGlobUserArgs *likeArgs = sqlite3_user_data(context); if (!likeArgs) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation no user args", -1); return; // COV_NF_END } // Read the strings const char *pattern = (const char *)sqlite3_value_text(argv[0]); const char *target = (const char *)sqlite3_value_text(argv[1]); if (!pattern || !target) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation missing pattern or value", -1); return; // COV_NF_END } CFStringRef patternString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, pattern, kCFStringEncodingUTF8, kCFAllocatorNull); CFStringRef targetString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, target, kCFStringEncodingUTF8, kCFAllocatorNull); GTMCFAutorelease(patternString); GTMCFAutorelease(targetString); if (!(patternString && targetString)) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation failed " \ "to allocate CFStrings", -1); return; // COV_NF_END } UniChar escapeChar = 0; // If there is a third argument it is the escape character if (argc == 3) { const char *escape = (const char *)sqlite3_value_text(argv[2]); if (!escape) { sqlite3_result_error(context, "LIKE CF implementation missing " \ "escape character", -1); return; } CFStringRef escapeString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, escape, kCFStringEncodingUTF8, kCFAllocatorNull); GTMCFAutorelease(escapeString); if (!escapeString) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation failed to " \ "allocate CFString for ESCAPE", -1); return; // COV_NF_END } if (CFStringGetLength(escapeString) != 1) { sqlite3_result_error(context, "CF implementation ESCAPE expression " \ "must be single character", -1); return; } escapeChar = CFStringGetCharacterAtIndex(escapeString, 0); } // Do the compare LikeGlobCompare(context, patternString, targetString, 0x25, // % 0x5F, // _ escapeChar, NO, // LIKE does not support character sets *(likeArgs->compareOptionPtr)); } static void Like16(sqlite3_context *context, int argc, sqlite3_value **argv) { // Get our LIKE options LikeGlobUserArgs *likeArgs = sqlite3_user_data(context); if (!likeArgs) { // COV_NF_START - sql parser chokes if we feed any input // that could trigger this sqlite3_result_error(context, "LIKE CF implementation no user args", -1); return; // COV_NF_END } // For UTF16 variants we want our working string to be in native-endian // UTF16. This gives us the fewest number of copies (since SQLite converts // in-place). There is no advantage to breaking out the string construction // to use UTF16BE or UTF16LE because all that does is move the conversion // work into the CFString constructor, so just use simple code. int patternByteCount = sqlite3_value_bytes16(argv[0]); const UniChar *patternText = (void *)sqlite3_value_text16(argv[0]); int targetByteCount = sqlite3_value_bytes16(argv[1]); const UniChar *targetText = (void *)sqlite3_value_text16(argv[1]); if (!patternByteCount || !patternText || !targetByteCount || !targetText) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation missing pattern or value", -1); return; // COV_NF_END } CFStringRef patternString = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, patternText, patternByteCount / sizeof(UniChar), kCFAllocatorNull); CFStringRef targetString = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, targetText, targetByteCount / sizeof(UniChar), kCFAllocatorNull); GTMCFAutorelease(patternString); GTMCFAutorelease(targetString); if (!(patternString && targetString)) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation failed " \ "to allocate CFStrings", -1); return; // COV_NF_END } // If there is a third argument it is the escape character, force a // UTF8 conversion for simplicity UniChar escapeChar = 0; if (argc == 3) { const char *escape = (const char *)sqlite3_value_text(argv[2]); if (!escape) { sqlite3_result_error(context, "LIKE CF implementation " \ "missing escape character", -1); return; } CFStringRef escapeString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, escape, kCFStringEncodingUTF8, kCFAllocatorNull); GTMCFAutorelease(escapeString); if (!escapeString) { // COV_NF_START sqlite3_result_error(context, "LIKE CF implementation failed to " \ "allocate CFString for ESCAPE", -1); return; // COV_NF_END } if (CFStringGetLength(escapeString) != 1) { sqlite3_result_error(context, "CF implementation ESCAPE expression " \ "must be single character", -1); return; } escapeChar = CFStringGetCharacterAtIndex(escapeString, 0); } // Do the compare LikeGlobCompare(context, patternString, targetString, 0x25, // % 0x5F, // _ escapeChar, NO, // LIKE does not support character sets *(likeArgs->compareOptionPtr)); } static void Glob8(sqlite3_context *context, int argc, sqlite3_value **argv) { // Get our GLOB options LikeGlobUserArgs *globArgs = sqlite3_user_data(context); if (!globArgs) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation no user args", -1); return; // COV_NF_END } // Read the strings const char *pattern = (const char *)sqlite3_value_text(argv[0]); const char *target = (const char *)sqlite3_value_text(argv[1]); if (!pattern || !target) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation missing " \ "pattern or value", -1); return; // COV_NF_END } CFStringRef patternString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, pattern, kCFStringEncodingUTF8, kCFAllocatorNull); CFStringRef targetString = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, target, kCFStringEncodingUTF8, kCFAllocatorNull); GTMCFAutorelease(patternString); GTMCFAutorelease(targetString); if (!(patternString && targetString)) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation failed to " \ "allocate CFStrings", -1); // COV_NF_END } else { // Do the compare LikeGlobCompare(context, patternString, targetString, 0x2A, // * 0x3F, // ? 0, // GLOB does not support escape characters YES, // GLOB supports character sets *(globArgs->compareOptionPtr)); } } static void Glob16(sqlite3_context *context, int argc, sqlite3_value **argv) { // Get our GLOB options LikeGlobUserArgs *globArgs = sqlite3_user_data(context); if (!globArgs) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation no user args", -1); return; // COV_NF_END } // For UTF16 variants we want our working string to be in // native-endian UTF16. This gives us the fewest number of copies // (since SQLite converts in-place). There is no advantage to // breaking out the string construction to use UTF16BE or UTF16LE // because all that does is move the conversion work into the // CFString constructor, so just use simple code. int patternByteCount = sqlite3_value_bytes16(argv[0]); const UniChar *patternText = (void *)sqlite3_value_text16(argv[0]); int targetByteCount = sqlite3_value_bytes16(argv[1]); const UniChar *targetText = (void *)sqlite3_value_text16(argv[1]); if (!patternByteCount || !patternText || !targetByteCount || !targetText) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation missing pattern or value", -1); return; // COV_NF_END } CFStringRef patternString = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, patternText, patternByteCount / sizeof(UniChar), kCFAllocatorNull); CFStringRef targetString = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, targetText, targetByteCount / sizeof(UniChar), kCFAllocatorNull); GTMCFAutorelease(patternString); GTMCFAutorelease(targetString); if (!(patternString && targetString)) { // COV_NF_START sqlite3_result_error(context, "GLOB CF implementation failed to "\ "allocate CFStrings", -1); // COV_NF_END } else { // Do the compare LikeGlobCompare(context, patternString, targetString, 0x2A, // * 0x3F, // ? 0, // GLOB does not support escape characters YES, // GLOB supports character sets *(globArgs->compareOptionPtr)); } } // ----------------------------------------------------------------------------- @implementation GTMSQLiteStatement #pragma mark Creation, Access and Finalization + (id)statementWithSQL:(NSString *)sql inDatabase:(GTMSQLiteDatabase *)gtmdb errorCode:(int *)err { return [[[GTMSQLiteStatement alloc] initWithSQL:sql inDatabase:gtmdb errorCode:err] autorelease]; } - (id)initWithSQL:(NSString *)sql inDatabase:(GTMSQLiteDatabase *)gtmdb errorCode:(int *)err { int rc; id obj; if ((self = [super init])) { // Sanity obj = self; if (sql && gtmdb) { // Find out if the database is using our CF extensions hasCFAdditions_ = [gtmdb hasCFAdditions]; // Prepare if (hasCFAdditions_) { sql = [sql precomposedStringWithCanonicalMapping]; } if (sql) { rc = sqlite3_prepare([gtmdb sqlite3DB], [sql UTF8String], -1, &statement_, NULL); if (rc != SQLITE_OK) { [self release]; obj = nil; } } else { // COV_NF_START rc = SQLITE_INTERNAL; [self release]; obj = nil; // COV_NF_END } } else { rc = SQLITE_MISUSE; [self release]; obj = nil; } } else { // COV_NF_START rc = SQLITE_INTERNAL; obj = nil; // COV_NF_END } if (err) *err = rc; return obj; } - (void)dealloc { if (statement_) { _GTMDevLog(@"-[GTMSQLiteStatement finalizeStatement] must be called when" @" statement is no longer needed"); } [super dealloc]; } - (sqlite3_stmt *)sqlite3Statement { return statement_; } - (int)finalizeStatement { if (!statement_) return SQLITE_MISUSE; int rc = sqlite3_finalize(statement_); statement_ = NULL; return rc; } #pragma mark Parameters and Binding - (int)parameterCount { if (!statement_) return -1; return sqlite3_bind_parameter_count(statement_); } - (int)positionOfParameterNamed:(NSString *)paramName { if (!statement_) return -1; if (hasCFAdditions_) { NSString *cleanedString = [paramName precomposedStringWithCanonicalMapping]; if (!cleanedString) return -1; return sqlite3_bind_parameter_index(statement_, [cleanedString UTF8String]); } else { return sqlite3_bind_parameter_index(statement_, [paramName UTF8String]); } } - (NSString *)nameOfParameterAtPosition:(int)position { if ((position < 1) || !statement_) return nil; const char *name = sqlite3_bind_parameter_name(statement_, position); if (!name) return nil; NSString *nameString = [NSString stringWithUTF8String:name]; if (hasCFAdditions_) { return [nameString precomposedStringWithCanonicalMapping]; } else { return nameString; } } - (int)bindSQLNullAtPosition:(int)position { if (!statement_) return SQLITE_MISUSE; return sqlite3_bind_null(statement_, position); } - (int)bindBlobAtPosition:(int)position bytes:(void *)bytes length:(int)length { if (!statement_ || !bytes || !length) return SQLITE_MISUSE; return sqlite3_bind_blob(statement_, position, bytes, length, SQLITE_TRANSIENT); } - (int)bindBlobAtPosition:(int)position data:(NSData *)data { if (!statement_ || !data || !position) return SQLITE_MISUSE; int blobLength = (int)[data length]; _GTMDevAssert((blobLength < INT_MAX), @"sqlite methods do not support data lengths " @"exceeding 32 bit sizes"); return [self bindBlobAtPosition:position bytes:(void *)[data bytes] length:blobLength]; } - (int)bindDoubleAtPosition:(int)position value:(double)value { if (!statement_) return SQLITE_MISUSE; return sqlite3_bind_double(statement_, position, value); } - (int)bindNumberAsDoubleAtPosition:(int)position number:(NSNumber *)number { if (!number || !statement_) return SQLITE_MISUSE; return sqlite3_bind_double(statement_, position, [number doubleValue]); } - (int)bindInt32AtPosition:(int)position value:(int)value { if (!statement_) return SQLITE_MISUSE; return sqlite3_bind_int(statement_, position, value); } - (int)bindNumberAsInt32AtPosition:(int)position number:(NSNumber *)number { if (!number || !statement_) return SQLITE_MISUSE; return sqlite3_bind_int(statement_, position, [number intValue]); } - (int)bindLongLongAtPosition:(int)position value:(long long)value { if (!statement_) return SQLITE_MISUSE; return sqlite3_bind_int64(statement_, position, value); } - (int)bindNumberAsLongLongAtPosition:(int)position number:(NSNumber *)number { if (!number || !statement_) return SQLITE_MISUSE; return sqlite3_bind_int64(statement_, position, [number longLongValue]); } - (int)bindStringAtPosition:(int)position string:(NSString *)string { if (!string || !statement_) return SQLITE_MISUSE; if (hasCFAdditions_) { string = [string precomposedStringWithCanonicalMapping]; if (!string) return SQLITE_INTERNAL; } return sqlite3_bind_text(statement_, position, [string UTF8String], -1, SQLITE_TRANSIENT); } #pragma mark Results - (int)resultColumnCount { if (!statement_) return -1; return sqlite3_column_count(statement_); } - (NSString *)resultColumnNameAtPosition:(int)position { if (!statement_) return nil; const char *name = sqlite3_column_name(statement_, position); if (!name) return nil; NSString *nameString = [NSString stringWithUTF8String:name]; if (hasCFAdditions_) { return [nameString precomposedStringWithCanonicalMapping]; } else { return nameString; } } - (int)rowDataCount { if (!statement_) return -1; return sqlite3_data_count(statement_); } - (int)resultColumnTypeAtPosition:(int)position { if (!statement_) return -1; return sqlite3_column_type(statement_, position); } - (NSData *)resultBlobDataAtPosition:(int)position { if (!statement_) return nil; const void *bytes = sqlite3_column_blob(statement_, position); int length = sqlite3_column_bytes(statement_, position); if (!(bytes && length)) return nil; return [NSData dataWithBytes:bytes length:length]; } - (double)resultDoubleAtPosition:(int)position { if (!statement_) return 0; return sqlite3_column_double(statement_, position); } - (int)resultInt32AtPosition:(int)position { if (!statement_) return 0; return sqlite3_column_int(statement_, position); } - (long long)resultLongLongAtPosition:(int)position { if (!statement_) return 0; return sqlite3_column_int64(statement_, position); } - (NSNumber *)resultNumberAtPosition:(int)position { if (!statement_) return nil; int type = [self resultColumnTypeAtPosition:position]; if (type == SQLITE_FLOAT) { // Special case for floats return [NSNumber numberWithDouble:[self resultDoubleAtPosition:position]]; } else { // Everything else is cast to int long long result = [self resultLongLongAtPosition:position]; return [NSNumber numberWithLongLong:result]; } } - (NSString *)resultStringAtPosition:(int)position { if (!statement_) return nil; const char *text = (const char *)sqlite3_column_text(statement_, position); if (!text) return nil; NSString *result = [NSString stringWithUTF8String:text]; if (hasCFAdditions_) { return [result precomposedStringWithCanonicalMapping]; } else { return result; } } - (id)resultFoundationObjectAtPosition:(int)position { if (!statement_) return nil; int type = [self resultColumnTypeAtPosition:position]; id result = nil; switch (type) { case SQLITE_INTEGER: case SQLITE_FLOAT: result = [self resultNumberAtPosition:position]; break; case SQLITE_TEXT: result = [self resultStringAtPosition:position]; break; case SQLITE_BLOB: result = [self resultBlobDataAtPosition:position]; break; case SQLITE_NULL: result = [NSNull null]; break; } return result; } - (NSArray *)resultRowArray { int count = [self rowDataCount]; if (count < 1) return nil; NSMutableArray *finalArray = [NSMutableArray array]; for (int i = 0; i < count; i++) { id coldata = [self resultFoundationObjectAtPosition:i]; if (!coldata) return nil; // Oops [finalArray addObject:coldata]; } if (![finalArray count]) return nil; return finalArray; } - (NSDictionary *)resultRowDictionary { int count = [self rowDataCount]; if (count < 1) return nil; NSMutableDictionary *finalDict = [NSMutableDictionary dictionary]; for (int i = 0; i < count; i++) { id coldata = [self resultFoundationObjectAtPosition:i]; NSString *colname = [self resultColumnNameAtPosition:i]; if (!(coldata && colname)) continue; [finalDict setObject:coldata forKey:colname]; } if (![finalDict count]) return nil; return finalDict; } #pragma mark Rows - (int)stepRow { int rc = SQLITE_BUSY; while (rc == SQLITE_BUSY) { rc = [self stepRowWithTimeout]; } return rc; } - (int)stepRowWithTimeout { if (!statement_) return SQLITE_MISUSE; return sqlite3_step(statement_); } - (int)reset { if (!statement_) return SQLITE_MISUSE; return sqlite3_reset(statement_); } + (BOOL)isCompleteStatement:(NSString *)statement { BOOL isComplete = NO; if (statement) { isComplete = (sqlite3_complete([statement UTF8String]) ? YES : NO); } return isComplete; } + (NSString*)quoteAndEscapeString:(NSString *)string { char *quoted = sqlite3_mprintf("'%q'", [string UTF8String]); if (!quoted) return nil; NSString *quotedString = [NSString stringWithUTF8String:quoted]; sqlite3_free(quoted); return quotedString; } @end