From fde1ea051c0385638accd67362cd25f2fe1ae654 Mon Sep 17 00:00:00 2001 From: "thomasvl@gmail.com" Date: Thu, 26 Feb 2009 01:35:12 +0000 Subject: adding SQLite helper/wrapper --- Foundation/GTMSQLite.m | 2013 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2013 insertions(+) create mode 100644 Foundation/GTMSQLite.m (limited to 'Foundation/GTMSQLite.m') diff --git a/Foundation/GTMSQLite.m b/Foundation/GTMSQLite.m new file mode 100644 index 0000000..01e3bad --- /dev/null +++ b/Foundation/GTMSQLite.m @@ -0,0 +1,2013 @@ +// +// 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 +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +#import +#endif +#import "GTMSQLite.h" +#import "GTMMethodCheck.h" +#import "GTMDefines.h" +#include +#import "GTMGarbageCollection.h" + +typedef struct { + BOOL upperCase; + int textRep; +} UpperLowerUserArgs; + +typedef struct { + BOOL reverse; + CFOptionFlags compareOptions; + int textRep; +} CollateUserArgs; + +typedef struct { + CFOptionFlags *compareOptionPtr; + int textRep; +} LikeGlobUserArgs; + +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +// While we want to be compatible with Tiger, some operations are more +// efficient when implemented with Leopard APIs. We look those up dynamically. +// CFStringCreateWithBytesNoCopy +static const char* const kCFStringCreateWithBytesNoCopySymbolName = + "CFStringCreateWithBytesNoCopy"; + +typedef CFStringRef (*CFStringCreateWithBytesNoCopyPtrType)(CFAllocatorRef, + const UInt8 *, + CFIndex, + CFStringEncoding, + Boolean, + CFAllocatorRef); +static CFStringCreateWithBytesNoCopyPtrType gCFStringCreateWithBytesNoCopySymbol = NULL; +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + +// 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"); + 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 MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + if (inOptions & kCFCompareDiacriticInsensitive) { + outOptions |= kCFCompareDiacriticInsensitive; + } + if (inOptions & kCFCompareWidthInsensitive) { + outOptions |= kCFCompareWidthInsensitive; + } +#endif + 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(); + +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + // Compiling pre-Leopard try to find some symbols dynamically + gCFStringCreateWithBytesNoCopySymbol = + (CFStringCreateWithBytesNoCopyPtrType)dlsym( + RTLD_DEFAULT, + kCFStringCreateWithBytesNoCopySymbolName); +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +} + ++ (int)sqliteVersionNumber { + return sqlite3_libversion_number(); +} + ++ (NSString *)sqliteVersionString { + return [NSString stringWithUTF8String:sqlite3_libversion()]; +} + +- (id)initWithPath:(NSString *)path + withCFAdditions:(BOOL)additions + utf8:(BOOL)useUTF8 + errorCode:(int *)err { + int rc = SQLITE_INTERNAL; + + if ((self = [super init])) { + path_ = [path copy]; + if (useUTF8) { + rc = sqlite3_open([path_ fileSystemRepresentation], &db_); + } else { + CFStringEncoding cfEncoding; +#if __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)initInMemoryWithCFAdditions:(BOOL)additions + utf8:(BOOL)useUTF8 + errorCode:(int *)err { + return [self initWithPath:@":memory:" + withCFAdditions:additions + utf8:useUTF8 + errorCode:err]; +} + +#if GTM_SUPPORT_GC +- (void)finalize { + [self cleanupDB]; + [super finalize]; +} +#endif + +- (void)dealloc { + [self cleanupDB]; + [super dealloc]; +} + +- (void)cleanupDB { + if (db_) { + int rc = sqlite3_close(db_); + if (rc != SQLITE_OK) { + // COV_NF_START + _GTMDevLog(@"Unable to close \"%@\", error code: %d", self, rc); + _GTMDevLog(@"Did you forget to call -[GTMSQLiteStatement" + @" finalizeStatement] on one of your statements?"); + // COV_NF_END + } + } + [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 + 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 + 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 + 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) + 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"); + + // 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 UTF8 encoding in UpperLower8"); + 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: + // 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; + GTM_FOREACH_OBJECT(collationFlag, 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; +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 + } else if ([collationFlag isEqualToString:@"nodiacritic"]) { + userArgs->compareOptions |= kCFCompareDiacriticInsensitive; + atLeastOneValidFlag = YES; + } else if ([collationFlag isEqualToString:@"widthinsensitive"]) { + userArgs->compareOptions |= kCFCompareWidthInsensitive; + atLeastOneValidFlag = YES; +#else + } else if (([collationFlag isEqualToString:@"nodiacritic"]) || + ([collationFlag isEqualToString:@"widthinsensitive"])) { + _GTMDevLog(@"GTMSQLiteDatabase 10.5 collating not " + @"available on 10.4 or earlier"); +#endif + } + } + + // 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; +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + if (gCFStringCreateWithBytesNoCopySymbol) { + string1 = gCFStringCreateWithBytesNoCopySymbol(kCFAllocatorDefault, + str1, + length1, + kCFStringEncodingUTF8, + false, + kCFAllocatorNull); + string2 = gCFStringCreateWithBytesNoCopySymbol(kCFAllocatorDefault, + str2, + length2, + kCFStringEncodingUTF8, + false, + kCFAllocatorNull); + } else { + // Have to use the copy-based variants + string1 = CFStringCreateWithBytes(kCFAllocatorDefault, + str1, + length1, + kCFStringEncodingUTF8, + false); + string2 = CFStringCreateWithBytes(kCFAllocatorDefault, + str2, + length2, + kCFStringEncodingUTF8, + false); + } +#else // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + string1 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, + str1, + length1, + kCFStringEncodingUTF8, + false, + kCFAllocatorNull); + string2 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, + str2, + length2, + kCFStringEncodingUTF8, + false, + kCFAllocatorNull); +#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 + 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 defined __BIG_ENDIAN__ + (userArgs->textRep == SQLITE_UTF16BE) +#elif defined __LITTLE_ENDIAN__ + (userArgs->textRep == SQLITE_UTF16LE) +#else +#error Not big or little endian? +#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) { + // COV_NF_START - this code doesn't appear to be hittable + // setSupport implies that the escape char is NULL + // additionally, setSupport implies GLOB, and running a GLOB + // query with an escape clause appears to not work in SQLITE3 + // when I tried it from the command line + // No matter what the character follows the escape copy it to the + // buffer + patternIndex++; + if (patternIndex >= patternLength) { + // 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; + } + patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer, + patternIndex); + // COV_NF_END + } + // 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) { + sqlite3_result_error(context, + "LIKE CF implementation missing pattern or value", -1); + return; + } + 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) { + sqlite3_result_error(context, + "LIKE CF implementation missing pattern or value", -1); + return; + } + 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; +} + +#if GTM_SUPPORT_GC +- (void)finalize { + _GTMDevAssert(!statement_, + @"-[GTMSQLiteStatement finalizeStatement] must be called when" + @" statement is no longer needed"); + [super finalize]; +} +#endif + +- (void)dealloc { + _GTMDevAssert(!statement_, + @"-[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 -- cgit v1.2.3