diff options
author | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-02-26 01:35:12 +0000 |
---|---|---|
committer | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-02-26 01:35:12 +0000 |
commit | fde1ea051c0385638accd67362cd25f2fe1ae654 (patch) | |
tree | a43b02c31f73aa8a83e20abf619c02ec25030e32 | |
parent | 93796b6367645ea20b01649c4e397a95370ed727 (diff) |
adding SQLite helper/wrapper
-rw-r--r-- | Foundation/GTMSQLite.h | 713 | ||||
-rw-r--r-- | Foundation/GTMSQLite.m | 2013 | ||||
-rw-r--r-- | Foundation/GTMSQLiteTest.m | 1769 |
3 files changed, 4495 insertions, 0 deletions
diff --git a/Foundation/GTMSQLite.h b/Foundation/GTMSQLite.h new file mode 100644 index 0000000..0697d5a --- /dev/null +++ b/Foundation/GTMSQLite.h @@ -0,0 +1,713 @@ +// +// GTMSQLite.h +// +// Copyright 2006-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. +// +// +// This class is a convenience wrapper for SQLite storage with +// release/retain semantics. In its most basic form, that is all this +// class offers. You have the option of activating "CFAdditions" on +// init which patches or overrides the following SQLite functionality: +// +// - Strings you pass through the API layer will always be converted +// to precomposed UTF-8 with compatibility mapping +// (kCFStringNormalizationFormKC). This is done in an attempt to +// make SQLite correctly handle string equality for composed +// character sequences. This change applies only to +// NSStrings/CFStrings passed through the GTMSQLiteDatabase or +// GTMSQLiteStatement. Direct access to the database using the +// underlying sqlite3_* handles is not affected. +// +// - The SQL UPPER/LOWER functions are replaced with CFString-based +// implementations which (unlike SQLite's native implementation) +// handle case conversion outside the ASCII range. These +// implementations seem to be 20-30% slower than the SQLite +// implementations but may be worth it for accuracy. +// +// - The SQLite "NOCASE" collation is replaced with a CFString-based +// collation that is case insensitive but still uses literal +// comparison (composition-sensitive). +// +// - Additional collation sequences can be created by using these keywords +// separated by underscores. Each option corresponds to a CFStringCompareFlags +// option. +// NOCASE (kCFCompareCaseInsensitive) +// NONLITERAL (kCFCompareNonliteral) +// LOCALIZED (kCFCompareLocalized) +// NUMERIC (kCFCompareNumerically) + +// These additional options are available when linking with the 10.5 SDK: +// NODIACRITIC (kCFCompareDiacriticInsensitive) +// WIDTHINSENSITIVE (kCFCompareWidthInsensitive) +// +// Ordering of the above options can be changed by adding "REVERSE". +// +// Thus, for a case-insensitive, width-insensitive, composition-insensitive +// comparison that ignores diacritical marks and sorts in reverse use: +// +// NOCASE_NONLITERAL_NODIACRITIC_WIDTHINSENSITIVE_REVERSE +// +// - SQL LIKE and GLOB commands are implemented with CFString/CFCharacterSet +// comparisons. As with the other CF additions, this gives us better handling +// of case and composed character sequences. However, whereever reasonable, +// SQLite semantics have been retained. Specific notes: +// +// * LIKE is case insensitive and uses non-literal comparison +// (kCFCompareNonliteral) by default. It is possible to modify this +// behavior using the accessor methods. You must use those methods +// instead of the SQLite "PRAGMA case_sensitive_like" in order for them +// to interact properly with our CFString implementations. +// +// * ESCAPE clauses to LIKE are honored, but the escape character must +// be expressable as a single UniChar (UTF16). The escaped characters in +// LIKE only escape the following UniChar, not a composed character +// sequence. This is not viewed as a limitation since the use of ESCAPE +// is typically only for characters with meaning to SQL LIKE ('%', '_') +// all of which can be expressed as a single UniChar. +// +// * GLOB is by default case sensitive but non-literal. Again, accessor +// methods are available to change this behavior. +// +// * Single character pattern matches ('_' for LIKE, '?' for GLOB) will +// always consume a full composed character sequence. +// +// * As with the standard SQLite implementation, character set comparisons +// are only available for GLOB. +// +// * Character set comparisons are always literal and case sensitive and do +// not take into account composed character sequences. Essentially +// character sets should always be expressed as a set of single UniChars +// or ranges between single UniChars. +// + +// SQLite is preinstalled on 10.4 only. As long as we're using the OS version +// of the library, limit ourself to Tiger+ +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_4 +#error SQLite support is Tiger or later +#endif + +#import <Foundation/Foundation.h> +#import <sqlite3.h> + +/// Wrapper for SQLite with release/retain semantics and CFString convenience features +@interface GTMSQLiteDatabase : NSObject { + @protected + sqlite3 *db_; // strong + NSString *path_; // strong + int timeoutMS_; + BOOL hasCFAdditions_; + CFOptionFlags likeOptions_; + CFOptionFlags globOptions_; + NSMutableArray *userArgDataPool_; // strong +} + +// Get the numeric version number of the SQLite library (compiled in value +// for SQLITE_VERSION_NUMBER). +// +// Returns: +// Integer version number +// ++ (int)sqliteVersionNumber; + +// Get the string version number of the SQLite library. +// +// Returns: +// Autoreleased NSString version string +// ++ (NSString *)sqliteVersionString; + +// Create and open a database instance on a file-based database. +// +// Args: +// path: Path to the database. If it does not exist an empty database +// will be created. +// withCFAdditions: If true, the SQLite database will include CFString +// based string functions and collation sequences. See +// the class header for information on these differences +// and performance impact. +// err: Result code from SQLite. If nil is returned by this function +// check the result code for the error. If NULL no result code is +// reported. +// +- (id)initWithPath:(NSString *)path + withCFAdditions:(BOOL)additions + utf8:(BOOL)useUTF8 + errorCode:(int *)err; + +// Create and open a memory-based database. Memory-based databases +// cannot be shared amongst threads, and each instance is unique. See +// SQLite documentation for details. +// +// For argument details see [... initWithPath:withCFAdditions:errorCode:] +// +- (id)initInMemoryWithCFAdditions:(BOOL)additions + utf8:(BOOL)useUTF8 + errorCode:(int *)err; + +// Get the underlying SQLite database handle. In general you should +// never do this, if you do use this be careful with how you compose +// and decompse strings you pass to the database. +// +// Returns: +// sqlite3 pointer +// +- (sqlite3 *)sqlite3DB; + +// Enable/Disable the database synchronous mode. Disabling +// synchronous mode results in much faster insert throughput at the +// cost of safety. See the SQlite documentation for details. +// +// Args: +// enable: Boolean flag to determine mode. +// +- (void)synchronousMode:(BOOL)enable; + +// Check if this database instance has our CFString functions and collation +// sequences (see top of file for details). +// +// Returns: +// YES if the GTMSQLiteDatabase instance has our CF additions +// +- (BOOL)hasCFAdditions; + +// Set comparison options for the "LIKE" operator for databases with +// our CF addtions active. +// +// Args: +// options: CFStringCompareFlags value. Note that a limited list +// of options are supported. For example one cannot +// use kCFCompareBackwards as an option. +// +- (void)setLikeComparisonOptions:(CFOptionFlags)options; + +// Get current comparison options for the "LIKE" operator in a database +// with our CF additions active. +// +// Returns: +// Current comparison options or zero if CF additions are inactive. +// +- (CFOptionFlags)likeComparisonOptions; + +// Set comparison options for the "GLOB" operator for databases with +// our CF addtions active. +// +// Args: +// options: CFStringCompareFlags value. Note that a limited list +// of options are supported. For example one cannot +// use kCFCompareBackwards as an option. +// +- (void)setGlobComparisonOptions:(CFOptionFlags)options; + +// Get current comparison options for the "GLOB" operator in a database +// with our CF additions active. +// +// Returns: +// Current comparison options or zero if CF additions are inactive. +// +- (CFOptionFlags)globComparisonOptions; + +// Obtain the last error code from the database +// +// Returns: +// SQLite error code, if no error is pending returns SQLITE_OK +// +- (int)lastErrorCode; + +// Obtain an error string for the last error from the database +// +// Returns: +// Autoreleased NSString error message +// +- (NSString *)lastErrorString; + +// Obtain a count of rows added, mmodified or deleted by the most recent +// statement. See sqlite3_changes() for details and limitations. +// +// Returns: +// Row count +// +- (int)lastChangeCount; + +// Obtain a count of rows added, mmodified or deleted since the database +// was opened. See sqlite3_total_changes() for details and limitations. +// +// Returns: +// Row count +// +- (int)totalChangeCount; + +// Obtain the last insert row ID +// +// Returns: +// 64-bit row ID +// +- (unsigned long long)lastInsertRowID; + +// Interrupt any currently running database operations as soon as possible. +// Running operations will receive a SQLITE_INTERRUPT and will need to +// handle it correctly (this is the callers problem to deal with). +// +- (void)interrupt; + +// Set the timeout value in milliseconds. This is a database global affecting +// all running and future statements. +// +// Args: +// timeoutMS: Integer count in ms SQLite will wait for the database to +// unlock before giving up and returning SQLITE_BUSY. A value +// of 0 or less means the database always returns immediately. +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)setBusyTimeoutMS:(int)timeoutMS; + +// Get the current busy timeout in milliseconds. +// +// Returns: +// Current database busy timeout value in ms, 0 or less means no timeout. +// +- (int)busyTimeoutMS; + +// Execute a string containing one or more SQL statements. No returned data +// is available, use GTMSQLiteStatement for that usage. +// +// Args: +// sql: Raw SQL statement to prepare. It is the caller's responsibility +// to properly escape the SQL. +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)executeSQL:(NSString *)sql; + +// Convenience method to start a deferred transaction (most common case). +// +// Returns: +// YES if the transaction started successfully +// +- (BOOL)beginDeferredTransaction; + +// Convenience method to roll back a transaction. +// +// Returns: +// YES if the transaction rolled back successfully +// +- (BOOL)rollback; + +// Convenience method to commit a transaction. +// +// Returns: +// YES if the transaction committed successfully +// +- (BOOL)commit; + +@end + +// Wrapper class for SQLite statements with retain/release semantics. +// Attempts to behave like an NSEnumerator, however you should bind +// your values before beginning enumeration and unlike NSEnumerator, +// a reset is supported. +// +// The GTMSQLiteDatabase class has options to modify some SQL +// functions and force particular string representations. This class +// honors the database preferences for those options. See the +// GTMSQLiteDatabase header for details. + +/// Wrapper class for SQLite statements with retain/release +/// semantics. +@interface GTMSQLiteStatement : NSObject { + +@protected + sqlite3_stmt *statement_; + BOOL hasCFAdditions_; +} + +#pragma mark Creation, Access and Finalization + +// Create an autoreleased prepared statement, see initWithSQL: for arguments. +// +// NOTE: Even though this object is autoreleased you MUST call +// [finalizeStatement] on this when your done. See the init for explanation. +// +// Returns: +// Autoreleased GTMSQLiteStatement +// ++ (id)statementWithSQL:(NSString *)sql + inDatabase:(GTMSQLiteDatabase *)gtmdb + errorCode:(int *)err; + +// Designated initializer, create a prepared statement. Positional and named +// parameters are supported, see the SQLite documentation. +// +// NOTE: Although this object will clean up its statement when deallocated, +// you are REQUIRED to "finalize" the statement when you are +// through with it. Failing to do this will prevent the database from allowing +// new transactions or queries. In other words, leaving an instance on the +// autorelease pool unfinalized may interfere with other database usage if any +// caller sharing the database uses transactions. +// +// Args: +// sql: Raw SQL statement to prepare. It is the caller's responsibility +// to properly escape the SQL and make sure that the SQL contains +// only _one_ statement. Additional statements are silently ignored. +// db: The GTMSQLiteDatabase (not retained) +// err: Result code from SQLite. If nil is returned by this function +// check the result code for the error. If NULL no result code is +// reported. +// +- (id)initWithSQL:(NSString *)sql + inDatabase:(GTMSQLiteDatabase *)gtmdb + errorCode:(int *)err; + +// Get the underlying SQLite statement handle. In general you should never +// do this, if you do use this be careful with how you compose and +// decompse strings you pass to the database. +// +// Returns: +// sqlite3_stmt pointer +// +- (sqlite3_stmt *)sqlite3Statement; + +// Finalize the statement, allowing other transactions to start on the database +// This method MUST be called when you are done with a statement. Failure to +// do so means that the database will not be torn down properly when it's +// retain count drops to 0 or GC collects it. +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)finalizeStatement; + +#pragma mark Parameters and Binding + +// Get the number of parameters that can be bound in the prepared statement. +// +// Returns: +// Integer count of parameters or -1 on error +// +- (int)parameterCount; + +// Get the position of a parameter with a given name. +// +// Args: +// paramName: String name of the parameter, including any leading punctuation +// (see SQLite docs) +// +// Returns: +// 1-based parameter position index or -1 on error +// +- (int)positionOfParameterNamed:(NSString *)paramName; + +// Get the name of a parameter at a particular index. +// +// Args: +// position: Parameter position (1-based index) +// +// Returns: +// Autoreleased string name of the parameter, including any leading +// punctuation (see SQLite docs) or nil on error. +// +- (NSString *)nameOfParameterAtPosition:(int)position; + +// Bind a NULL at a given position +// +// Args: +// position: Parameter position (1-based index) +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindSQLNullAtPosition:(int)position; + +// Bind a blob parameter at a given position index to a raw pointer and +// length. The data will be copied by SQLite +// +// Args: +// position: Parameter position (1-based index) +// bytes: Raw pointer to the data to copy/bind +// length: Number of bytes in the blob +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindBlobAtPosition:(int)position bytes:(void *)bytes length:(int)length; + +// Bind an NSData as a blob at a given position. The data will be copied +// by SQLite. +// +// Args: +// position: Parameter position (1-based index) +// data: NSData to convert to blob +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindBlobAtPosition:(int)position data:(NSData *)data; + +// Bind a double at the given position (for floats convert to double). +// +// Args: +// position: Parameter position (1-based index) +// value: Double to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindDoubleAtPosition:(int)position value:(double)value; + +// Bind an NSNumber as a double value at the given position. +// +// Args: +// position: Parameter position (1-based index) +// number: NSNumber to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindNumberAsDoubleAtPosition:(int)position number:(NSNumber *)number; + +// Bind a 32-bit integer at the given position. +// +// Args: +// position: Parameter position (1-based index) +// value: Integer to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindInt32AtPosition:(int)position value:(int)value; + +// Bind an NSNumber as a 32-bit integer value at the given position. +// +// Args: +// position: Parameter position (1-based index) +// number: NSNumber to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindNumberAsInt32AtPosition:(int)position number:(NSNumber *)number; + +// Bind a 64-bit integer at the given position. +// +// Args: +// position: Parameter position (1-based index) +// value: Int64 value to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindLongLongAtPosition:(int)position value:(long long)value; + +// Bind an NSNumber as a 64-bit integer value at the given position. +// +// Args: +// position: Parameter position (1-based index) +// number: NSNumber to bind +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindNumberAsLongLongAtPosition:(int)position number:(NSNumber *)number; + +// Bind a string at the given position. +// +// Args: +// position: Parameter position (1-based index) +// string: String to bind (string will be converted to UTF8 and copied). +// NOTE: For bindings it is not necessary for you to SQL escape +// your strings. +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)bindStringAtPosition:(int)position string:(NSString *)string; + +#pragma mark Results + +// Get the number of result columns per row this statement will generate. +// +// Returns: +// Column count, 0 if no columns will be returned ("UPDATE.." etc.), +// -1 on error. +// +- (int)resultColumnCount; + +// Get the name of result colument at a given index. +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Autoreleased NSString column name or nil if no column exists at that +// position or error. +// +- (NSString *)resultColumnNameAtPosition:(int)position; + +// Get the number of data values in the current row of this statement. +// Generally this will be the same as resultColumnCount:, except when row +// iteration is done (see SQLite docs for sqlite3_data_count()). +// +// Returns: +// Data count or 0 if no data will be returned, -1 on error. +// +- (int)rowDataCount; + +// Get the SQLite type constant for a column in a row. Note that because +// SQLite does not enforce column type restrictions the type of a particular +// column in a row may not match the declared type of the column. +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// SQLite data type constant (i.e. SQLITE_INTEGER, SQLITE_FLOAT, etc.) or +// -1 on error. +// +- (int)resultColumnTypeAtPosition:(int)position; + +// Get the data for a result row blob column as an NSData +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Autoreleased NSData, nil on error +// +- (NSData *)resultBlobDataAtPosition:(int)position; + +// Get the data for a result row blob column as a double +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Double value +// +- (double)resultDoubleAtPosition:(int)position; + +// Get the data for a result row blob column as an integer +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Integer value +// +- (int)resultInt32AtPosition:(int)position; + +// Get the data for a result row blob column as a long long +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Long long value +// +- (long long)resultLongLongAtPosition:(int)position; + +// Get the data for a result row blob column as an NSNumber +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Autoreleased NSNumber value or nil on error +// +- (NSNumber *)resultNumberAtPosition:(int)position; + +// Get the data for a result row blob column as an NSString +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Autoreleased NSString value or nil on error +// +- (NSString *)resultStringAtPosition:(int)position; + +// Get a Foundation object (NSData, NSNumber, NSString, NSNull) for the column, +// autodetecting the most appropriate representation. +// +// Args: +// position: Column position (0-based index) +// +// Returns: +// Autoreleased Foundation type, nil on error +// +- (id)resultFoundationObjectAtPosition:(int)position; + +// Get an array of Foundation objects for the row in query column order. +// +// Returns: +// Autoreleased array of Foundation types or nil if there is no +// data in the row or error +// +- (NSArray *)resultRowArray; + +// Get a dictionary of Foundation objects for the row keyed by column name. +// +// Returns: +// Autoreleased dictionary of Foundation types or nil if there is no +// data in the row or error. +// +- (NSDictionary *)resultRowDictionary; + +#pragma mark Rows + +// Step the statement forward one row, potentially spinning forever till +// the row can be located (if database is SQLITE_BUSY). +// +// Returns: +// SQLite result code, SQLITE_ROW if a row was found or SQLITE_DONE if +// no further rows match the statement. +// +- (int)stepRow; + +// Step the statement forward one row, waiting at most the currrent database +// busy timeout (see [GTMSQLiteDatabase setBusyTimeoutMS]). +// +// Returns: +// SQLite result code, SQLITE_ROW if a row was found or SQLITE_DONE if +// no further rows match the statement. If SQLITE_BUSY is returned the +// database did not unlock during the timeout. +// +- (int)stepRowWithTimeout; + +// Reset the statement starting again at the first row +// +// Returns: +// SQLite result code, SQLITE_OK on no error +// +- (int)reset; + +// Check if the SQLite parser recognizes the receiver as one or more valid +// SQLite statements. +// +// Returns: +// YES if the string is a complete and valid SQLite statement +// ++ (BOOL)isCompleteStatement:(NSString *)string; + +// Quote and escape the receiver for SQL. +// Example: "This is wild! It's fun!" +// Becomes: "'This is wild! It''s fun!'" +// +// Returns: +// Autoreleased NSString ++ (NSString *)quoteAndEscapeString:(NSString *)string; + +@end 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 <Foundation/Foundation.h> +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 +#import <dlfcn.h> +#endif +#import "GTMSQLite.h" +#import "GTMMethodCheck.h" +#import "GTMDefines.h" +#include <limits.h> +#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 diff --git a/Foundation/GTMSQLiteTest.m b/Foundation/GTMSQLiteTest.m new file mode 100644 index 0000000..578f6b7 --- /dev/null +++ b/Foundation/GTMSQLiteTest.m @@ -0,0 +1,1769 @@ +// +// GTMSQLiteTest.m +// +// Copyright 2006-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 "GTMSQLite.h" +#import "GTMSenTestCase.h" +#import "GTMUnitTestDevLog.h" +#import "GTMGarbageCollection.h" + +@interface GTMSQLiteTest : GTMTestCase +@end + +// This variable is used by a custom upper function that we set in a +// SQLite database to indicate that the custom function was +// successfully called. It has to be a global rather than instance +// variable because the custom upper function is not an instance function +static BOOL customUpperFunctionCalled = NO; + +@interface GTMSQLiteStatementTest : GTMTestCase +@end + +// Prototype for LIKE/GLOB test helper +static NSArray* LikeGlobTestHelper(GTMSQLiteDatabase *db, NSString *sql); + +@implementation GTMSQLiteTest + +// Test cases for change counting +- (void)testTransactionAPI { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + err = [db executeSQL:@"CREATE TABLE foo (bar TEXT COLLATE NOCASE_NONLITERAL);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to create table"); + + int changeCount = [db lastChangeCount]; + STAssertEquals(changeCount, 0, + @"Change count was not 0 after creating database/table!"); + + err = [db executeSQL:@"insert into foo (bar) values ('blah!');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + changeCount = [db lastChangeCount]; + STAssertEquals(changeCount, 1, @"Change count was not 1!"); + + // Test last row id! + unsigned long long lastRowId; + lastRowId = [db lastInsertRowID]; + STAssertEquals(lastRowId, (unsigned long long)1L, + @"First row in database was not 1?"); + + // Test setting busy and retrieving it! + int busyTimeout = 10000; + err = [db setBusyTimeoutMS:busyTimeout]; + STAssertEquals(err, SQLITE_OK, @"Error setting busy timeout"); + + int retrievedBusyTimeout; + retrievedBusyTimeout = [db busyTimeoutMS]; + STAssertEquals(retrievedBusyTimeout, busyTimeout, + @"Retrieved busy time out was not equal to what we set it" + @" to!"); + + BOOL xactOpSucceeded; + + xactOpSucceeded = [db beginDeferredTransaction]; + STAssertTrue(xactOpSucceeded, @"beginDeferredTransaction failed!"); + + err = [db executeSQL:@"insert into foo (bar) values ('blah!');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + changeCount = [db lastChangeCount]; + STAssertEquals(changeCount, 1, + @"Change count didn't stay the same" + @"when inserting during transaction"); + + xactOpSucceeded = [db rollback]; + STAssertTrue(xactOpSucceeded, @"could not rollback!"); + + changeCount = [db lastChangeCount]; + STAssertEquals(changeCount, 1, @"Change count isn't 1 after rollback :-("); + + xactOpSucceeded = [db beginDeferredTransaction]; + STAssertTrue(xactOpSucceeded, @"beginDeferredTransaction failed!"); + + for (unsigned int i = 0; i < 100; i++) { + err = [db executeSQL:@"insert into foo (bar) values ('blah!');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + } + + xactOpSucceeded = [db commit]; + STAssertTrue(xactOpSucceeded, @"could not commit!"); + + changeCount = [db totalChangeCount]; + STAssertEquals(changeCount, 102, @"Change count isn't 102 after commit :-("); +} + +- (void)testSQLiteWithoutCFAdditions { + int err; + GTMSQLiteDatabase *dbNoCFAdditions = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:NO + utf8:YES + errorCode:&err] + autorelease]; + + STAssertNotNil(dbNoCFAdditions, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + err = [dbNoCFAdditions executeSQL:nil]; + STAssertEquals(err, SQLITE_MISUSE, @"Nil SQL did not return error"); + + err = [dbNoCFAdditions executeSQL:@"SELECT UPPER('Fred');"]; + STAssertEquals(err, SQLITE_OK, @"Nil SQL did not return error"); +} + +- (void)testSynchronousAPI { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + [db synchronousMode:YES]; + [db synchronousMode:NO]; +} + +- (void)testEmptyStringsCollation { + int err; + GTMSQLiteDatabase *db8 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + STAssertNotNil(db8, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + GTMSQLiteDatabase *db16 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] + autorelease]; + + STAssertNotNil(db16, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + NSArray *databases = [NSArray arrayWithObjects:db8, db16, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + err = [db executeSQL: + @"CREATE TABLE foo (bar TEXT COLLATE NOCASE_NONLITERAL," + @" barrev text collate reverse);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for collation test"); + // Create blank rows to test matching inside collation functions + err = [db executeSQL:@"insert into foo (bar, barrev) values ('','');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + // Insert one row we want to match + err = [db executeSQL: + @"INSERT INTO foo (bar, barrev) VALUES " + @"('teststring','teststring');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + NSString *matchString = @"foobar"; + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE bar == '%@';", matchString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + [statement stepRow]; + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE barrev == '%@' order by barrev;", matchString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + [statement stepRow]; + + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE bar == '';"] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + [statement stepRow]; + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE barrev == '' order by barrev;"] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + [statement stepRow]; + [statement finalizeStatement]; + } +} + +- (void)testUTF16Database { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] + autorelease]; + + STAssertNotNil(db, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + err = [db executeSQL:@"CREATE TABLE foo (bar TEXT COLLATE NOCASE_NONLITERAL);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to create table for collation test"); + + // Insert one row we want to match + err = [db executeSQL:[NSString stringWithFormat: + @"INSERT INTO foo (bar) VALUES ('%@');", + [NSString stringWithCString:"Frédéric" encoding:NSUTF8StringEncoding]]]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + // Create blank rows to test matching inside collation functions + err = [db executeSQL:@"insert into foo (bar) values ('');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + err = [db executeSQL:@"insert into foo (bar) values ('');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + // Loop over a few things all of which should match + NSArray *testArray = [NSArray arrayWithObjects: + [NSString stringWithCString:"Frédéric" + encoding:NSUTF8StringEncoding], + [NSString stringWithCString:"frédéric" + encoding:NSUTF8StringEncoding], + [NSString stringWithCString:"FRÉDÉRIC" + encoding:NSUTF8StringEncoding], + nil]; + NSString *testString = nil; + GTM_FOREACH_OBJECT(testString, testArray) { + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE bar == '%@';", testString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + int count = 0; + while ([statement stepRow] == SQLITE_ROW) { + count++; + } + STAssertEquals(count, 1, @"Wrong number of collated rows for \"%@\"", + testString); + [statement finalizeStatement]; + } + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:@"select * from foo;" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + + while ([statement stepRow] == SQLITE_ROW) ; + [statement finalizeStatement]; + +} + +- (void)testUpperLower { + + // Test our custom UPPER/LOWER implementation, need a database and statement + // to do it. + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + STAssertNotNil(db, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + GTMSQLiteStatement *statement = nil; + + // Test simple ASCII + statement = [GTMSQLiteStatement statementWithSQL:@"SELECT LOWER('Fred');" + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + @"fred", + @"LOWER failed for ASCII string"); + [statement finalizeStatement]; + + statement = [GTMSQLiteStatement statementWithSQL:@"SELECT UPPER('Fred');" + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + @"FRED", + @"UPPER failed for ASCII string"); + + [statement finalizeStatement]; + // Test UTF-8, have to do some dancing to make the compiler take + // UTF8 literals + NSString *utfNormalString = + [NSString stringWithCString:"Frédéric" + encoding:NSUTF8StringEncoding]; + NSString *utfLowerString = + [NSString stringWithCString:"frédéric" + encoding:NSUTF8StringEncoding]; + NSString *utfUpperString = + [NSString stringWithCString:"FRÉDÉRIC" encoding:NSUTF8StringEncoding]; + + statement = + [GTMSQLiteStatement statementWithSQL: + [NSString stringWithFormat:@"SELECT LOWER('%@');", utfNormalString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + utfLowerString, + @"UPPER failed for UTF8 string"); + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL: + [NSString stringWithFormat:@"SELECT UPPER('%@');", utfNormalString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + utfUpperString, + @"UPPER failed for UTF8 string"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"Should be done"); + [statement finalizeStatement]; +} + +- (void)testUpperLower16 { + + // Test our custom UPPER/LOWER implementation, need a database and + // statement to do it. + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] + autorelease]; + STAssertNotNil(db, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + GTMSQLiteStatement *statement = nil; + + // Test simple ASCII + statement = [GTMSQLiteStatement statementWithSQL:@"SELECT LOWER('Fred');" + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + @"fred", + @"LOWER failed for ASCII string"); + [statement finalizeStatement]; + + statement = [GTMSQLiteStatement statementWithSQL:@"SELECT UPPER('Fred');" + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertEqualObjects([statement resultStringAtPosition:0], + @"FRED", + @"UPPER failed for ASCII string"); + [statement finalizeStatement]; +} + +typedef struct { + BOOL upperCase; + int textRep; +} UpperLowerUserArgs; + +static void TestUpperLower16Impl(sqlite3_context *context, + int argc, sqlite3_value **argv); + +- (void)testUTF16DatabasesAreReallyUTF16 { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] + autorelease]; + + const struct { + const char *sqlName; + UpperLowerUserArgs userArgs; + void *function; + } customUpperLower[] = { + { "upper", { YES, SQLITE_UTF16 }, &TestUpperLower16Impl }, + { "upper", { YES, SQLITE_UTF16BE }, &TestUpperLower16Impl }, + { "upper", { YES, SQLITE_UTF16LE }, &TestUpperLower16Impl } + }; + + + sqlite3 *sqldb = [db sqlite3DB]; + int rc; + for (size_t i = 0; + i < (sizeof(customUpperLower) / sizeof(customUpperLower[0])); + i++) { + rc = sqlite3_create_function(sqldb, + customUpperLower[i].sqlName, + 1, + customUpperLower[i].userArgs.textRep, + (void *)&customUpperLower[i].userArgs, + customUpperLower[i].function, + NULL, + NULL); + STAssertEquals(rc, SQLITE_OK, + @"Failed to register upper function" + @"with SQLite db"); + } + + customUpperFunctionCalled = NO; + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:@"SELECT UPPER('Fred');" + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"Failed to step row"); + STAssertTrue(customUpperFunctionCalled, + @"Custom upper function was not called!"); + [statement finalizeStatement]; +} + +- (void)testLikeComparisonOptions { + int err; + + GTMSQLiteDatabase *db8 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] autorelease]; + + GTMSQLiteDatabase *db16 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] autorelease]; + + NSArray *databases = [NSArray arrayWithObjects:db8, db16, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + CFOptionFlags c = 0, oldFlags; + + oldFlags = [db likeComparisonOptions]; + + // We'll do a case sensitivity test by making comparison options + // case insensitive + [db setLikeComparisonOptions:c]; + + STAssertTrue([db likeComparisonOptions] == 0, + @"LIKE Comparison options setter/getter does not work!"); + + err = [db executeSQL:@"CREATE TABLE foo (bar TEXT);"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for like comparison options test"); + + err = [db executeSQL:@"insert into foo values('test like test');"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create row for like comparison options test"); + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:@"select * from foo where bar like '%LIKE%'" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"failed to retrieve row!"); + + // Now change it back to case insensitive and rerun the same query + c |= kCFCompareCaseInsensitive; + [db setLikeComparisonOptions:c]; + err = [statement reset]; + STAssertEquals(err, SQLITE_OK, @"failed to reset select statement"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to retrieve row!"); + + // Now reset comparison options + [db setLikeComparisonOptions:oldFlags]; + + [statement finalizeStatement]; + } +} + +- (void)testGlobComparisonOptions { + int err; + GTMSQLiteDatabase *db = [[[GTMSQLiteDatabase alloc] + initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] autorelease]; + + CFOptionFlags c = 0, oldFlags; + + oldFlags = [db globComparisonOptions]; + + [db setGlobComparisonOptions:c]; + + STAssertTrue([db globComparisonOptions] == 0, + @"GLOB Comparison options setter/getter does not work!"); + + err = [db executeSQL:@"CREATE TABLE foo (bar TEXT);"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for glob comparison options test"); + + err = [db executeSQL:@"insert into foo values('test like test');"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create row for glob comparison options test"); + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:@"select * from foo where bar GLOB 'TEST*'" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"failed to retrieve row!"); + + // Now change it back to case insensitive and rerun the same query + c |= kCFCompareCaseInsensitive; + [db setGlobComparisonOptions:c]; + err = [statement reset]; + STAssertEquals(err, SQLITE_OK, @"failed to reset select statement"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to retrieve row!"); + + [statement finalizeStatement]; + + // Now reset comparison options + [db setGlobComparisonOptions:oldFlags]; +} + +- (void)testCFStringReverseCollation { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] autorelease]; + + err = [db executeSQL:@"CREATE table foo_reverse (bar TEXT COLLATE REVERSE);"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for reverse collation test"); + + err = [db executeSQL:@"insert into foo_reverse values('a2');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + err = [db executeSQL:@"insert into foo_reverse values('b1');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:@"SELECT bar from foo_reverse order by bar" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row"); + NSString *oneRow = [statement resultStringAtPosition:0]; + + STAssertEqualStrings(oneRow, @"b1", @"b did not come first!"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row!"); + + STAssertEquals(err, [db lastErrorCode], + @"lastErrorCode API did not match what last API returned!"); + // Calling lastErrorCode resets API error, so the next string will not indicate any error + STAssertEqualStrings(@"not an error", [db lastErrorString], + @"lastErrorString API did not match expected string!"); + + oneRow = [statement resultStringAtPosition:0]; + STAssertEqualStrings(oneRow, @"a2", @"a did not come second!"); + + [statement finalizeStatement]; +} + +- (void)testCFStringNumericCollation { + int err; + GTMSQLiteDatabase *db = [[[GTMSQLiteDatabase alloc] + initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] autorelease]; + + err = [db executeSQL: + @"CREATE table numeric_test_table " + @"(numeric_sort TEXT COLLATE NUMERIC, lexographic_sort TEXT);"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for numeric collation test"); + + err = [db executeSQL:@"insert into numeric_test_table values('4','17');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + err = [db executeSQL:@"insert into numeric_test_table values('17','4');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:@"SELECT numeric_sort from numeric_test_table order by numeric_sort" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row"); + NSString *oneRow = [statement resultStringAtPosition:0]; + + STAssertEqualStrings(oneRow, @"4", @"4 did not come first!"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row!"); + + oneRow = [statement resultStringAtPosition:0]; + STAssertEqualStrings(oneRow, @"17", @"17 did not come second!"); + + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL: + @"SELECT lexographic_sort from numeric_test_table " + @"order by lexographic_sort" + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"failed to create statement for lexographic sort"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row"); + oneRow = [statement resultStringAtPosition:0]; + + STAssertEqualStrings(oneRow, @"17", @"17 did not come first!"); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, @"failed to advance row!"); + + oneRow = [statement resultStringAtPosition:0]; + STAssertEqualStrings(oneRow, @"4", @"4 did not come second!"); + + [statement finalizeStatement]; +} + +- (void)testCFStringCollation { + + // Test just one case of the collations, they all exercise largely the + // same code + int err; + GTMSQLiteDatabase *db = + [[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err]; + STAssertNotNil(db, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + err = [db executeSQL: + @"CREATE TABLE foo (bar TEXT COLLATE NOCASE_NONLITERAL_LOCALIZED);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to create table for collation test"); + + // Insert one row we want to match + err = [db executeSQL:[NSString stringWithFormat: + @"INSERT INTO foo (bar) VALUES ('%@');", + [NSString stringWithCString:"Frédéric" encoding:NSUTF8StringEncoding]]]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute SQL"); + + // Loop over a few things all of which should match + NSArray *testArray = [NSArray arrayWithObjects: + [NSString stringWithCString:"Frédéric" + encoding:NSUTF8StringEncoding], + [NSString stringWithCString:"frédéric" + encoding:NSUTF8StringEncoding], + [NSString stringWithCString:"FRÉDÉRIC" + encoding:NSUTF8StringEncoding], + nil]; + + NSString *testString = nil; + GTM_FOREACH_OBJECT(testString, testArray) { + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL:[NSString stringWithFormat: + @"SELECT bar FROM foo WHERE bar == '%@';", testString] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + int count = 0; + while ([statement stepRow] == SQLITE_ROW) { + count++; + } + STAssertEquals(count, 1, @"Wrong number of collated rows for \"%@\"", + testString); + [statement finalizeStatement]; + } + + // Force a release to test the statement cleanup + [db release]; + +} + +// Diacritic & width insensitive collations are not supported +// on Tiger +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 +- (void)testDiacriticAndWidthInsensitiveCollations { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] autorelease]; + STAssertNotNil(db, @"Failed to create DB"); + STAssertEquals(err, SQLITE_OK, @"Failed to create DB"); + + NSString *tableSQL = + @"CREATE TABLE FOOBAR (collated TEXT COLLATE NODIACRITIC_WIDTHINSENSITIVE, " + @" noncollated TEXT);"; + + err = [db executeSQL:tableSQL]; + STAssertEquals(err, SQLITE_OK, @"error creating table"); + + NSString *testStringValue = [NSString stringWithCString:"Frédéric" + encoding:NSUTF8StringEncoding]; + // Insert one row we want to match + err = [db executeSQL:[NSString stringWithFormat: + @"INSERT INTO FOOBAR (collated, noncollated) " + @"VALUES ('%@','%@');", + testStringValue, testStringValue]]; + + GTMSQLiteStatement *statement = + [GTMSQLiteStatement statementWithSQL: + [NSString stringWithFormat:@"SELECT noncollated FROM foobar" + @" WHERE noncollated == 'Frederic';"] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + // Make sure the comparison query didn't return a row because + // we're doing a comparison on the row without the collation + STAssertEquals([statement stepRow], SQLITE_DONE, + @"Comparison with diacritics did not succeed"); + + [statement finalizeStatement]; + + statement = + [GTMSQLiteStatement statementWithSQL: + [NSString stringWithFormat:@"SELECT collated FROM foobar" + @" WHERE collated == 'Frederic';"] + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + STAssertEquals([statement stepRow], SQLITE_ROW, + @"Comparison ignoring diacritics did not succeed"); + [statement finalizeStatement]; +} +#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_ + +- (void)testCFStringLikeGlob { + + // Test cases drawn from SQLite test case source + int err; + GTMSQLiteDatabase *db8 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + STAssertNotNil(db8, @"Failed to create database"); + STAssertEquals(err, SQLITE_OK, @"Failed to create database"); + + GTMSQLiteDatabase *db16 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:NO + errorCode:&err] + autorelease]; + + STAssertNotNil(db16, @"Failed to create database"); + STAssertEquals(err, SQLITE_OK, @"Failed to create database"); + + NSArray *databases = [NSArray arrayWithObjects:db8, db16, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + err = [db executeSQL:@"CREATE TABLE t1 (x TEXT);"]; + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for LIKE/GLOB test"); + + // Insert data set + err = [db executeSQL:@"INSERT INTO t1 VALUES ('a');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('ab');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('abc');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('abcd');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('acd');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('abd');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('bc');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('bcd');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('xyz');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('ABC');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('CDE');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + err = [db executeSQL:@"INSERT INTO t1 VALUES ('ABC abc xyz');"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + + // Section 1, case tests + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"ABC", @"abc", nil]), + @"Fail on LIKE test 1.1"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'abc' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", nil]), + @"Fail on LIKE test 1.2"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'ABC' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"ABC", @"abc", nil]), + @"Fail on LIKE test 1.3"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"ABC", @"ABC abc xyz", @"abc", @"abcd", nil]), + @"Fail on LIKE test 3.1"); + [db setLikeComparisonOptions:(kCFCompareNonliteral)]; + err = [db executeSQL:@"CREATE INDEX i1 ON t1(x);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.3"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'a_c' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", nil]), + @"Fail on LIKE test 3.5"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'ab%d' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abcd", @"abd", nil]), + @"Fail on LIKE test 3.7"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'a_c%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.9"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE '%bcd' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abcd", @"bcd", nil]), + @"Fail on LIKE test 3.11"); + [db setLikeComparisonOptions:(kCFCompareNonliteral | kCFCompareCaseInsensitive)]; + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"ABC", @"ABC abc xyz", @"abc", @"abcd", nil]), + @"Fail on LIKE test 3.13"); + [db setLikeComparisonOptions:(kCFCompareNonliteral)]; + err = [db executeSQL:@"DROP INDEX i1;"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.15"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'abc*' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.17"); + err = [db executeSQL:@"CREATE INDEX i1 ON t1(x);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'abc*' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.19"); + [db setLikeComparisonOptions:(kCFCompareNonliteral)]; + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'abc*' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 3.21"); + [db setLikeComparisonOptions:(kCFCompareNonliteral | + kCFCompareCaseInsensitive)]; + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'a[bc]d' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abd", @"acd", nil]), + @"Fail on LIKE test 3.23"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a[^xyz]d' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abd", @"acd", nil]), + @"Fail on glob inverted character set test 3.24"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a[^' ORDER BY 1;"), + ([NSArray array]), + @"Fail on glob inverted character set test 3.25"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a['"), + ([NSArray array]), + @"Unclosed glob character set did not return empty result set 3.26"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a[^]'"), + ([NSArray array]), + @"Unclosed glob inverted character set did not return empty " + @"result set 3.27"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a[^]c]d'"), + ([NSArray arrayWithObjects:@"abd", nil]), + @"Glob character set with inverted set not matching ] did not " + @"return right rows 3.28"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x GLOB 'a[bcdefg'"), + ([NSArray array]), + @"Unclosed glob character set did not return empty result set 3.29"); + + // Section 4 + [db setLikeComparisonOptions:(kCFCompareNonliteral)]; + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 4.1"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE +x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 4.2"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE ('ab' || 'c%') ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 4.3"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x LIKE 'a[xyz]\%' ESCAPE ''"), + ([NSArray array]), + @"0-Character escape clause did not return empty set 4.4"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x LIKE " + @"'a[xyz]\%' ESCAPE NULL"), + ([NSArray array]), + @"Null escape did not return empty set 4.5"); + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x from t1 where x LIKE 'a[xyz]\\%' " + @"ESCAPE '\\'"), + ([NSArray array]), + @"Literal percent match using ESCAPE clause did not return empty result " + @"set 4.6"); + + + // Section 5 + [db setLikeComparisonOptions:(kCFCompareNonliteral | kCFCompareCaseInsensitive)]; + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"ABC", @"ABC abc xyz", @"abc", @"abcd", nil]), + @"Fail on LIKE test 5.1"); + + err = [db executeSQL:@"CREATE TABLE t2(x COLLATE NOCASE);"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + + err = [db executeSQL:@"INSERT INTO t2 SELECT * FROM t1;"]; + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + + err = [db executeSQL:@"CREATE INDEX i2 ON t2(x COLLATE NOCASE);"]; + + STAssertEquals(err, SQLITE_OK, @"Failed to execute sql"); + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t2 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"ABC", @"ABC abc xyz", @"abcd", nil]), + @"Fail on LIKE test 5.3"); + + [db setLikeComparisonOptions:(kCFCompareNonliteral)]; + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t2 WHERE x LIKE 'abc%' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 5.5"); + + [db setLikeComparisonOptions:(kCFCompareNonliteral | kCFCompareCaseInsensitive)]; + + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t2 WHERE x GLOB 'abc*' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abc", @"abcd", nil]), + @"Fail on LIKE test 5.5"); + + // Non standard tests not from the SQLite source + STAssertEqualObjects( + LikeGlobTestHelper(db, + @"SELECT x FROM t1 WHERE x GLOB 'a[b-d]d' ORDER BY 1;"), + ([NSArray arrayWithObjects:@"abd", @"acd", nil]), + @"Fail on GLOB with character range"); + } +} + +- (void)testDescription { + int err; + GTMSQLiteDatabase *db8 = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + STAssertNotNil(db8, @"Failed to create database"); + STAssertEquals(err, SQLITE_OK, @"Failed to create database"); + STAssertNotNil([db8 description], nil); +} + +// // From GTMSQLite.m +// CFStringEncoding SqliteTextEncodingToCFStringEncoding(int enc); + +// - (void)testEncodingMappingIsCorrect { +// STAssertTrue(SqliteTextEncodingToCFStringEncoding(SQLITE_UTF8) == +// kCFStringEncodingUTF8, +// @"helper method didn't return right encoding for " +// @"kCFStringEncodingUTF8"); + +// STAssertTrue(SqliteTextEncodingToCFStringEncoding(SQLITE_UTF16BE) +// == kCFStringEncodingUTF16BE, +// @"helper method didn't return right encoding for " +// @"kCFStringEncodingUTF16BE"); + +// STAssertTrue(SqliteTextEncodingToCFStringEncoding(SQLITE_UTF16LE) +// == kCFStringEncodingUTF16LE, +// @"helper method didn't return right encoding for " +// @"kCFStringEncodingUTF16LE"); + +// STAssertTrue(SqliteTextEncodingToCFStringEncoding(9999) +// == kCFStringEncodingUTF8, @"helper method didn't " +// @"return default encoding for invalid input"); +// } + +@end + + +// Helper function for LIKE/GLOB testing +static NSArray* LikeGlobTestHelper(GTMSQLiteDatabase *db, NSString *sql) { + + int err; + NSMutableArray *resultArray = [NSMutableArray array]; + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:sql + inDatabase:db + errorCode:&err]; + if (!statement || err != SQLITE_OK) return nil; + while ([statement stepRow] == SQLITE_ROW) { + id result = [statement resultFoundationObjectAtPosition:0]; + if (result) [resultArray addObject:result]; + } + if (err != SQLITE_DONE && err != SQLITE_OK) { + resultArray = nil; + } + [statement finalizeStatement]; + + return resultArray; +} + +// ============================================================================= + +@implementation GTMSQLiteStatementTest + +#pragma mark Parameters/binding tests + +- (void)testInitAPI { + int err; + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:nil + inDatabase:nil + errorCode:&err]; + STAssertNil(statement, @"Create statement succeeded with nil SQL string"); + STAssertEquals(err, SQLITE_MISUSE, @"Err was not SQLITE_MISUSE on nil " + @"SQL string"); + + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + statement = [GTMSQLiteStatement statementWithSQL:@"select * from blah" + inDatabase:db + errorCode:&err]; + + STAssertNil(statement, @"Select statement succeeded with invalid table"); + STAssertNotEquals(err, SQLITE_OK, + @"Err was not SQLITE_MISUSE on invalid table"); + + [statement finalizeStatement]; +} + +- (void)testParameterCountAPI { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + NSString *tableCreateSQL = + @"CREATE TABLE foo (tc TEXT," + @"ic integer," + @"rc real," + @"bc blob);"; + + err = [db executeSQL:tableCreateSQL]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for collation test"); + NSString *insert = + @"insert into foo (tc, ic, rc, bc) values (:tc, :ic, :rc, :bc);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + STAssertEquals([statement parameterCount], 4, + @"Bound parameter count was not 4"); + + [statement sqlite3Statement]; + [statement finalizeStatement]; +} + +- (void)testPositionOfNamedParameterAPI { + int err; + + GTMSQLiteDatabase *dbWithCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + GTMSQLiteDatabase *dbWithoutCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:NO + utf8:YES + errorCode:&err] + autorelease]; + + NSArray *databases = [NSArray arrayWithObjects:dbWithCF, dbWithoutCF, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + NSString *tableCreateSQL = + @"CREATE TABLE foo (tc TEXT," + @"ic integer," + @"rc real," + @"bc blob);"; + err = [db executeSQL:tableCreateSQL]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for collation test"); + NSString *insert = + @"insert into foo (tc, ic, rc, bc) " + @"values (:tc, :ic, :rc, :bc);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create statement"); + + NSArray *parameterNames = [NSArray arrayWithObjects:@":tc", + @":ic", + @":rc", + @":bc", nil]; + + for (unsigned int i = 1; i <= [parameterNames count]; i++) { + NSString *paramName = [parameterNames objectAtIndex:i-1]; + // Cast to signed int to avoid type errors from STAssertEquals + STAssertEquals((int)i, + [statement positionOfParameterNamed:paramName], + @"positionOfParameterNamed API did not return correct " + @"results"); + STAssertEqualStrings(paramName, + [statement nameOfParameterAtPosition:i], + @"nameOfParameterAtPosition API did not return " + @"correct name"); + } + [statement finalizeStatement]; + } +} + +- (void)testBindingBlob { + int err; + const int BLOB_COLUMN = 0; + GTMSQLiteDatabase *dbWithCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + GTMSQLiteDatabase *dbWithoutCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:NO + utf8:YES + errorCode:&err] + autorelease]; + + NSArray *databases = [NSArray arrayWithObjects:dbWithCF, dbWithoutCF, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + // Test strategy is to create a table with 3 columns + // Insert some values, and use the result collection APIs + // to make sure we get the same values back + err = [db executeSQL: + @"CREATE TABLE blobby (bc blob);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for BLOB binding test"); + NSString *insert = @"insert into blobby (bc) values (:bc);"; + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + char bytes[] = "DEADBEEF"; + NSUInteger bytesLen = strlen(bytes); + NSData *originalBytes = [NSData dataWithBytes:bytes length:bytesLen]; + + err = [statement bindBlobAtPosition:1 data:originalBytes]; + + STAssertEquals(err, SQLITE_OK, @"error binding BLOB at position 1"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"failed to insert BLOB for BLOB test"); + + [statement finalizeStatement]; + + NSString *selectSQL = @"select * from blobby;"; + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + STAssertEquals([statement resultColumnCount], 1, + @"result had more columns than the table had?"); + + STAssertEqualStrings([statement resultColumnNameAtPosition:BLOB_COLUMN], + @"bc", + @"column name dictionary was not correct"); + + STAssertEquals([statement rowDataCount], + 1, + @"More than one column returned?"); + + STAssertEquals([statement resultColumnTypeAtPosition:BLOB_COLUMN], + SQLITE_BLOB, + @"Query for column 1 of test table was not BLOB!"); + + NSData *returnedbytes = [statement resultBlobDataAtPosition:BLOB_COLUMN]; + STAssertTrue([originalBytes isEqualToData:returnedbytes], + @"Queried data was not equal :-("); + [statement finalizeStatement]; + } +} + +- (void)testBindingNull { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + err = [db executeSQL: + @"CREATE TABLE foo (tc TEXT);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for NULL binding test"); + NSString *insert = @"insert into foo (tc) values (:tc);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + err = [statement bindSQLNullAtPosition:1]; + + STAssertEquals(err, SQLITE_OK, @"error binding NULL at position 1"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"failed to insert NULL for Null Binding test"); + + [statement finalizeStatement]; + + NSString *selectSQL = @"select 1 from foo where tc is NULL;"; + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + [statement finalizeStatement]; +} + +- (void)testBindingDoubles { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + // Test strategy is to create a table with 2 real columns. + // For the first one, we'll use bindDoubleAtPosition + // For the second one, we'll use bindNumberAsDoubleAtPosition + // Then, for verification, we'll use a query that returns + // all rows where the columns are equal + double testVal = 42.42; + NSNumber *doubleValue = [NSNumber numberWithDouble:testVal]; + + err = [db executeSQL: + @"CREATE TABLE realTable (rc1 REAL, rc2 REAL);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for double binding test"); + NSString *insert = @"insert into realTable (rc1, rc2) values (:rc1, :rc2);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + err = [statement bindDoubleAtPosition:1 value:testVal]; + STAssertEquals(err, SQLITE_OK, @"error binding double at position 1"); + + err = [statement bindNumberAsDoubleAtPosition:2 number:doubleValue]; + STAssertEquals(err, SQLITE_OK, + @"error binding number as double at " + @"position 2"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, + @"failed to insert doubles for double " + @"binding test"); + + [statement finalizeStatement]; + + NSString *selectSQL = @"select rc1, rc2 from realTable where rc1 = rc2;"; + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + double retrievedValue = [statement resultDoubleAtPosition:0]; + STAssertEquals(retrievedValue, testVal, + @"Retrieved double did not equal " + @"original"); + + NSNumber *retrievedNumber = [statement resultNumberAtPosition:1]; + STAssertEqualObjects(retrievedNumber, doubleValue, + @"Retrieved NSNumber object did not equal"); + + [statement finalizeStatement]; +} + +- (void) testResultCollectionAPI { + int err; + GTMSQLiteDatabase *dbWithCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + GTMSQLiteDatabase *dbWithoutCF = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:NO + utf8:YES + errorCode:&err] + autorelease]; + + NSArray *databases = [NSArray arrayWithObjects:dbWithCF, dbWithoutCF, nil]; + GTMSQLiteDatabase *db; + GTM_FOREACH_OBJECT(db, databases) { + // Test strategy is to create a table with 3 columns + // Insert some values, and use the result collection APIs + // to make sure we get the same values back + err = [db executeSQL: + @"CREATE TABLE test (a integer, b text, c blob, d text);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for result collection test"); + + NSString *insert = + @"insert into test (a, b, c, d) " + @"values (42, 'text text', :bc, null);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + + char blobChars[] = "DEADBEEF"; + NSUInteger blobLength = strlen(blobChars); + NSData *blobData = [NSData dataWithBytes:blobChars length:blobLength]; + + err = [statement bindBlobAtPosition:1 data:blobData]; + STAssertEquals(err, SQLITE_OK, @"error binding BLOB at position 1"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, + @"failed to insert doubles for double " + @"binding test"); + + NSString *selectSQL = @"select * from test;"; + + [statement finalizeStatement]; + + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + STAssertNotNil([statement resultRowArray], + @"Failed to retrieve result array"); + STAssertNotNil([statement resultRowDictionary], + @"Failed to retrieve result dictionary"); + [statement finalizeStatement]; + } +} + +- (void) testBindingIntegers { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + // Test strategy is to create a table with 2 real columns. + // For the first one, we'll use bindIntegerAtPosition + // For the second one, we'll use bindNumberAsIntegerAtPosition + // Then, for verification, we'll use a query that returns + // all rows where the columns are equal + int testVal = 42; + NSNumber *intValue = [NSNumber numberWithInt:testVal]; + + err = [db executeSQL: + @"CREATE TABLE integerTable (ic1 integer, ic2 integer);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for integer binding test"); + NSString *insert = + @"insert into integerTable (ic1, ic2) values (:ic1, :ic2);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + err = [statement bindInt32AtPosition:1 value:testVal]; + STAssertEquals(err, SQLITE_OK, @"error binding integer at position 1"); + + err = [statement bindNumberAsInt32AtPosition:2 number:intValue]; + STAssertEquals(err, SQLITE_OK, + @"error binding number as integer at " + @"position 2"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, + @"failed to insert integers for integer " + @"binding test"); + + [statement finalizeStatement]; + + NSString *selectSQL = @"select ic1, ic2 from integerTable where ic1 = ic2;"; + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + int retrievedValue = [statement resultInt32AtPosition:0]; + STAssertEquals(retrievedValue, testVal, + @"Retrieved integer did not equal " + @"original"); + + NSNumber *retrievedNumber = [statement resultNumberAtPosition:1]; + STAssertEqualObjects(retrievedNumber, intValue, + @"Retrieved NSNumber object did not equal"); + + [statement finalizeStatement]; +} + +- (void) testBindingLongLongs { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + // Test strategy is to create a table with 2 long long columns. + // For the first one, we'll use bindLongLongAtPosition + // For the second one, we'll use bindNumberAsLongLongAtPosition + // Then, for verification, we'll use a query that returns + // all rows where the columns are equal + long long testVal = LLONG_MAX; + NSNumber *longlongValue = [NSNumber numberWithLongLong:testVal]; + + err = [db executeSQL: + @"CREATE TABLE longlongTable (llc1 integer, llc2 integer);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for long long binding test"); + NSString *insert = + @"insert into longlongTable (llc1, llc2) " + @"values (:llc1, :llc2);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + err = [statement bindLongLongAtPosition:1 value:testVal]; + STAssertEquals(err, SQLITE_OK, @"error binding long long at position 1"); + + err = [statement bindNumberAsLongLongAtPosition:2 number:longlongValue]; + STAssertEquals(err, SQLITE_OK, + @"error binding number as long long at " + @"position 2"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, + @"failed to insert long longs for long long " + @"binding test"); + + [statement finalizeStatement]; + + NSString *selectSQL = @"select llc1, llc2 from longlongTable where llc1 = llc2;"; + + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + long long retrievedValue = [statement resultLongLongAtPosition:0]; + STAssertEquals(retrievedValue, testVal, + @"Retrieved long long did not equal " + @"original"); + + NSNumber *retrievedNumber = [statement resultNumberAtPosition:1]; + STAssertEqualObjects(retrievedNumber, longlongValue, + @"Retrieved NSNumber object did not equal"); + + [statement finalizeStatement]; +} + +- (void) testBindingString { + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + // Test strategy is to create a table with 1 string column + // Then, for verification, we'll use a query that returns + // all rows where the strings are equal + err = [db executeSQL: + @"CREATE TABLE stringTable (sc1 text);"]; + + STAssertEquals(err, SQLITE_OK, + @"Failed to create table for string binding test"); + + NSString *insert = + @"insert into stringTable (sc1) " + @"values (:sc1);"; + + GTMSQLiteStatement *statement = [GTMSQLiteStatement statementWithSQL:insert + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create insert statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create insert statement"); + + NSString *testVal = @"this is a test string"; + err = [statement bindStringAtPosition:1 string:testVal]; + STAssertEquals(err, SQLITE_OK, @"error binding string at position 1"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, + @"failed to insert string for string binding test"); + + [statement finalizeStatement]; + + NSString *selectSQL = + [NSString stringWithFormat:@"select 1 from stringtable where sc1 = '%@';", + testVal]; + + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + // Check that we got at least one row back + STAssertEquals(err, SQLITE_ROW, @"did not retrieve a row from db :-("); + err = [statement stepRow]; + STAssertEquals(err, SQLITE_DONE, @"retrieved more than 1 row from db :-("); + [statement finalizeStatement]; +} + +- (void)testThatNotFinalizingStatementsThrowsAssertion { + // The run-time check is discouraged, but we're using it because the + // same test binary is used for both GC & Non-GC runs + if (!GTMIsGarbageCollectionEnabled()) { + NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; + + int err; + GTMSQLiteDatabase *db = + [[[GTMSQLiteDatabase alloc] initInMemoryWithCFAdditions:YES + utf8:YES + errorCode:&err] + autorelease]; + + STAssertNotNil(db, @"Failed to create database"); + + NSString *selectSQL = @"select 1"; + GTMSQLiteStatement *statement; + statement = [GTMSQLiteStatement statementWithSQL:selectSQL + inDatabase:db + errorCode:&err]; + + STAssertNotNil(statement, @"Failed to create select statement"); + STAssertEquals(err, SQLITE_OK, @"Failed to create select statement"); + + err = [statement stepRow]; + STAssertEquals(err, SQLITE_ROW, + @"failed to step row for finalize test"); + + NSString *expectedLog = + @"-[GTMSQLiteStatement finalizeStatement] must be called " + @"when statement is no longer needed"; + + [GTMUnitTestDevLog expectString:expectedLog]; + + STAssertThrows([localPool drain], + @"Failing to clean up database did not throw assertion!"); + } +} + +- (void)testCompleteSQLString { + NSString *str = @"CREATE TABLE longlongTable (llc1 integer, llc2 integer);"; + BOOL isComplete = [GTMSQLiteStatement isCompleteStatement:str]; + STAssertTrue(isComplete, nil); + isComplete = [GTMSQLiteStatement isCompleteStatement:@""]; + STAssertTrue(isComplete, nil); + isComplete = [GTMSQLiteStatement isCompleteStatement:@"CR"]; + STAssertFalse(isComplete, nil); +} + +- (void)testQuotingSQLString { + NSString *str = @"This is wild! It's fun!"; + NSString *str2 = [GTMSQLiteStatement quoteAndEscapeString:str]; + STAssertEqualObjects(str2, @"'This is wild! It''s fun!'", nil); + str2 = [GTMSQLiteStatement quoteAndEscapeString:@""]; + STAssertEqualObjects(str2, @"''", nil); +} + +- (void)testVersion { + STAssertGreaterThan([GTMSQLiteDatabase sqliteVersionNumber], 0, nil); + STAssertNotNil([GTMSQLiteDatabase sqliteVersionString], nil); +} + +@end + +static void TestUpperLower16Impl(sqlite3_context *context, + int argc, sqlite3_value **argv) { + + customUpperFunctionCalled = YES; +} |