blob: aa5d5f5ea0348bb00d03a3d566b0d961b09af896 [file] [log] [blame]
//
// 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 <Foundation/Foundation.h>
#import "GTMSQLite.h"
#import "GTMMethodCheck.h"
#import "GTMDefines.h"
#include <limits.h>
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");
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
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 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)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
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:
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