aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:35:12 +0000
committerGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:35:12 +0000
commitfde1ea051c0385638accd67362cd25f2fe1ae654 (patch)
treea43b02c31f73aa8a83e20abf619c02ec25030e32
parent93796b6367645ea20b01649c4e397a95370ed727 (diff)
adding SQLite helper/wrapper
-rw-r--r--Foundation/GTMSQLite.h713
-rw-r--r--Foundation/GTMSQLite.m2013
-rw-r--r--Foundation/GTMSQLiteTest.m1769
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;
+}