aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAbhi Beckert <abhi@abhibeckert.com>2017-08-04 13:20:12 +1000
committerAbhi Beckert <abhi@abhibeckert.com>2017-08-04 13:20:12 +1000
commitebf7d8b7db4144d304bf2224db19d787d631eda0 (patch)
tree5b1481d8ded07101891b3acce80b385a204f1ef8
parentff1db69283f69b8e9dc7fc373db242c37698c7c2 (diff)
parent1cbc8f7ca081a6538a2df484d89723cf441acb3c (diff)
downloadsequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.tar.gz
sequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.tar.bz2
sequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.zip
Merge remote-tracking branch 'sequelpro/master'
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/001-cpp-dependency.diff13
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/002-new-types.diff15
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql.h11
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_com.h3
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_version.h4
-rw-r--r--Frameworks/SPMySQLFramework/MySQL Client Libraries/lib/libmysqlclient.abin7917176 -> 7932592 bytes
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h14
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m14
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m38
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m9
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m87
-rwxr-xr-xFrameworks/SPMySQLFramework/build-mysql-client.sh10
-rw-r--r--Source/SPAppController.m2
-rw-r--r--Source/SPCopyTable.m37
-rw-r--r--Source/SPCustomQuery.h8
-rw-r--r--Source/SPCustomQuery.m12
-rw-r--r--Source/SPDataAdditions.h1
-rw-r--r--Source/SPDataAdditions.m134
-rw-r--r--Source/SPDataImport.m58
-rw-r--r--Source/SPDataStorage.m159
-rw-r--r--Source/SPDatabaseDocument.m6
-rw-r--r--Source/SPExportInitializer.m4
-rw-r--r--Source/SPExtendedTableInfo.m5
-rw-r--r--Source/SPFileHandle.h4
-rw-r--r--Source/SPFileHandle.m66
-rw-r--r--Source/SPIndexesController.m163
-rw-r--r--Source/SPProcessListController.m47
-rw-r--r--Source/SPTableContent.h1
-rw-r--r--Source/SPTableContent.m79
-rw-r--r--Source/SPTableContentDataSource.h2
-rw-r--r--Source/SPTableContentDataSource.m24
-rw-r--r--Source/SPTableContentDelegate.m45
-rw-r--r--Source/SPTableData.m205
-rw-r--r--Source/SPTablesList.m7
-rw-r--r--Source/SPWindowController.m7
-rw-r--r--Source/SPWindowControllerDelegate.m16
-rw-r--r--UnitTests/SPDataAdditionsTests.m76
-rw-r--r--readme.md7
38 files changed, 981 insertions, 412 deletions
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/001-cpp-dependency.diff b/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/001-cpp-dependency.diff
new file mode 100644
index 00000000..06c20001
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/001-cpp-dependency.diff
@@ -0,0 +1,13 @@
+--- mysql-5.5.56-dist/extra/yassl/taocrypt/include/runtime.hpp 2017-04-27 09:12:30.000000000 +0200
++++ mysql-5.5.56/extra/yassl/taocrypt/include/runtime.hpp 2017-05-20 23:27:14.000000000 +0200
+@@ -53,8 +53,8 @@
+ #endif
+
+ /* Disallow inline __cxa_pure_virtual() */
+-static int __cxa_pure_virtual() __attribute__((noinline, used));
+-static int __cxa_pure_virtual()
++int __cxa_pure_virtual() __attribute__((noinline, used));
++int __cxa_pure_virtual()
+ {
+ // oops, pure virtual called!
+ return 0;
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/002-new-types.diff b/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/002-new-types.diff
new file mode 100644
index 00000000..bb42f9d9
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/Patches/002-new-types.diff
@@ -0,0 +1,15 @@
+--- mysql-5.5.56-dist/include/mysql_com.h 2017-04-27 09:12:30.000000000 +0200
++++ mysql-5.5.56/include/mysql_com.h 2017-05-21 01:46:44.000000000 +0200
+@@ -349,7 +349,11 @@
+ MYSQL_TYPE_DATETIME, MYSQL_TYPE_YEAR,
+ MYSQL_TYPE_NEWDATE, MYSQL_TYPE_VARCHAR,
+ MYSQL_TYPE_BIT,
+- MYSQL_TYPE_NEWDECIMAL=246,
++ MYSQL_TYPE_TIMESTAMP2,
++ MYSQL_TYPE_DATETIME2,
++ MYSQL_TYPE_TIME2,
++ MYSQL_TYPE_JSON=245,
++ MYSQL_TYPE_NEWDECIMAL=246,
+ MYSQL_TYPE_ENUM=247,
+ MYSQL_TYPE_SET=248,
+ MYSQL_TYPE_TINY_BLOB=249,
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql.h b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql.h
index da29cb34..3a27ab41 100644
--- a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql.h
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql.h
@@ -1,4 +1,4 @@
-/* Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
+/* Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -167,7 +167,9 @@ enum mysql_option
MYSQL_OPT_GUESS_CONNECTION, MYSQL_SET_CLIENT_IP, MYSQL_SECURE_AUTH,
MYSQL_REPORT_DATA_TRUNCATION, MYSQL_OPT_RECONNECT,
MYSQL_OPT_SSL_VERIFY_SERVER_CERT, MYSQL_PLUGIN_DIR, MYSQL_DEFAULT_AUTH,
- MYSQL_ENABLE_CLEARTEXT_PLUGIN
+ MYSQL_ENABLE_CLEARTEXT_PLUGIN,
+ /* Set MYSQL_OPT_SSL_MODE to be the same as in 5.6 (ABI compatibility). */
+ MYSQL_OPT_SSL_MODE= 38
};
/**
@@ -224,6 +226,11 @@ enum mysql_protocol_type
MYSQL_PROTOCOL_PIPE, MYSQL_PROTOCOL_MEMORY
};
+enum mysql_ssl_mode
+{
+ SSL_MODE_REQUIRED= 3
+};
+
typedef struct character_set
{
unsigned int number; /* character set number */
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_com.h b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_com.h
index 8a8c019d..a3800c41 100644
--- a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_com.h
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_com.h
@@ -1,4 +1,4 @@
-/* Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.
+/* Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -26,6 +26,7 @@
#define USERNAME_CHAR_LENGTH 16
#define NAME_LEN (NAME_CHAR_LEN*SYSTEM_CHARSET_MBMAXLEN)
#define USERNAME_LENGTH (USERNAME_CHAR_LENGTH*SYSTEM_CHARSET_MBMAXLEN)
+#define CONNECT_STRING_MAXLEN 1024
#define MYSQL_AUTODETECT_CHARSET_NAME "auto"
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_version.h b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_version.h
index 7a8578ad..8c18116a 100644
--- a/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_version.h
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/include/mysql_version.h
@@ -11,11 +11,11 @@
#include <custom_conf.h>
#else
#define PROTOCOL_VERSION 10
-#define MYSQL_SERVER_VERSION "5.5.42"
+#define MYSQL_SERVER_VERSION "5.5.56"
#define MYSQL_BASE_VERSION "mysqld-5.5"
#define MYSQL_SERVER_SUFFIX_DEF ""
#define FRM_VER 6
-#define MYSQL_VERSION_ID 50542
+#define MYSQL_VERSION_ID 50556
#define MYSQL_PORT 3306
#define MYSQL_PORT_DEFAULT 0
#define MYSQL_UNIX_ADDR "/tmp/mysql.sock"
diff --git a/Frameworks/SPMySQLFramework/MySQL Client Libraries/lib/libmysqlclient.a b/Frameworks/SPMySQLFramework/MySQL Client Libraries/lib/libmysqlclient.a
index aa0d6109..0fccae22 100644
--- a/Frameworks/SPMySQLFramework/MySQL Client Libraries/lib/libmysqlclient.a
+++ b/Frameworks/SPMySQLFramework/MySQL Client Libraries/lib/libmysqlclient.a
Binary files differ
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
index 6f7b1a9a..77b70bf9 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.h
@@ -34,6 +34,7 @@
@interface SPMySQLConnection (Conversion)
+ (const char *)_cStringForString:(NSString *)aString usingEncoding:(NSStringEncoding)anEncoding returningLengthAs:(NSUInteger *)cStringLengthPointer;
++ (NSString *)_stringForCString:(const char *)cString usingEncoding:(NSStringEncoding)encoding;
- (const char *)_cStringForString:(NSString *)aString;
- (NSString *)_stringForCString:(const char *)cString;
@@ -56,3 +57,16 @@ static inline const char* _cStringForStringWithEncoding(NSString* aString, NSStr
return (const char *)(*cachedMethodPointer)(cachedClass, cachedSelector, aString, anEncoding, cStringLengthPointer);
}
+
+/**
+ * Converts a C string (NUL-terminated) to an NSString using the supplied encoding.
+ *
+ * Unlike +[NSString stringWithCString:encoding:] which will crash on a NULL pointer, this method will return nil instead.
+ */
+static inline NSString * _stringForCStringWithEncoding(const char *aString, NSStringEncoding inputEncoding)
+{
+ //This implementation is smaller than the cached selector voodoo above, so let's do it inline
+
+ //NSString will crash on NULL ptr
+ return (aString == NULL)? nil : [NSString stringWithCString:aString encoding:inputEncoding];
+}
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
index 676684ca..a7d293ea 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Conversion.m
@@ -79,7 +79,7 @@
}
/**
- * Converts a C string to an NSString using the supplied encoding.
+ * Converts a C string to an NSString using the current connection encoding.
* This method *will not* correctly preserve nul characters within c strings; instead
* the first nul character within the string will be treated as the line ending. This
* is unavoidable without supplying a string length, so this method should not be widely
@@ -87,11 +87,15 @@
*/
- (NSString *)_stringForCString:(const char *)cString
{
+ return _stringForCStringWithEncoding(cString, stringEncoding);
+}
- // Don't try and convert null strings
- if (cString == NULL) return nil;
-
- return [NSString stringWithCString:cString encoding:stringEncoding];
+/**
+ * @see _stringForCStringWithEncoding()
+ */
++ (NSString *)_stringForCString:(const char *)cString usingEncoding:(NSStringEncoding)encoding
+{
+ return _stringForCStringWithEncoding(cString, encoding);
}
@end
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
index d46b5552..65a7ae02 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Ping & KeepAlive.m
@@ -85,9 +85,9 @@
if(keepAliveThread) {
NSLog(@"warning: overwriting existing keepAliveThread: %@, results may be unpredictable!",keepAliveThread);
}
+ keepAliveThread = [NSThread currentThread];
}
- keepAliveThread = [NSThread currentThread];
[keepAliveThread setName:[NSString stringWithFormat:@"SPMySQL connection keepalive monitor thread (id=%p)", self]];
// If the maximum number of ping failures has been reached, determine whether to reconnect.
@@ -159,11 +159,13 @@ end_cleanup:
if (timeout > 0) pingTimeout = timeout;
// Set up a struct containing details the ping task will need
- SPMySQLConnectionPingDetails *pingDetails = malloc(sizeof(SPMySQLConnectionPingDetails));
- pingDetails->mySQLConnection = mySQLConnection;
- pingDetails->keepAliveLastPingSuccessPointer = &keepAliveLastPingSuccess;
- pingDetails->keepAlivePingThreadActivePointer = &keepAlivePingThreadActive;
- pingDetails->parentId = self;
+ // we can do this on the stack since this method makes sure to outlive the ping thread
+ SPMySQLConnectionPingDetails pingDetails = {
+ .mySQLConnection = mySQLConnection,
+ .keepAliveLastPingSuccessPointer = &keepAliveLastPingSuccess,
+ .keepAlivePingThreadActivePointer = &keepAlivePingThreadActive,
+ .parentId = self
+ };
// Create a pthread for the ping
pthread_t keepAlivePingThread_t;
@@ -171,7 +173,7 @@ end_cleanup:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
- pthread_create(&keepAlivePingThread_t, &attr, (void *)&_backgroundPingTask, pingDetails);
+ pthread_create(&keepAlivePingThread_t, &attr, (void *)&_backgroundPingTask, &pingDetails);
// Record the ping start time
pingStartTime_t = mach_absolute_time();
@@ -200,13 +202,12 @@ end_cleanup:
}
} while (keepAlivePingThreadActive);
- //wait for thread to go away, otherwise our free() below might run before _pingThreadCleanup()
+ //wait for thread to go away, otherwise pingDetails may go away before _pingThreadCleanup() finishes
pthread_join(keepAlivePingThread_t, NULL);
// Clean up
keepAlivePingThread_t = NULL;
pthread_attr_destroy(&attr);
- free(pingDetails);
// Unlock the connection
[self _unlockConnection];
@@ -282,7 +283,24 @@ void _pingThreadCleanup(void *pingDetails)
if (keepAliveThread) {
// Mark the thread as cancelled
- [keepAliveThread cancel];
+ @synchronized(self) {
+ // the synchronized is neccesary here, because we don't retain keepAliveThread.
+ // If it were ommitted, for example this could happen:
+ //
+ // this thread keepalive thread
+ // -------------- -----------------
+ // 1 fetch value of keepAliveThread to register
+ // 2 keepAliveThread = nil
+ // 3 [[NSThread currentThread] release]
+ // 4 objc_msgSend() <-- invalid memory accessed
+ //
+ // With synchronized we are guaranteed to either message nil or block the keepAliveThread from exiting
+ // (and thus releasing the NSThread object) until this call finishes.
+ //
+ // We can omit it in the other 2 cases, since keepAliveThread is already volatile and we are only
+ // checking for NULL, not dereferencing it.
+ [keepAliveThread cancel];
+ }
// Wait inside a time limit of ten seconds for it to exit
uint64_t threadCancelStartTime_t = mach_absolute_time();
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
index 594756be..ef98a21c 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Querying & Preparation.m
@@ -323,7 +323,8 @@
// Store the error state
theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
theErrorID = mysql_errno(mySQLConnection);
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
// Prevent retries if the query was cancelled or not a connection error
if (lastQueryWasCancelled || ![SPMySQLConnection isErrorIDConnectionError:theErrorID]) {
@@ -382,7 +383,8 @@
// Update the error message, if appropriate, to reflect result store errors or overall success
theErrorMessage = [self _stringForCString:mysql_error(mySQLConnection)];
theErrorID = mysql_errno(mySQLConnection);
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
} else {
theResult = [[SPMySQLEmptyResult alloc] init];
}
@@ -735,7 +737,8 @@
{
// If a SQLSTATE wasn't supplied, select one from the connection
if(!theSqlstate) {
- theSqlstate = [self _stringForCString:mysql_sqlstate(mySQLConnection)];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ theSqlstate = _stringForCStringWithEncoding(mysql_sqlstate(mySQLConnection), NSISOLatin1StringEncoding);
}
// Clear the last SQLSTATE stored on the instance
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
index e8692895..f581c03c 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m
@@ -606,6 +606,7 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
mysql_options(theConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&timeout);
// Set the connection encoding
+ NSStringEncoding connectEncodingNS = [SPMySQLConnection stringEncodingForMySQLCharset:[encodingName UTF8String]];
mysql_options(theConnection, MYSQL_SET_CHARSET_NAME, [encodingName UTF8String]);
// Set up the connection variables in the format MySQL needs, from the class-wide variables
@@ -614,22 +615,36 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
const char *thePassword = NULL;
const char *theSocket = NULL;
- if (host) theHost = [self _cStringForString:host];
- if (username) theUsername = [self _cStringForString:username];
+ if (host) theHost = [host UTF8String]; //mysql calls getaddrinfo on the hostname. Apples code uses -UTF8String in that situation.
+ if (username) theUsername = _cStringForStringWithEncoding(username, connectEncodingNS, NULL); //during connect this is in MYSQL_SET_CHARSET_NAME encoding
- // If a password was supplied, use it; otherwise ask the delegate if appropriate
+ // If a password was supplied, use it; otherwise ask the delegate if appropriate.
+ //
+ // Note that password has no charset in mysql: If a user password is set to 'ü' on a latin1 connection
+ // and you later try to connect on an UTF-8 terminal (or vice versa) it will fail. The MySQL (5.5) manual wrongly states that
+ // MYSQL_SET_CHARSET_NAME has influence over that, but it does not and could not, since the password is hashed by the client
+ // before transmitting it to the server and the (5.5) client has no charset support, effectively treating password as
+ // a NUL-terminated byte array.
+ // There is one exception, though: The "mysql_clear_password" auth plugin sends the password in plaintext and the server side
+ // MAY choose to do a charset conversion as appropriate before handing it to whatever backend is used.
+ // Since we don't know which auth plugin server and client will agree upon, we'll do as the manual says...
if (password) {
- thePassword = [self _cStringForString:password];
+ thePassword = _cStringForStringWithEncoding(password, connectEncodingNS, NULL);
} else if ([delegate respondsToSelector:@selector(keychainPasswordForConnection:)]) {
- thePassword = [self _cStringForString:[delegate keychainPasswordForConnection:self]];
+ thePassword = _cStringForStringWithEncoding([delegate keychainPasswordForConnection:self], connectEncodingNS, NULL);
}
// If set to use a socket and a socket was supplied, use it; otherwise, search for a socket to use
if (useSocket) {
- if ([socketPath length]) {
- theSocket = [self _cStringForString:socketPath];
- } else {
- theSocket = [self _cStringForString:[SPMySQLConnection findSocketPath]];
+ //default to user supplied path
+ NSString *mySocketPath = socketPath;
+ //if none was given, search in the default locations instead
+ if (![mySocketPath length]) {
+ mySocketPath = [SPMySQLConnection findSocketPath];
+ }
+ //get C string if we have a path (danger: method will throw on empty/nil string!)
+ if([mySocketPath length]) {
+ theSocket = [mySocketPath fileSystemRepresentation];
}
}
@@ -640,20 +655,39 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
const char *theCACertificatePath = NULL;
const char *theSSLCiphers = SPMySQLSSLPermissibleCiphers;
- if (sslKeyFilePath) {
- theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslKeyFilePath length]) {
+ theSSLKeyFilePath = [[sslKeyFilePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
- if (sslCertificatePath) {
- theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslCertificatePath length]) {
+ theSSLCertificatePath = [[sslCertificatePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
- if (sslCACertificatePath) {
- theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] UTF8String];
+ if ([sslCACertificatePath length]) {
+ theCACertificatePath = [[sslCACertificatePath stringByExpandingTildeInPath] fileSystemRepresentation];
}
if(sslCipherList) {
theSSLCiphers = [sslCipherList UTF8String];
}
+ // Calling mysql_ssl_set() to libmysqlclient only means that connecting with SSL would be nice.
+ // If the server doesn't support SSL though, it will *silently* fall back to plaintext and in the worst case even transmit
+ // the password in cleartext.
+ //
+ // Setting MYSQL_OPT_SSL_MODE is required, to actually make it abort the connection if the server doesn't signal SSL support.
+ //
+ // mysql 5.5.55+
+ // mysql 5.6.36+
+ // mysql 5.7.11+ (5.7.3 - 5.7.10 with a different name)
+ // mysql 8.0+
mysql_ssl_set(theConnection, theSSLKeyFilePath, theSSLCertificatePath, theCACertificatePath, NULL, theSSLCiphers);
+ enum mysql_ssl_mode opt_ssl_mode = SSL_MODE_REQUIRED;
+ if(mysql_options(theConnection, MYSQL_OPT_SSL_MODE, (void *)&opt_ssl_mode)) {
+ if(isMaster) {
+ [self _updateLastErrorMessage:@"libmysqlclient is missing support for MYSQL_OPT_SSL_MODE"];
+ [self _updateLastSqlstate:@"HY000"];
+ [self _updateLastErrorID:2026];
+ }
+ return NULL;
+ }
}
MYSQL *connectionStatus = mysql_real_connect(theConnection, theHost, theUsername, thePassword, NULL, (unsigned int)port, theSocket, [self clientFlags]);
@@ -663,9 +697,30 @@ static uint64_t _elapsedMicroSecondsSinceAbsoluteTime(uint64_t comparisonTime)
// If the connection is the master connection, record the error state
if (isMaster) {
+ // <TODO>
+ // this is tricky: mysql_error() is supposed to return data encoded in character_set_results (in mysql 5.5+),
+ // yet the whole API treats it as if it were a plain C string.
+ // So if the charset is e.g. utf16 the mysql server will itself fall over that and return an empty error message
+ // (5.5, 5.7: the message is really missing at the network layer).
+ // (Side Note: There is a workaround for server generated error messages: "show warnings" will also include errors
+ // and because it uses a regular results table it can contain the actual error message)
+ //
+ // Before 5.5 things are much worse, because the charset of the message depends on the language of the error messages
+ // (which can be changed at runtime per session (or at launch time in 4.1)) plus all arguments in the template string
+ // will retain their original encoding.
+ // So if you connect with utf8 to a server with russian locale the error message will be in koi8r and contain the name of
+ // an erroneus value in utf8...
+ //
+ // On the other hand mysql_error() may also return errors generated by the client locally.
+ // The client has no charset support and simply assumes the local charset is ASCII-compatible.
+ // The english messages are compiled into the client (see libmysql/errmsg.c and include/errmsg.h).
+ // We could use a little trick, though: client errors are in the exclusive range 2000 to 2999 (CR_MIN_ERROR/CR_MAX_ERROR)
+ // and all their string arguments are either hostnames or file system paths, which on OS X use UTF-8.
[self _updateLastErrorMessage:[self _stringForCString:mysql_error(theConnection)]];
+ // </TODO>
[self _updateLastErrorID:mysql_errno(theConnection)];
- [self _updateLastSqlstate:[self _stringForCString:mysql_sqlstate(theConnection)]];
+ // sqlstate is always an ASCII string, regardless of charset (but use latin1 anyway as that is less picky about invalid bytes)
+ [self _updateLastSqlstate:_stringForCStringWithEncoding(mysql_sqlstate(theConnection),NSISOLatin1StringEncoding)];
}
return NULL;
diff --git a/Frameworks/SPMySQLFramework/build-mysql-client.sh b/Frameworks/SPMySQLFramework/build-mysql-client.sh
index 2e7c4fc9..5475b0a0 100755
--- a/Frameworks/SPMySQLFramework/build-mysql-client.sh
+++ b/Frameworks/SPMySQLFramework/build-mysql-client.sh
@@ -156,9 +156,15 @@ then
exit 1
fi
+# For CMake 3.0+ use CMAKE_OSX_SYSROOT and CMAKE_OSX_DEPLOYMENT_TARGET to set SDK path and minimum version
+CONFIGURE_OPTIONS="${CONFIGURE_OPTIONS} -DCMAKE_OSX_SYSROOT='${SDK_PATH}' -DCMAKE_OSX_DEPLOYMENT_TARGET=${MIN_OS_X_VERSION}"
+
+# For CMake 2 add these parameters to the CFLAGS/CXXFLAGS:
+# -isysroot ${SDK_PATH} -mmacosx-version-min=${MIN_OS_X_VERSION}
+
# C/C++ compiler flags
-export CFLAGS="-isysroot ${SDK_PATH} ${ARCHITECTURES} -O3 -fno-omit-frame-pointer -fno-exceptions -mmacosx-version-min=${MIN_OS_X_VERSION}"
-export CXXFLAGS="-isysroot ${SDK_PATH} ${ARCHITECTURES} -O3 -fno-omit-frame-pointer -felide-constructors -fno-exceptions -fno-rtti -mmacosx-version-min=${MIN_OS_X_VERSION}"
+export CFLAGS="${ARCHITECTURES} -O3 -fno-omit-frame-pointer -fno-exceptions"
+export CXXFLAGS="${ARCHITECTURES} -O3 -fno-omit-frame-pointer -felide-constructors -fno-exceptions -fno-rtti"
echo "$ESC[1mConfiguring MySQL source...$ESC[0m"
diff --git a/Source/SPAppController.m b/Source/SPAppController.m
index 3028c0f6..afdfd16f 100644
--- a/Source/SPAppController.m
+++ b/Source/SPAppController.m
@@ -770,7 +770,7 @@
}
else {
NSBeep();
- NSLog(@"Error in sequelpro URL scheme");
+ NSLog(@"Error in sequelpro URL scheme for URL <%@>",url);
}
}
diff --git a/Source/SPCopyTable.m b/Source/SPCopyTable.m
index ed7b1d71..aa77ffbc 100644
--- a/Source/SPCopyTable.m
+++ b/Source/SPCopyTable.m
@@ -184,6 +184,7 @@ static const NSInteger kBlobAsImageFile = 4;
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -197,8 +198,12 @@ static const NSInteger kBlobAsImageFile = 4;
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendFormat:@"%@\t", displayString];
[displayString release];
@@ -315,6 +320,7 @@ static const NSInteger kBlobAsImageFile = 4;
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -328,8 +334,12 @@ static const NSInteger kBlobAsImageFile = 4;
[result appendFormat:@"\"%@\",", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendFormat:@"\"%@\",", displayString];
[displayString release];
@@ -620,6 +630,7 @@ static const NSInteger kBlobAsImageFile = 4;
Class nsDataClass = [NSData class];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
NSStringEncoding connectionEncoding = [mySQLConnection stringEncoding];
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -632,10 +643,15 @@ static const NSInteger kBlobAsImageFile = 4;
else if ([cellData isSPNotLoaded])
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:nsDataClass]) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:connectionEncoding];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:connectionEncoding];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendString:displayString];
+ [result appendString:@"\t"];
[displayString release];
}
}
@@ -749,7 +765,6 @@ static const NSInteger kBlobAsImageFile = 4;
- (NSUInteger)autodetectWidthForColumnDefinition:(NSDictionary *)columnDefinition maxRows:(NSUInteger)rowsToCheck
{
CGFloat columnBaseWidth;
- id contentString;
NSUInteger cellWidth, maxCellWidth, i;
NSRange linebreakRange;
double rowStep;
@@ -762,6 +777,7 @@ static const NSInteger kBlobAsImageFile = 4;
NSUInteger columnIndex = (NSUInteger)[[columnDefinition objectForKey:@"datacolumnindex"] integerValue];
NSDictionary *stringAttributes = @{NSFontAttributeName : tableFont};
Class spmysqlGeometryData = [SPMySQLGeometryData class];
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
// Check the number of rows available to check, sampling every n rows
if ([tableStorage count] < rowsToCheck)
@@ -779,7 +795,7 @@ static const NSInteger kBlobAsImageFile = 4;
for (i = 0; i < rowsToCheck; i += rowStep) {
// Retrieve part of the cell's content to get widths, topping out at a maximum length
- contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500);
+ id contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500);
// If the cell hasn't loaded yet, skip processing
if (!contentString)
@@ -801,7 +817,10 @@ static const NSInteger kBlobAsImageFile = 4;
// Otherwise, ensure the cell is represented as a short string
if ([contentString isKindOfClass:[NSData class]]) {
- contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
+ if (hexBlobs)
+ contentString = [[NSString alloc] initWithFormat:@"0x%@", [(NSData *)contentString dataToHexString]];
+ else
+ contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
} else if ([(NSString *)contentString length] > 500) {
contentString = [contentString substringToIndex:500];
}
diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h
index fe31be6d..7491b304 100644
--- a/Source/SPCustomQuery.h
+++ b/Source/SPCustomQuery.h
@@ -57,16 +57,14 @@
@class SPMySQLConnection;
@class SPMySQLStreamingResultStore;
@class SPTextView;
-
-#ifdef SP_CODA
@class SPDatabaseDocument;
@class SPTablesList;
-#endif
+
@interface SPCustomQuery : NSObject <NSTableViewDataSource, NSWindowDelegate, NSTableViewDelegate, SPDatabaseContentViewDelegate>
{
- IBOutlet id tableDocumentInstance;
- IBOutlet id tablesListInstance;
+ IBOutlet SPDatabaseDocument *tableDocumentInstance;
+ IBOutlet SPTablesList *tablesListInstance;
#ifndef SP_CODA
IBOutlet id queryFavoritesButton;
diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m
index 3cbd6a4d..c14c275d 100644
--- a/Source/SPCustomQuery.m
+++ b/Source/SPCustomQuery.m
@@ -70,6 +70,7 @@
- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview;
+ (NSString *)linkToHelpTopic:(NSString *)aTopic;
+- (void)documentWillClose:(NSNotification *)notification;
@end
@@ -3986,6 +3987,10 @@
selector:@selector(endDocumentTaskForTab:)
name:SPDocumentTaskEndNotification
object:tableDocumentInstance];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentWillClose:)
+ name:SPDocumentWillCloseNotification
+ object:tableDocumentInstance];
#ifndef SP_CODA
[prefs addObserver:self forKeyPath:SPGlobalResultTableFont options:NSKeyValueObservingOptionNew context:NULL];
@@ -4046,6 +4051,13 @@
return value;
}
+//this method is called right before the UI objects are deallocated
+- (void)documentWillClose:(NSNotification *)notification
+{
+ // if a result load is in progress we must stop the timer or it may try to call invalid IBOutlets
+ [self clearQueryLoadTimer];
+}
+
#pragma mark -
- (void)dealloc
diff --git a/Source/SPDataAdditions.h b/Source/SPDataAdditions.h
index cd8374f6..a446e315 100644
--- a/Source/SPDataAdditions.h
+++ b/Source/SPDataAdditions.h
@@ -43,6 +43,7 @@ typedef NS_OPTIONS(NSUInteger, SPLineTerminator) {
- (NSData *)dataEncryptedWithKey:(NSData *)aesKey IV:(NSData *)iv;
- (NSData *)dataDecryptedWithPassword:(NSString *)password;
- (NSData *)dataDecryptedWithKey:(NSData *)key;
++ (NSData *)dataWithHexString:(NSString *)hex;
- (NSData *)compress;
- (NSData *)decompress;
diff --git a/Source/SPDataAdditions.m b/Source/SPDataAdditions.m
index 53d18274..19539cd0 100644
--- a/Source/SPDataAdditions.m
+++ b/Source/SPDataAdditions.m
@@ -344,6 +344,140 @@ uint32_t LimitUInt32(NSUInteger i);
}
/**
+ * Returns the integer value for a single hex-encoded nibble or -1 for invalid values.
+ * Supported characters: 0-9,a-f,A-F
+ *
+ * Note: You usually would call this method like ((hexchar2nibble(highByte) << 4) + hexchar2nibble(lowByte)) to decode a single hex-encoded byte.
+ */
+static int hexchar2nibble(char c)
+{
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
+/**
+ * Decodes a sequence of hex digits to raw byte values.
+ * This function is very strict about the allowed inputs and must only be used for validated inputs!
+ *
+ * - If numRawBytes != 0 and inBuffer == NULL or outBuffer == NULL, this will crash
+ * - The hex sequence must ONLY contain chars 0-9,a-f,A-F or the result will be undefined
+ * - The sequence must be padded to have an even length. numRawBytes is the number of bytes AFTER decoding, so inBuffer must be exactly 2x as large
+ * - inBuffer and outBuffer may be the same pointer
+ */
+static void decodeValidHexSequence(const char *inBuffer,uint8_t *outBuffer, NSUInteger numRawBytes)
+{
+ NSUInteger outIndex = 0;
+ NSUInteger srcIndex = 0;
+ while (outIndex < numRawBytes) {
+ uint8_t v = (hexchar2nibble(inBuffer[srcIndex]) << 4) + hexchar2nibble(inBuffer[srcIndex+1]);
+ outBuffer[outIndex++] = v;
+ srcIndex += 2;
+ }
+}
+
+/**
+ * Interpret a string of hex digits in 'hex' as hex data, and return
+ * an NSData representation of the data. Spaces are permitted within
+ * the string and an initial '0x' will be ignored. If bad input
+ * is detected, nil is returned.
+ *
+ * Alternatively the MySQL-style X'val' syntax is also supported,
+ * with the same restrictions as in MySQL:
+ * - val must always be an even number of characters
+ * - val cannot contain whitespace (whitespace before/after is ok)
+ * - The leading x is case-INsensitive
+ */
++ (NSData *)dataWithHexString:(NSString *)hex
+{
+ if(!hex) return nil; // no string
+ const char *sourceBytes = [hex UTF8String];
+
+ size_t length = strlen(sourceBytes); // keep in mind that [hex length] is the number of Unicode characters, not the number of bytes
+ if (length < 1) return [NSData data]; // empty string
+
+ NSUInteger srcIndex = 0;
+ NSData *data = nil;
+ NSUInteger nbytes;
+
+ //skip leading whitespace (in order to properly check for leading "0x")
+ while(srcIndex < length && (sourceBytes[srcIndex] == ' ' || sourceBytes[srcIndex] == '\t')) srcIndex++;
+
+ // bypass initial 0x
+ if(srcIndex+1 < length && sourceBytes[srcIndex] == '0' && sourceBytes[srcIndex+1] == 'x' ) {
+ srcIndex += 2;
+ }
+ //check for mysql syntax
+ else if(srcIndex+2 < length && (sourceBytes[srcIndex] == 'x' || sourceBytes[srcIndex] == 'X') && sourceBytes[srcIndex+1] == '\'') {
+ srcIndex += 2;
+ //look for the terminating quote
+ NSUInteger startIndex = srcIndex;
+ NSUInteger endIndex = startIndex; //startIndex points to the first character inside the quotes, which may already be the terminating quote
+ while(endIndex < length) {
+ char c = sourceBytes[endIndex];
+ //if we've hit the terminator, verify that only whitespace follows and stop reading
+ if(c == '\'') {
+ NSUInteger afterIndex = endIndex+1;
+ while (afterIndex < length) {
+ c = sourceBytes[afterIndex++];
+ if(c != ' ' && c != '\t') return nil;
+ }
+ break;
+ }
+ endIndex++;
+ // Check for non-hex characters
+ if (hexchar2nibble(c) < 0) return nil;
+ }
+ // Check for unterminated sequence and uneven number of bytes
+ NSUInteger n = endIndex - startIndex;
+ if(endIndex == length || ((n % 2) != 0)) return nil;
+ // shortcut
+ if(n == 0) return [NSData data];
+ //looks good, create the output buffer and decode
+ nbytes = n / 2;
+ unsigned char *outBuf = malloc(nbytes);
+ decodeValidHexSequence(&sourceBytes[startIndex], outBuf, nbytes);
+ return [NSData dataWithBytesNoCopy:outBuf length:nbytes freeWhenDone:YES];
+ }
+
+ // Copy input while removing spaces and tabs.
+ char *trimmedFull = (char *)malloc(length + 1);
+ char *trimmed = (trimmedFull + 1); //we'll use the first byte in case we have to fill in a leading '0'
+ NSUInteger trimIndex = 0;
+ NSUInteger n = 0; // n = # of hex digits
+ while(srcIndex < length) {
+ char c = sourceBytes[srcIndex++];
+ if(c == ' ' || c == '\t') continue;
+ trimmed[trimIndex++] = c;
+ if(!c) break;
+ n++;
+ // Check for non-hex characters
+ if (hexchar2nibble(c) < 0) goto fail_cleanup;
+ }
+ //shortcut
+ if(n == 0) {
+ data = [NSData data];
+ goto fail_cleanup;
+ }
+
+ BOOL isEven = ((n % 2) == 0);
+ nbytes = !isEven ? (n + 1) / 2 : n / 2; //adjust for cases where "0aff" is written as "aff" (e.g.)
+ if(!isEven) {
+ trimmed--;
+ trimmed[0] = '0';
+ }
+
+ //we'll just decode the data in-place since the raw values have to be shorter by definition, anyway
+ decodeValidHexSequence(trimmed, (uint8_t *)trimmedFull, nbytes);
+ return [NSData dataWithBytesNoCopy:trimmedFull length:nbytes freeWhenDone:YES];
+
+fail_cleanup:
+ free(trimmedFull);
+ return data;
+}
+
+/**
* Returns the hex representation of the given data.
*/
- (NSString *)dataToFormattedHexString
diff --git a/Source/SPDataImport.m b/Source/SPDataImport.m
index 058ff916..4c680801 100644
--- a/Source/SPDataImport.m
+++ b/Source/SPDataImport.m
@@ -162,6 +162,7 @@
{
SPMainQSync(^{
[NSApp endSheet:singleProgressSheet];
+ [singleProgressBar setIndeterminate:YES];
[singleProgressSheet orderOut:nil];
[singleProgressBar stopAnimation:self];
[singleProgressBar setMaxValue:100];
@@ -397,16 +398,12 @@
fileTotalLength = (NSUInteger)[[[[NSFileManager defaultManager] attributesOfItemAtPath:filename error:NULL] objectForKey:NSFileSize] longLongValue];
if (!fileTotalLength) fileTotalLength = 1;
- // If importing a bzipped file, use indeterminate progress bars as no progress is available
- BOOL useIndeterminate = NO;
- if ([sqlFileHandle compressionFormat] == SPBzip2Compression) useIndeterminate = YES;
-
SPMainQSync(^{
// Reset progress interface
[errorsView setString:@""];
[singleProgressTitle setStringValue:NSLocalizedString(@"Importing SQL", @"text showing that the application is importing SQL")];
[singleProgressText setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
- [singleProgressBar setIndeterminate:useIndeterminate];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar setMaxValue:fileTotalLength];
[singleProgressBar setUsesThreadedAnimation:YES];
[singleProgressBar startAnimation:self];
@@ -787,16 +784,12 @@
if (!fileTotalLength) fileTotalLength = 1;
fileIsCompressed = ([csvFileHandle compressionFormat] != SPNoCompression);
- // If importing a bzipped file, use indeterminate progress bars as no progress is available
- BOOL useIndeterminate = NO;
- if ([csvFileHandle compressionFormat] == SPBzip2Compression) useIndeterminate = YES;
-
// Reset progress interface
SPMainQSync(^{
[errorsView setString:@""];
[singleProgressTitle setStringValue:NSLocalizedString(@"Importing CSV", @"text showing that the application is importing CSV")];
[singleProgressText setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
- [singleProgressBar setIndeterminate:YES];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar setUsesThreadedAnimation:YES];
[singleProgressBar startAnimation:self];
@@ -971,8 +964,8 @@
// Reset progress interface and open the progress sheet
SPMainQSync(^{
- [singleProgressBar setIndeterminate:useIndeterminate];
[singleProgressBar setMaxValue:fileTotalLength];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar startAnimation:self];
[NSApp beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil];
[singleProgressSheet makeKeyWindow];
@@ -1213,29 +1206,30 @@
document:tableDocumentInstance
notificationName:@"Import Finished"];
+ SPMainQSync(^{
+ if(importIntoNewTable) {
- if(importIntoNewTable) {
-
- // Select the new table
-
- // Update current database tables
- [[tablesListInstance onMainThread] updateTables:self];
-
- // Re-query the structure of all databases in the background
- [[tableDocumentInstance databaseStructureRetrieval] queryDbStructureInBackgroundWithUserInfo:@{@"forceUpdate" : @YES}];
-
- // Select the new table
- [tablesListInstance selectItemWithName:selectedTableTarget];
-
- } else {
-
- // If import was done into a new table or the table selected for import is also selected in the content view,
- // update the content view - on the main thread to avoid crashes.
- if ([tablesListInstance tableName] && [selectedTableTarget isEqualToString:[tablesListInstance tableName]]) {
- [tableDocumentInstance setContentRequiresReload:YES];
+ // Select the new table
+
+ // Update current database tables
+ [tablesListInstance updateTables:self];
+
+ // Re-query the structure of all databases in the background
+ [[tableDocumentInstance databaseStructureRetrieval] queryDbStructureInBackgroundWithUserInfo:@{@"forceUpdate" : @YES}];
+
+ // Select the new table
+ [tablesListInstance selectItemWithName:selectedTableTarget];
+
+ } else {
+
+ // If import was done into a new table or the table selected for import is also selected in the content view,
+ // update the content view - on the main thread to avoid crashes.
+ if ([tablesListInstance tableName] && [selectedTableTarget isEqualToString:[tablesListInstance tableName]]) {
+ [tableDocumentInstance setContentRequiresReload:YES];
+ }
+
}
-
- }
+ });
}
diff --git a/Source/SPDataStorage.m b/Source/SPDataStorage.m
index 44c2dc9d..5db56b1e 100644
--- a/Source/SPDataStorage.m
+++ b/Source/SPDataStorage.m
@@ -37,6 +37,7 @@
@interface SPDataStorage (Private_API)
- (void) _checkNewRow:(NSMutableArray *)aRow;
+- (void) _addRowUnsafeUnchecked:(NSMutableArray *)aRow;
@end
@@ -58,33 +59,38 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) setDataStorage:(SPMySQLStreamingResultStore *)newDataStorage updatingExisting:(BOOL)updateExistingStore
{
- SPMySQLStreamingResultStore *oldDataStorage = dataStorage;
+ BOOL *oldUnloadedColumns;
+ NSPointerArray *oldEditedRows;
+ SPMySQLStreamingResultStore *oldDataStorage;
+
+ @synchronized(self) {
+ oldDataStorage = dataStorage;
- if (oldDataStorage) {
- // If the table is reloading data, link to the current data store for smoother loads
- if (updateExistingStore) {
- [newDataStorage replaceExistingResultStore:oldDataStorage];
+ if (oldDataStorage) {
+ // If the table is reloading data, link to the current data store for smoother loads
+ if (updateExistingStore) {
+ [newDataStorage replaceExistingResultStore:oldDataStorage];
+ }
}
- }
- [newDataStorage retain];
+ [newDataStorage retain];
- NSPointerArray *newEditedRows = [[NSPointerArray alloc] init];
- NSUInteger newNumberOfColumns = [newDataStorage numberOfFields];
- BOOL *newUnloadedColumns = calloc(newNumberOfColumns, sizeof(BOOL));
- for (NSUInteger i = 0; i < newNumberOfColumns; i++) {
- newUnloadedColumns[i] = NO;
- }
-
- BOOL *oldUnloadedColumns = unloadedColumns;
- NSPointerArray *oldEditedRows = editedRows;
- @synchronized(self) {
+ NSPointerArray *newEditedRows = [[NSPointerArray alloc] init];
+ NSUInteger newNumberOfColumns = [newDataStorage numberOfFields];
+ BOOL *newUnloadedColumns = calloc(newNumberOfColumns, sizeof(BOOL));
+ for (NSUInteger i = 0; i < newNumberOfColumns; i++) {
+ newUnloadedColumns[i] = NO;
+ }
+
+ oldUnloadedColumns = unloadedColumns;
+ oldEditedRows = editedRows;
dataStorage = newDataStorage;
numberOfColumns = newNumberOfColumns;
unloadedColumns = newUnloadedColumns;
editedRowCount = 0;
editedRows = newEditedRows;
}
+
free(oldUnloadedColumns);
[oldEditedRows release];
[oldDataStorage release];
@@ -107,6 +113,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
/**
* Return a mutable array containing the data for a specified row.
+ * The returned array will be a shallow copy of the internal row object.
*/
- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex
{
@@ -117,12 +124,12 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, anIndex);
if (editedRow != NULL) {
- return editedRow;
+ return [NSMutableArray arrayWithArray:editedRow]; //make a copy to not give away control of our internal state
}
}
// Otherwise, prepare to return the underlying storage row
- NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex);
+ NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex); //returned array is already a copy
// Modify unloaded cells as appropriate
for (NSUInteger i = 0; i < numberOfColumns; i++) {
@@ -252,11 +259,14 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
// If an edited row exists for the supplied index, use that; otherwise use the underlying
// storage row
if (state->state < editedRowCount) {
- targetRow = SPDataStorageGetEditedRow(editedRows, state->state);
+ NSMutableArray *internalRow = SPDataStorageGetEditedRow(editedRows, state->state);
+ if(internalRow != NULL) {
+ targetRow = [NSMutableArray arrayWithArray:internalRow]; //make a copy to not give away control of our internal state
+ }
}
if (targetRow == nil) {
- targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state);
+ targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state); //returned array is already a copy
// Modify unloaded cells as appropriate
for (NSUInteger i = 0; i < numberOfColumns; i++) {
@@ -287,16 +297,18 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) addRowWithContents:(NSMutableArray *)aRow
{
- @synchronized(self) {
- // Verify the row is of the correct length
- [self _checkNewRow:aRow];
-
- // Add the new row to the editable store
- [editedRows addPointer:aRow];
- editedRowCount++;
-
- // Update the underlying store as well to keep counts correct
- [dataStorage addDummyRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ // Verify the row is of the correct length
+ [self _checkNewRow:newArray];
+ [self _addRowUnsafeUnchecked:newArray];
+ }
+ }
+ @finally {
+ [newArray release];
}
}
@@ -307,39 +319,58 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex
{
- @synchronized(self) {
- unsigned long long numberOfRows = SPMySQLResultStoreGetRowCount(dataStorage);
-
- // Verify the row is of the correct length
- [self _checkNewRow:aRow];
-
- // Throw an exception if the index is out of bounds
- if (anIndex > numberOfRows) {
- [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, numberOfRows];
- }
-
- // If "inserting" at the end of the array just add a row
- if (anIndex == numberOfRows) {
- return [self addRowWithContents:aRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ unsigned long long numberOfRows = SPMySQLResultStoreGetRowCount(dataStorage);
+
+ // Verify the row is of the correct length
+ [self _checkNewRow:newArray];
+
+ // Throw an exception if the index is out of bounds
+ if (anIndex > numberOfRows) {
+ [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, numberOfRows];
+ }
+
+ // If "inserting" at the end of the array just add a row
+ if (anIndex == numberOfRows) {
+ [self _addRowUnsafeUnchecked:newArray];
+ return;
+ }
+
+ // Add the new row to the editable store
+ [editedRows insertPointer:newArray atIndex:anIndex];
+ editedRowCount++;
+
+ // Update the underlying store to keep counts and indices correct
+ [dataStorage insertDummyRowAtIndex:anIndex];
}
-
- // Add the new row to the editable store
- [editedRows insertPointer:aRow atIndex:anIndex];
- editedRowCount++;
-
- // Update the underlying store to keep counts and indices correct
- [dataStorage insertDummyRowAtIndex:anIndex];
+ }
+ @finally {
+ [newArray release];
}
}
/**
* Replace a row with contents of the supplied NSArray.
+ *
+ * Note that the supplied objects within the array are retained as a reference rather than copied.
*/
- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow
{
- @synchronized(self) {
- [self _checkNewRow:aRow];
- [editedRows replacePointerAtIndex:anIndex withPointer:aRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ [self _checkNewRow:newArray];
+ [editedRows replacePointerAtIndex:anIndex withPointer:newArray];
+ }
+ }
+ @finally {
+ [newArray release];
}
}
@@ -357,7 +388,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
// Make sure that the row in question is editable
if (editableRow == nil) {
- editableRow = [self rowContentsAtIndex:rowIndex];
+ editableRow = [self rowContentsAtIndex:rowIndex]; //already returns a copy, so we don't have to go via -replaceRowAtIndex:withRowContents:
[editedRows replacePointerAtIndex:rowIndex withPointer:editableRow];
}
}
@@ -483,6 +514,10 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore
{
@synchronized(self) {
+ if(resultStore != dataStorage) {
+ NSLog(@"%s: received delegate callback from an unknown result store %p (expected: %p). Ignored!", __PRETTY_FUNCTION__, resultStore, dataStorage);
+ return;
+ }
[editedRows setCount:(NSUInteger)[resultStore numberOfRows]];
editedRowCount = [editedRows count];
}
@@ -536,4 +571,16 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
}
}
+// DO NOT CALL THIS METHOD UNLESS YOU CURRENTLY HAVE A LOCK ON SELF!!!
+// DO NOT CALL THIS METHOD UNLESS YOU HAVE CALLED _checkNewRow: FIRST!
+- (void)_addRowUnsafeUnchecked:(NSMutableArray *)aRow
+{
+ // Add the new row to the editable store
+ [editedRows addPointer:aRow];
+ editedRowCount++;
+
+ // Update the underlying store as well to keep counts correct
+ [dataStorage addDummyRow];
+}
+
@end
diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m
index d19b3f4c..2581967c 100644
--- a/Source/SPDatabaseDocument.m
+++ b/Source/SPDatabaseDocument.m
@@ -6503,6 +6503,9 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
- (void)dealloc
{
NSAssert([NSThread isMainThread], @"Calling %s from a background thread is not supported!", __func__);
+
+ // Tell listeners that this database document is being closed - fixes retain cycles and allows cleanup
+ [[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentWillCloseNotification object:self];
// Unregister observers
[self _removePreferenceObservers];
@@ -6517,9 +6520,6 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
for (id retainedObject in nibObjectsToRelease) [retainedObject release];
SPClear(nibObjectsToRelease);
-
- // Tell listeners that this database document is being closed - fixes retain cycles and allows cleanup
- [[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentWillCloseNotification object:self];
SPClear(databaseStructureRetrieval);
diff --git a/Source/SPExportInitializer.m b/Source/SPExportInitializer.m
index 3238ea5c..9269cac8 100644
--- a/Source/SPExportInitializer.m
+++ b/Source/SPExportInitializer.m
@@ -520,7 +520,7 @@
BOOL tableNameInTokens = NO;
NSArray *representedObjects = [exportCustomFilenameTokenField objectValue];
for (id representedObject in representedObjects) {
- if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES;
+ if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:SPFileNameTableTokenName]) tableNameInTokens = YES;
}
[exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])];
}
@@ -582,7 +582,7 @@
BOOL tableNameInTokens = NO;
NSArray *representedObjects = [exportCustomFilenameTokenField objectValue];
for (id representedObject in representedObjects) {
- if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES;
+ if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:SPFileNameTableTokenName]) tableNameInTokens = YES;
}
[exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])];
}
diff --git a/Source/SPExtendedTableInfo.m b/Source/SPExtendedTableInfo.m
index df084bf3..dc311bdf 100644
--- a/Source/SPExtendedTableInfo.m
+++ b/Source/SPExtendedTableInfo.m
@@ -404,7 +404,8 @@ static NSString *SPMySQLCommentField = @"Comment";
[tableSizeFree setStringValue:[self _formatValueWithKey:SPMySQLDataFreeField inDictionary:statusFields]];
// Set comments
- NSString *commentText = [statusFields objectForKey:SPMySQLCommentField];
+ // Note: On MySQL the comment column is marked as NOT NULL, but we still received crash reports because it was NULL!? (#2791)
+ NSString *commentText = [[statusFields objectForKey:SPMySQLCommentField] unboxNull];
if (!commentText) commentText = @"";
@@ -541,7 +542,7 @@ static NSString *SPMySQLCommentField = @"Comment";
if ((object == tableCommentsTextView) && ([object isEditable]) && ([selectedTable length] > 0)) {
- NSString *currentComment = [[tableDataInstance statusValueForKey:@"Comment"] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSString *currentComment = [[[tableDataInstance statusValueForKey:SPMySQLCommentField] unboxNull] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *newComment = [[tableCommentsTextView string] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Check that the user actually changed the tables comment
diff --git a/Source/SPFileHandle.h b/Source/SPFileHandle.h
index 5622b1b6..2b605f59 100644
--- a/Source/SPFileHandle.h
+++ b/Source/SPFileHandle.h
@@ -28,7 +28,7 @@
//
// More info at <https://github.com/sequelpro/sequelpro>
-union SPSomeFileHandle;
+struct SPRawFileHandles;
/**
* @class SPFileHandle SPFileHandle.h
*
@@ -40,7 +40,7 @@ union SPSomeFileHandle;
*/
@interface SPFileHandle : NSObject
{
- union SPSomeFileHandle *wrappedFile;
+ struct SPRawFileHandles *wrappedFile;
char *wrappedFilePath;
NSMutableData *buffer;
diff --git a/Source/SPFileHandle.m b/Source/SPFileHandle.m
index 7da3d100..af6f413f 100644
--- a/Source/SPFileHandle.m
+++ b/Source/SPFileHandle.m
@@ -37,7 +37,7 @@
// waits until some has been written out. This can affect speed and memory usage.
#define SPFH_MAX_WRITE_BUFFER_SIZE 1048576
-union SPSomeFileHandle {
+struct SPRawFileHandles {
FILE *file;
BZFILE *bzfile;
gzFile *gzfile;
@@ -46,6 +46,7 @@ union SPSomeFileHandle {
@interface SPFileHandle ()
- (void)_writeBufferToData;
+- (void)_closeFileHandles;
@end
@@ -132,11 +133,11 @@ union SPSomeFileHandle {
if (isBzip2) {
compressionFormat = SPBzip2Compression;
- wrappedFile->bzfile = BZ2_bzopen(path, "rb");
+ wrappedFile->bzfile = BZ2_bzReadOpen(NULL, theFile, 0, 0, NULL, 0);
}
}
- // Default to plain
- if(compressionFormat == SPNoCompression) {
+ // We need to save the file handle both in plain and BZ2 format
+ if(compressionFormat == SPNoCompression || compressionFormat == SPBzip2Compression) {
wrappedFile->file = theFile;
}
else {
@@ -231,7 +232,7 @@ union SPSomeFileHandle {
}
/**
- * Returns the on-disk (raw/uncompressed) length of data read so far.
+ * Returns the on-disk (raw/compressed) length of data read so far.
* This includes any compression headers within the data, and can be used
* for progress bars when processing files.
*/
@@ -243,7 +244,7 @@ union SPSomeFileHandle {
return gzoffset(wrappedFile->gzfile);
}
else if(compressionFormat == SPBzip2Compression) {
- return 0;
+ return ftell(wrappedFile->file);
}
else {
return ftell(wrappedFile->file);
@@ -263,15 +264,7 @@ union SPSomeFileHandle {
if (compressionFormat == useCompressionFormat) return;
// Regardless of the supplied argument, close the current file according to how it was previously opened
- if (compressionFormat == SPGzipCompression) {
- gzclose(wrappedFile->gzfile);
- }
- else if (compressionFormat == SPBzip2Compression) {
- BZ2_bzclose(wrappedFile->bzfile);
- }
- else {
- fclose(wrappedFile->file);
- }
+ [self _closeFileHandles];
if (dataWritten) [NSException raise:NSInternalInconsistencyException format:@"Cannot change compression settings when data has already been written."];
@@ -282,7 +275,8 @@ union SPSomeFileHandle {
gzbuffer(wrappedFile->gzfile, 131072);
}
else if (compressionFormat == SPBzip2Compression) {
- wrappedFile->bzfile = BZ2_bzopen(wrappedFilePath, "wb");
+ wrappedFile->file = fopen(wrappedFilePath, "wb");
+ wrappedFile->bzfile = BZ2_bzWriteOpen(NULL, wrappedFile->file, 9, 0, 0);
}
else {
wrappedFile->file = fopen(wrappedFilePath, "wb");
@@ -343,16 +337,7 @@ union SPSomeFileHandle {
{
if (!fileIsClosed) {
[self synchronizeFile];
-
- if (compressionFormat == SPGzipCompression) {
- gzclose(wrappedFile->gzfile);
- }
- else if (compressionFormat == SPBzip2Compression) {
- BZ2_bzclose(wrappedFile->bzfile);
- }
- else {
- fclose(wrappedFile->file);
- }
+ [self _closeFileHandles];
if (processingThread) {
if ([processingThread isExecuting]) {
@@ -442,6 +427,35 @@ union SPSomeFileHandle {
[writePool drain];
}
+/**
+ * Close any open file handles
+ */
+- (void)_closeFileHandles
+{
+ if (compressionFormat == SPGzipCompression) {
+ gzclose(wrappedFile->gzfile);
+ wrappedFile->gzfile = NULL;
+ }
+ else if (compressionFormat == SPBzip2Compression) {
+ if (fileMode == O_RDONLY) {
+ BZ2_bzReadClose(NULL, wrappedFile->bzfile);
+ }
+ else if (fileMode == O_WRONLY) {
+ BZ2_bzWriteClose(NULL, wrappedFile->bzfile, 0, NULL, NULL);
+ }
+ else {
+ [NSException raise:NSInvalidArgumentException format:@"SPFileHandle only supports read-only and write-only file modes"];
+ }
+ fclose(wrappedFile->file);
+ wrappedFile->bzfile = NULL;
+ wrappedFile->file = NULL;
+ }
+ else {
+ fclose(wrappedFile->file);
+ wrappedFile->file = NULL;
+ }
+}
+
#pragma mark -
/**
diff --git a/Source/SPIndexesController.m b/Source/SPIndexesController.m
index debeaf30..dcf01ccb 100644
--- a/Source/SPIndexesController.m
+++ b/Source/SPIndexesController.m
@@ -40,6 +40,7 @@
#import "SPTableStructure.h"
#import "SPTableStructureLoading.h"
#import "SPThreadAdditions.h"
+#import "SPFunctions.h"
#import <SPMySQL/SPMySQL.h>
@@ -245,31 +246,15 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
if ((index == -1) || (index > ((NSInteger)[indexes count] - 1))) return;
- NSString *keyName = [[indexes objectAtIndex:index] objectForKey:@"Key_name"];
- NSString *columnName = [[indexes objectAtIndex:index] objectForKey:@"Column_name"];
-
- BOOL hasForeignKey = NO;
- NSString *constraintName = @"";
-
- // Check to see whether the user is attempting to remove an index that a foreign key constraint depends on
- // thus would result in an error if not dropped before removing the index.
- for (NSDictionary *constraint in [tableData getConstraints])
- {
- for (NSString *column in [constraint objectForKey:@"columns"])
- {
- if ([column isEqualToString:columnName]) {
- hasForeignKey = YES;
- constraintName = [constraint objectForKey:@"name"];
- break;
- }
- }
- }
+ NSString *keyName = [[indexes objectAtIndex:index] objectForKey:@"Key_name"];
+
+ if(![keyName length]) return; //safeguard for the contextInfo array creation below
NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete index '%@'?", @"delete index message"), keyName]
defaultButton:NSLocalizedString(@"Delete", @"delete button")
alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
otherButton:nil
- informativeTextWithFormat:hasForeignKey ? NSLocalizedString(@"The foreign key relationship '%@' has a dependency on this index. This relationship must be removed before the index can be deleted.\n\nAre you sure you want to continue to delete the relationship and the index? This action cannot be undone.", @"delete index and foreign key informative message"), constraintName : NSLocalizedString(@"Are you sure you want to delete the index '%@'? This action cannot be undone.", @"delete index informative message"), keyName];
+ informativeTextWithFormat:NSLocalizedString(@"Are you sure you want to delete the index '%@'? This action cannot be undone.", @"delete index informative message"), keyName];
[alert setAlertStyle:NSCriticalAlertStyle];
@@ -283,7 +268,7 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
[alert beginSheetModalForWindow:[dbDocument parentWindow]
modalDelegate:self
didEndSelector:@selector(removeIndexSheetDidEnd:returnCode:contextInfo:)
- contextInfo:(hasForeignKey) ? @"removeIndexAndForeignKey" : @"removeIndex"];
+ contextInfo:[@{@"Key_name" : keyName} retain]]; // contextInfo is NOT retained by Cocoa!
}
/**
@@ -636,22 +621,19 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
{
// Order out current sheet to suppress overlapping of sheets
[[alert window] orderOut:nil];
+
+ NSDictionary *info = [(id)contextInfo autorelease]; //we explicitly retained it beforehand, because Cocoa does NOT!
if (returnCode == NSAlertDefaultReturn) {
[dbDocument startTaskWithDescription:NSLocalizedString(@"Removing index...", @"removing index task status message")];
- NSMutableDictionary *indexDetails = [NSMutableDictionary dictionary];
-
- [indexDetails setObject:[indexes objectAtIndex:[indexesTableView selectedRow]] forKey:@"Index"];
- [indexDetails setObject:[NSNumber numberWithBool:[(NSString *)contextInfo hasSuffix:@"AndForeignKey"]] forKey:@"RemoveForeignKey"];
-
if ([NSThread isMainThread]) {
- [NSThread detachNewThreadWithName:SPCtxt(@"SPIndexesController index removal thread", dbDocument) target:self selector:@selector(_removeIndexUsingDetails:) object:indexDetails];
+ [NSThread detachNewThreadWithName:SPCtxt(@"SPIndexesController index removal thread", dbDocument) target:self selector:@selector(_removeIndexUsingDetails:) object:info];
[dbDocument enableTaskCancellationWithTitle:NSLocalizedString(@"Cancel", @"cancel button") callbackObject:self callbackFunction:NULL];
}
else {
- [self _removeIndexUsingDetails:indexDetails];
+ [self _removeIndexUsingDetails:info];
}
}
}
@@ -911,58 +893,48 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- NSDictionary *index = [indexDetails objectForKey:@"Index"];
- BOOL removeForeignKey = [[indexDetails objectForKey:@"RemoveForeignKey"] boolValue];
+ NSString *index = [indexDetails objectForKey:@"Key_name"];
+ NSString *fkName = [indexDetails objectForKey:@"ForeignKey"];
// Remove the foreign key dependency before the index if required
- if (removeForeignKey) {
-
- NSString *columnName = [index objectForKey:@"Column_name"];
-
- NSString *constraintName = @"";
-
- // Check to see whether the user is attempting to remove an index that a foreign key constraint depends on
- // thus would result in an error if not dropped before removing the index.
- for (NSDictionary *constraint in [tableData getConstraints])
- {
- for (NSString *column in [constraint objectForKey:@"columns"])
- {
- if ([column isEqualToString:columnName]) {
- constraintName = [constraint objectForKey:@"name"];
- break;
- }
- }
- }
+ if ([fkName length]) {
- [connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP FOREIGN KEY %@", [table backtickQuotedString], [constraintName backtickQuotedString]]];
+ [connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP FOREIGN KEY %@", [table backtickQuotedString], [fkName backtickQuotedString]]];
// Check for errors, but only if the query wasn't cancelled
if ([connection queryErrored] && ![connection lastQueryWasCancelled]) {
NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
[errorDictionary setObject:NSLocalizedString(@"Unable to delete relation", @"error deleting relation message") forKey:@"title"];
- [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to delete the relation '%@'.\n\nMySQL said: %@", @"error deleting relation informative message"), constraintName, [connection lastErrorMessage]] forKey:@"message"];
+ [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to delete the relation '%@'.\n\nMySQL said: %@", @"error deleting relation informative message"), fkName, [connection lastErrorMessage]] forKey:@"message"];
[(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
}
}
- if ([[index objectForKey:@"Key_name"] isEqualToString:@"PRIMARY"]) {
+ if ([index isEqualToString:@"PRIMARY"]) {
[connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP PRIMARY KEY", [table backtickQuotedString]]];
}
else {
[connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP INDEX %@",
- [table backtickQuotedString], [[index objectForKey:@"Key_name"] backtickQuotedString]]];
+ [table backtickQuotedString], [index backtickQuotedString]]];
}
// Check for errors, but only if the query wasn't cancelled
if ([connection queryErrored] && ![connection lastQueryWasCancelled]) {
- NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
-
- [errorDictionary setObject:NSLocalizedString(@"Unable to delete index", @"error deleting index message") forKey:@"title"];
- [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occured while trying to delete the index.\n\nMySQL said: %@", @"error deleting index informative message"), [connection lastErrorMessage]] forKey:@"message"];
-
- [(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
+ //if the last error was 1553 and we did not already try to remove a FK beforehand, we have to request to remove the foreign key before we can remove the index
+ if([connection lastErrorID] == 1553 /* ER_DROP_INDEX_FK */ && ![fkName length]) {
+ NSDictionary *details = @{@"Key_name": index, @"error": SPBoxNil([connection lastErrorMessage])};
+ [self performSelectorOnMainThread:@selector(_removingIndexFailedWithForeignKeyError:) withObject:details waitUntilDone:NO];
+ }
+ else {
+ NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
+
+ [errorDictionary setObject:NSLocalizedString(@"Unable to delete index", @"error deleting index message") forKey:@"title"];
+ [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occured while trying to delete the index.\n\nMySQL said: %@", @"error deleting index informative message"), [connection lastErrorMessage]] forKey:@"message"];
+
+ [(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
+ }
}
else {
[tableData resetAllData];
@@ -977,6 +949,81 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
}
/**
+ * If removing an index failed, because an FK depends on it (mysql error 1553) this
+ * will ask the user to confirm deleting the FK, too (if it is found).
+ *
+ * MUST be called on the UI thread!
+ */
+- (void)_removingIndexFailedWithForeignKeyError:(NSDictionary *)info
+{
+ NSString *keyName = [info objectForKey:@"Key_name"];
+
+ //we have to find out which fk uses this index (and need to watch out for compound indexes)
+ NSString *constraintName = nil;
+
+ NSMutableArray *myColumns = [NSMutableArray array];
+
+ for (NSDictionary *indexPart in indexes) {
+ if ([[indexPart objectForKey:@"Key_name"] isEqualToString:keyName]) {
+ [myColumns addObject:[indexPart objectForKey:@"Column_name"]];
+ }
+ }
+
+ //if the index has no columns, something's fucky
+ if(![myColumns count]) {
+ SPOnewayAlertSheet(
+ [NSString stringWithFormat:NSLocalizedString(@"Failed to remove index '%@'", @"table structure : indexes : delete index : no columns error : title"),keyName],
+ [dbDocument parentWindow],
+ NSLocalizedString(@"Sequel Pro could not find any columns belonging to this index. Maybe it has been removed already?", @"table structure : indexes : delete index : no columns error : description")
+ );
+ return;
+ }
+
+ [myColumns sortUsingSelector:@selector(compare:)];
+
+ //now let's find a matching fk (ie. one that has the same columns as the index)
+ for (NSDictionary *fkInfo in [tableData getConstraints]) {
+ NSArray *fkColumns = [[fkInfo objectForKey:@"columns"] sortedArrayUsingSelector:@selector(compare:)];
+ if(![myColumns isEqualToArray:fkColumns]) continue;
+ if(constraintName != nil) {
+ goto no_or_multiple_matches; //we already found a matching FK, but there is another one!? -> abort
+ }
+ constraintName = [fkInfo objectForKey:@"name"];
+ }
+
+ if(!constraintName) goto no_or_multiple_matches; //we found no matching FK
+
+ NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"A foreign key needs this index", @"table structure : indexes : delete index : error 1553 : title")
+ defaultButton:NSLocalizedString(@"Delete Both", @"table structure : indexes : delete index : error 1553 : delete index and FK button")
+ alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
+ otherButton:nil
+ informativeTextWithFormat:NSLocalizedString(@"The foreign key relationship '%@' has a dependency on index '%@'. This relationship must be removed before the index can be deleted.\n\nAre you sure you want to continue to delete the relationship and the index? This action cannot be undone.", @"table structure : indexes : delete index : error 1553 : description"), constraintName, keyName];
+
+ [alert setAlertStyle:NSCriticalAlertStyle];
+
+ NSArray *buttons = [alert buttons];
+
+ // Change the alert's cancel button to have the key equivalent of return
+ [[buttons objectAtIndex:0] setKeyEquivalent:@"d"];
+ [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask];
+ [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
+
+ [alert beginSheetModalForWindow:[dbDocument parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(removeIndexSheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[@{@"Key_name" : keyName, @"ForeignKey": constraintName} retain]]; // contextInfo is NOT retained by Cocoa!
+
+ return;
+
+no_or_multiple_matches:
+ SPOnewayAlertSheet(
+ NSLocalizedString(@"A foreign key needs this index", @"table structure : indexes : delete index : error 1553, no FK found : title"),
+ [dbDocument parentWindow],
+ [NSString stringWithFormat:NSLocalizedString(@"This index cannot be deleted, because it is used by an existing foreign key relationship.\n\nPlease remove the relationship, before trying to remove this index.\n\nMySQL said: %@", @"table structure : indexes : delete index : error 1553, no FK found : description"), [info objectForKey:@"error"]]
+ );
+}
+
+/**
* Resizes the new index sheet's height by the supplied delta, while retaining the position of
* all interface controls to accommodate the advanced options view.
*
diff --git a/Source/SPProcessListController.m b/Source/SPProcessListController.m
index 8290b5d9..eb22d484 100644
--- a/Source/SPProcessListController.m
+++ b/Source/SPProcessListController.m
@@ -42,6 +42,9 @@ static NSString *SPKillProcessQueryMode = @"SPKillProcessQueryMode";
static NSString *SPKillProcessConnectionMode = @"SPKillProcessConnectionMode";
static NSString *SPTableViewIDColumnIdentifier = @"Id";
+static NSString * const SPKillModeKey = @"SPKillMode";
+static NSString * const SPKillIdKey = @"SPKillId";
+
@interface SPProcessListController (PrivateAPI)
- (void)_processListRefreshed;
@@ -283,7 +286,14 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[alert setAlertStyle:NSCriticalAlertStyle];
- [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessQueryMode];
+ // while the alert is displayed, the results may be updated and the selectedRow may point to a different
+ // row or has disappeared (= -1) by the time the didEndSelector is invoked,
+ // so we must remember the ACTUAL processId we prompt the user to kill.
+ NSDictionary *userInfo = @{SPKillModeKey: SPKillProcessQueryMode, SPKillIdKey: @(processId)};
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[userInfo retain]]; //keep in mind contextInfo is a void * and not an id => no memory management here
}
/**
@@ -311,7 +321,14 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[alert setAlertStyle:NSCriticalAlertStyle];
- [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessConnectionMode];
+ // while the alert is displayed, the results may be updated and the selectedRow may point to a different
+ // row or has disappeared (= -1) by the time the didEndSelector is invoked,
+ // so we must remember the ACTUAL processId we prompt the user to kill.
+ NSDictionary *userInfo = @{SPKillModeKey: SPKillProcessConnectionMode, SPKillIdKey: @(processId)};
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[userInfo retain]]; //keep in mind contextInfo is a void * and not an id => no memory management here
}
/**
@@ -364,7 +381,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
modalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
- contextInfo:nil];
+ contextInfo:NULL];
}
#pragma mark -
@@ -386,7 +403,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
/**
* Invoked when the kill alerts are dismissed. Decide what to do based on the user's decision.
*/
-- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
+- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
// Order out current sheet to suppress overlapping of sheets
if ([sheet respondsToSelector:@selector(orderOut:)]) {
@@ -396,20 +413,24 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[[sheet window] orderOut:nil];
}
- if (returnCode == NSAlertDefaultReturn) {
-
- if (sheet == customIntervalWindow) {
- [self _startAutoRefreshTimerWithInterval:[customIntervalTextField integerValue]];
- }
- else {
- long long processId = [[[processesFiltered objectAtIndex:[processListTableView selectedRow]] valueForKey:@"Id"] longLongValue];
+ if (sheet == customIntervalWindow) {
+ if (returnCode == NSAlertDefaultReturn) [self _startAutoRefreshTimerWithInterval:[customIntervalTextField integerValue]];
+ }
+ else {
+ NSDictionary *userInfo = [(NSDictionary *)contextInfo autorelease]; //we retained it during the beginSheet… call because Cocoa does not do memory management on void *.
+ if (returnCode == NSAlertDefaultReturn) {
+ long long processId = [[userInfo objectForKey:SPKillIdKey] longLongValue];
- if ([contextInfo isEqualToString:SPKillProcessQueryMode]) {
+ NSString *mode = [userInfo objectForKey:SPKillModeKey];
+ if ([mode isEqualToString:SPKillProcessQueryMode]) {
[self _killProcessQueryWithId:processId];
}
- else if ([contextInfo isEqualToString:SPKillProcessConnectionMode]) {
+ else if ([mode isEqualToString:SPKillProcessConnectionMode]) {
[self _killProcessConnectionWithId:processId];
}
+ else {
+ [NSException raise:NSInternalInconsistencyException format:@"%s: Unhandled branch for mode=%@", __PRETTY_FUNCTION__, mode];
+ }
}
}
}
diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h
index ac720d01..74329e20 100644
--- a/Source/SPTableContent.h
+++ b/Source/SPTableContent.h
@@ -125,7 +125,6 @@
BOOL _mainNibLoaded;
BOOL isWorking;
pthread_mutex_t tableValuesLock;
- NSCondition *tableLoadingCondition;
#ifndef SP_CODA
NSMutableArray *nibObjectsToRelease;
#endif
diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m
index 0738fe96..48871c11 100644
--- a/Source/SPTableContent.m
+++ b/Source/SPTableContent.m
@@ -31,6 +31,7 @@
#import "SPTableContent.h"
#import "SPTableContentFilter.h"
+#import "SPTableContentDataSource.h"
#import "SPDatabaseDocument.h"
#import "SPTableStructure.h"
#import "SPTableInfo.h"
@@ -81,6 +82,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
@interface SPTableContent ()
- (BOOL)cancelRowEditing;
+- (void)documentWillClose:(NSNotification *)notification;
@end
@@ -172,7 +174,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
usedQuery = [[NSString alloc] initWithString:@""];
tableLoadTimer = nil;
- tableLoadingCondition = [NSCondition new];
blackColor = [NSColor blackColor];
lightGrayColor = [NSColor lightGrayColor];
@@ -293,6 +294,10 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
selector:@selector(endDocumentTaskForTab:)
name:SPDocumentTaskEndNotification
object:tableDocumentInstance];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentWillClose:)
+ name:SPDocumentWillCloseNotification
+ object:tableDocumentInstance];
}
#pragma mark -
@@ -1056,11 +1061,8 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
// Set up the table updates timer and wait for it to notify this thread about completion
[[self onMainThread] initTableLoadTimer];
- [tableLoadingCondition lock];
- while (![tableValues dataDownloaded]) {
- [tableLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
- }
- [tableLoadingCondition unlock];
+ [tableValues awaitDataDownloaded];
+
tableRowsCount = [tableValues count];
// If the final column autoresize wasn't performed, perform it
@@ -1265,10 +1267,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
if ([tableValues dataDownloaded]) {
- [tableLoadingCondition lock];
- [tableLoadingCondition signal];
[self clearTableLoadTimer];
- [tableLoadingCondition unlock];
}
// Check whether a table update is required, based on whether new rows are
@@ -1324,20 +1323,21 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
{
NSAutoreleasePool *reloadPool = [[NSAutoreleasePool alloc] init];
- // Check whether a save of the current row is required.
- if (![[self onMainThread] saveRowOnDeselect]) return;
+ // Check whether a save of the current row is required, abort if pending changes couldn't be saved.
+ if ([[self onMainThread] saveRowOnDeselect]) {
- // Save view details to restore safely if possible (except viewport, which will be
- // preserved automatically, and can then be scrolled as the table loads)
- [self storeCurrentDetailsForRestoration];
- [self setViewportToRestore:NSZeroRect];
+ // Save view details to restore safely if possible (except viewport, which will be
+ // preserved automatically, and can then be scrolled as the table loads)
+ [self storeCurrentDetailsForRestoration];
+ [self setViewportToRestore:NSZeroRect];
- // Clear the table data column cache and status (including counts)
- [tableDataInstance resetColumnData];
- [tableDataInstance resetStatusData];
+ // Clear the table data column cache and status (including counts)
+ [tableDataInstance resetColumnData];
+ [tableDataInstance resetStatusData];
- // Load the table's data
- [self loadTable:[tablesListInstance tableName]];
+ // Load the table's data
+ [self loadTable:[tablesListInstance tableName]];
+ }
[tableDocumentInstance endTask];
@@ -2391,7 +2391,8 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
for (NSTableColumn *aTableColumn in tableColumns)
{
- id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, [[aTableColumn identifier] integerValue]);
+ NSUInteger columnIndex = [[aTableColumn identifier] integerValue];
+ id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, columnIndex);
if ([o isNSNull]) {
[tempRow addObject:includeNULLs ? [NSNull null] : [prefs objectForKey:SPNullValue]];
@@ -2442,7 +2443,17 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
[[image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01f] base64Encoding]]];
}
else {
- [tempRow addObject:hide ? @"&lt;BLOB&gt;" : [o stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]];
+ NSString *str;
+ if (hide) {
+ str = @"&lt;BLOB&gt;";
+ }
+ else if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ str = [NSString stringWithFormat:@"0x%@", [o dataToHexString]];
+ }
+ else {
+ str = [o stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
+ }
+ [tempRow addObject:str];
}
if(image) [image release];
@@ -2541,13 +2552,15 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
@"filterValue": targetFilterValue,
@"filterComparison": SPBoxNil(filterComparison)
};
- [self setFiltersToRestore:filterSettings];
-
- // Attempt to switch to the target table
- if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) {
- NSBeep();
- [self setFiltersToRestore:nil];
- }
+ SPMainQSync(^{
+ [self setFiltersToRestore:filterSettings];
+
+ // Attempt to switch to the target table
+ if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) {
+ NSBeep();
+ [self setFiltersToRestore:nil];
+ }
+ });
}
#ifndef SP_CODA
@@ -4140,6 +4153,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
tableRowsSelectable = YES;
}
+//this method is called right before the UI objects are deallocated
+- (void)documentWillClose:(NSNotification *)notification
+{
+ // if a result load is in progress we must stop the timer or it may try to call invalid IBOutlets
+ [self clearTableLoadTimer];
+}
+
#pragma mark -
#pragma mark KVO methods
@@ -4224,7 +4244,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
if(fieldEditor) SPClear(fieldEditor);
[self clearTableLoadTimer];
- SPClear(tableLoadingCondition);
SPClear(tableValues);
pthread_mutex_destroy(&tableValuesLock);
SPClear(dataColumns);
diff --git a/Source/SPTableContentDataSource.h b/Source/SPTableContentDataSource.h
index f257dce7..19864a80 100644
--- a/Source/SPTableContentDataSource.h
+++ b/Source/SPTableContentDataSource.h
@@ -32,4 +32,6 @@
@interface SPTableContent (SPTableContentDataSource)
+- (BOOL)cellValueIsDisplayedAsHexForColumn:(NSUInteger)columnIndex;
+
@end
diff --git a/Source/SPTableContentDataSource.m b/Source/SPTableContentDataSource.m
index 56e8df71..a623f83b 100644
--- a/Source/SPTableContentDataSource.m
+++ b/Source/SPTableContentDataSource.m
@@ -33,6 +33,7 @@
#import "SPDataStorage.h"
#import "SPCopyTable.h"
#import "SPTablesList.h"
+#import "SPAlertSheets.h"
#import <pthread.h>
#import <SPMySQL/SPMySQL.h>
@@ -119,7 +120,7 @@
if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
if ([(NSData *)value length] > 255) {
- return [NSString stringWithFormat:@"0x%@...", [[(NSData *)value subdataWithRange:NSMakeRange(0, 255)] dataToHexString]];
+ return [NSString stringWithFormat:@"0x%@…", [[(NSData *)value subdataWithRange:NSMakeRange(0, 255)] dataToHexString]];
}
return [NSString stringWithFormat:@"0x%@", [(NSData *)value dataToHexString]];
}
@@ -164,10 +165,10 @@
}
#endif
if (tableView == tableContentView) {
-
+ NSInteger columnIndex = [[tableColumn identifier] integerValue];
// If the current cell should have been edited in a sheet, do nothing - field closing will have already
// updated the field.
- if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:[[tableColumn identifier] integerValue] checkWithLock:NULL]) {
+ if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:columnIndex checkWithLock:NULL]) {
return;
}
@@ -190,18 +191,29 @@
currentlyEditingRow = rowIndex;
}
- NSDictionary *column = NSArrayObjectAtIndex(dataColumns, [[tableColumn identifier] integerValue]);
+ NSDictionary *column = NSArrayObjectAtIndex(dataColumns, columnIndex);
if (object) {
// Restore NULLs if necessary
if ([object isEqualToString:[prefs objectForKey:SPNullValue]] && [[column objectForKey:@"null"] boolValue]) {
object = [NSNull null];
}
+ else if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ // This is a binary object being edited as a hex string.
+ // Convert the string back to binary.
+ // Error checking is done in -control:textShouldEndEditing:
+ NSData *data = [NSData dataWithHexString:object];
+ if (!data) {
+ NSBeep();
+ return;
+ }
+ object = data;
+ }
- [tableValues replaceObjectInRow:rowIndex column:[[tableColumn identifier] integerValue] withObject:object];
+ [tableValues replaceObjectInRow:rowIndex column:columnIndex withObject:object];
}
else {
- [tableValues replaceObjectInRow:rowIndex column:[[tableColumn identifier] integerValue] withObject:@""];
+ [tableValues replaceObjectInRow:rowIndex column:columnIndex withObject:@""];
}
}
}
diff --git a/Source/SPTableContentDelegate.m b/Source/SPTableContentDelegate.m
index 186fbfcc..0a80b602 100644
--- a/Source/SPTableContentDelegate.m
+++ b/Source/SPTableContentDelegate.m
@@ -30,6 +30,7 @@
#import "SPTableContentDelegate.h"
#import "SPTableContentFilter.h"
+#import "SPTableContentDataSource.h"
#ifndef SP_CODA /* headers */
#import "SPAppController.h"
#endif
@@ -54,7 +55,6 @@
@interface SPTableContent (SPDeclaredAPI)
- (BOOL)cancelRowEditing;
-- (BOOL)cellValueIsDisplayedAsHexForColumn:(NSUInteger)columnIndex;
@end
@@ -273,13 +273,6 @@
// Retrieve the column definition
NSDictionary *columnDefinition = [cqColumnDefinition objectAtIndex:[[tableColumn identifier] integerValue]];
- // TODO: Fix editing of "Display as Hex" columns and remove this (also see above)
- if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
- NSBeep();
- [SPTooltip showWithObject:NSLocalizedString(@"Disable \"Display Binary Data as Hex\" in the View menu to edit this field.",@"Temporary : Tooltip shown when trying to edit a binary field in table content view while it is displayed using HEX conversion")];
- return NO;
- }
-
// Open the editing sheet if required
if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:[[tableColumn identifier] integerValue] checkWithLock:NULL]) {
@@ -518,10 +511,10 @@
}
else {
[cell setTextColor:blackColor];
- }
-
- if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
- [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : blueColor];
+
+ if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
+ [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : blueColor];
+ }
}
// Disable link arrows for the currently editing row and for any NULL or unloaded cells
@@ -687,6 +680,34 @@
#pragma mark -
#pragma mark Control delegate methods
+- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)editor
+{
+ // Validate hex input
+ // We do this here because the textfield will still be selected with the pending changes if we bail out here
+ if(control == tableContentView) {
+ NSInteger columnIndex = [tableContentView editedColumn];
+ if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ // special case: the "NULL" string
+ NSDictionary *column = NSArrayObjectAtIndex(dataColumns, columnIndex);
+ if ([[editor string] isEqualToString:[prefs objectForKey:SPNullValue]] && [[column objectForKey:@"null"] boolValue]) {
+ return YES;
+ }
+ // This is a binary object being edited as a hex string.
+ // Convert the string back to binary, checking for errors.
+ NSData *data = [NSData dataWithHexString:[editor string]];
+ if (!data) {
+ SPOnewayAlertSheet(
+ NSLocalizedString(@"Invalid hexadecimal value", @"table content : editing : error message title when parsing as hex string failed"),
+ [tableDocumentInstance parentWindow],
+ NSLocalizedString(@"A valid hex string may only contain the numbers 0-9 and letters A-F (a-f). It can optionally begin with „0x“ and spaces will be ignored.\nAlternatively the syntax X'val' is supported, too.", @"table content : editing : error message description when parsing as hex string failed")
+ );
+ return NO;
+ }
+ }
+ }
+ return YES;
+}
+
- (void)controlTextDidChange:(NSNotification *)notification
{
#ifndef SP_CODA
diff --git a/Source/SPTableData.m b/Source/SPTableData.m
index d095c33b..a0bfa7ff 100644
--- a/Source/SPTableData.m
+++ b/Source/SPTableData.m
@@ -582,11 +582,12 @@
if(fieldName == nil || [fieldName length] == 0) {
NSBeep();
SPOnewayAlertSheetWithStyle(
- NSLocalizedString(@"Error while parsing CREATE TABLE syntax",@"error while parsing CREATE TABLE syntax"),
- nil,
- nil,
- [NSString stringWithFormat:NSLocalizedString(@"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item.", @"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item."), fieldsParser],
- NSCriticalAlertStyle);
+ NSLocalizedString(@"Error while parsing CREATE TABLE syntax",@"error while parsing CREATE TABLE syntax"),
+ nil,
+ nil,
+ [NSString stringWithFormat:NSLocalizedString(@"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item.", @"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item."), fieldsParser],
+ NSCriticalAlertStyle
+ );
continue;
}
//if the next character is again a backtick, we stumbled across an escaped backtick. we have to continue parsing.
@@ -625,108 +626,116 @@
// Constraints
if ([[parts objectAtIndex:0] hasPrefix:@"CONSTRAINT"]) {
NSMutableDictionary *constraintDetails = [[NSMutableDictionary alloc] init];
-
- // Extract the relevant details from the constraint string
- [fieldsParser setString:[[parts objectAtIndex:1] stringByTrimmingCharactersInSet:bracketSet]];
- [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"name"];
-
- NSMutableArray *keyColumns = [NSMutableArray array];
- NSArray *keyColumnStrings = [[[parts objectAtIndex:4] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
-
- for (NSString *keyColumn in keyColumnStrings)
- {
- [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
- [keyColumns addObject:[fieldsParser unquotedString]];
- }
-
- [constraintDetails setObject:keyColumns forKey:@"columns"];
-
- NSString *part = [[parts objectAtIndex:6] stringByTrimmingCharactersInSet:bracketSet];
-
- NSArray *reference = [part captureComponentsMatchedByRegex:@"^`([\\w_.]+)`\\.`([\\w_.]+)`$" options:RKLCaseless range:NSMakeRange(0, [part length]) error:nil];
-
- if ([reference count]) {
- [constraintDetails setObject:[reference objectAtIndex:1] forKey:@"ref_database"];
- [constraintDetails setObject:[reference objectAtIndex:2] forKey:@"ref_table"];
- }
- else {
- [fieldsParser setString:part];
- [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"ref_table"];
- }
-
- NSMutableArray *refKeyColumns = [NSMutableArray array];
- NSArray *refKeyColumnStrings = [[[parts objectAtIndex:7] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
-
- for (NSString *keyColumn in refKeyColumnStrings)
- {
- [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
- [refKeyColumns addObject:[fieldsParser unquotedString]];
- }
-
- [constraintDetails setObject:refKeyColumns forKey:@"ref_columns"];
-
- NSUInteger nextOffs = 12;
- if ([parts count] > 8) {
- // NOTE: this won't get SET NULL | NO ACTION | RESTRICT
- if ([[parts objectAtIndex:9] hasPrefix:@"UPDATE"]) {
- if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"update"];
- nextOffs = 13;
- } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"update"];
- nextOffs = 13;
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
- forKey:@"update"];
- }
+ if([[parts objectAtIndex:2] hasPrefix:@"FOREIGN"] && [[parts objectAtIndex:3] hasPrefix:@"KEY"]) {
+ // Extract the relevant details from the constraint string
+ [fieldsParser setString:[[parts objectAtIndex:1] stringByTrimmingCharactersInSet:bracketSet]];
+ [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"name"];
+
+ NSMutableArray *keyColumns = [NSMutableArray array];
+ NSArray *keyColumnStrings = [[[parts objectAtIndex:4] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
+
+ for (NSString *keyColumn in keyColumnStrings)
+ {
+ [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
+ [keyColumns addObject:[fieldsParser unquotedString]];
}
- else if ([NSArrayObjectAtIndex(parts, 9) hasPrefix:@"DELETE"]) {
- if ([NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"]) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"delete"];
- nextOffs = 13;
- } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"delete"];
- nextOffs = 13;
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
- forKey:@"delete"];
- }
+
+ [constraintDetails setObject:keyColumns forKey:@"columns"];
+
+ NSString *part = [[parts objectAtIndex:6] stringByTrimmingCharactersInSet:bracketSet];
+
+ NSArray *reference = [part captureComponentsMatchedByRegex:@"^`([\\w_.]+)`\\.`([\\w_.]+)`$" options:RKLCaseless range:NSMakeRange(0, [part length]) error:nil];
+
+ if ([reference count]) {
+ [constraintDetails setObject:[reference objectAtIndex:1] forKey:@"ref_database"];
+ [constraintDetails setObject:[reference objectAtIndex:2] forKey:@"ref_table"];
}
- }
-
- if ([parts count] > nextOffs - 1) {
- if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"UPDATE"] ) {
- if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"update"];
- } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"update"];
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
- forKey:@"update"];
+ else {
+ [fieldsParser setString:part];
+ [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"ref_table"];
+ }
+
+ NSMutableArray *refKeyColumns = [NSMutableArray array];
+ NSArray *refKeyColumnStrings = [[[parts objectAtIndex:7] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
+
+ for (NSString *keyColumn in refKeyColumnStrings)
+ {
+ [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
+ [refKeyColumns addObject:[fieldsParser unquotedString]];
+ }
+
+ [constraintDetails setObject:refKeyColumns forKey:@"ref_columns"];
+
+ NSUInteger nextOffs = 12;
+
+ if ([parts count] > 8) {
+ // NOTE: this won't get SET NULL | NO ACTION | RESTRICT
+ if ([[parts objectAtIndex:9] hasPrefix:@"UPDATE"]) {
+ if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"update"];
+ nextOffs = 13;
+ } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"update"];
+ nextOffs = 13;
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
+ forKey:@"update"];
+ }
+ }
+ else if ([NSArrayObjectAtIndex(parts, 9) hasPrefix:@"DELETE"]) {
+ if ([NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"]) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"delete"];
+ nextOffs = 13;
+ } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"delete"];
+ nextOffs = 13;
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
+ forKey:@"delete"];
+ }
}
}
- else if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"DELETE"] ) {
- if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"delete"];
- } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"delete"];
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
- forKey:@"delete"];
+
+ if ([parts count] > nextOffs - 1) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"UPDATE"] ) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"update"];
+ } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"update"];
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
+ forKey:@"update"];
+ }
+ }
+ else if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"DELETE"] ) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"delete"];
+ } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"delete"];
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
+ forKey:@"delete"];
+ }
}
}
+
+ [constraints addObject:constraintDetails];
+ }
+ else {
+ //TODO: MariaDB 10.2.1+ (not Mysql) supports syntax:
+ // CONSTRAINT [constraint_name] CHECK (expression)
+ SPLog(@"Skipping unrecognized CONSTRAINT in CREATE stmt: %@", fieldsParser);
}
- [constraints addObject:constraintDetails];
[constraintDetails release];
}
diff --git a/Source/SPTablesList.m b/Source/SPTablesList.m
index 2fa26229..c75198b7 100644
--- a/Source/SPTablesList.m
+++ b/Source/SPTablesList.m
@@ -232,7 +232,8 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
NSString *pQuery = [NSString stringWithFormat:@"SELECT * FROM information_schema.routines WHERE routine_schema = %@ ORDER BY routine_name", [[tableDocumentInstance database] tickQuotedString]];
theResult = [mySQLConnection queryString:pQuery];
[theResult setDefaultRowReturnType:SPMySQLResultRowAsArray];
-
+ [theResult setReturnDataAsStrings:YES]; //see tables above
+
// Check for mysql errors - if information_schema is not accessible for some reasons
// omit adding procedures and functions
if(![mySQLConnection queryErrored] && theResult != nil && [theResult numberOfRows] && [theResult numberOfFields] > 3) {
@@ -1370,6 +1371,8 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
/**
* Select an item using the provided name; returns YES if the
* supplied name could be selected, or NO if not.
+ *
+ * MUST BE CALLED ON THE UI THREAD!
*/
- (BOOL)selectItemWithName:(NSString *)theName
{
@@ -1418,7 +1421,7 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
}
}
- [[tablesListView onMainThread] scrollRowToVisible:[tablesListView selectedRow]];
+ [tablesListView scrollRowToVisible:[tablesListView selectedRow]];
#endif
return YES;
diff --git a/Source/SPWindowController.m b/Source/SPWindowController.m
index 7f3c687b..9755cd0d 100644
--- a/Source/SPWindowController.m
+++ b/Source/SPWindowController.m
@@ -160,15 +160,14 @@
*/
- (IBAction)closeTab:(id)sender
{
- // Return if the selected tab shouldn't be closed
- if (![selectedTableDocument parentTabShouldClose]) return;
-
// If there are multiple tabs, close the front tab.
if ([tabView numberOfTabViewItems] > 1) {
+ // Return if the selected tab shouldn't be closed
+ if (![selectedTableDocument parentTabShouldClose]) return;
[tabView removeTabViewItem:[tabView selectedTabViewItem]];
-
}
else {
+ //trying to close the window will itself call parentTabShouldClose for all tabs in windowShouldClose:
[[self window] performClose:self];
}
}
diff --git a/Source/SPWindowControllerDelegate.m b/Source/SPWindowControllerDelegate.m
index 79b1e2f1..009dc0a4 100644
--- a/Source/SPWindowControllerDelegate.m
+++ b/Source/SPWindowControllerDelegate.m
@@ -56,15 +56,11 @@
*/
- (BOOL)windowShouldClose:(id)sender
{
- // Iterate through all tabs if more than one tab is opened only otherwise
- // [... parentTabShouldClose] will be called twice [see self closeTab:(id)sender]
- if ([[tabView tabViewItems] count] > 1) {
- for (NSTabViewItem *eachItem in [tabView tabViewItems])
- {
- SPDatabaseDocument *eachDocument = [eachItem identifier];
-
- if (![eachDocument parentTabShouldClose]) return NO;
- }
+ for (NSTabViewItem *eachItem in [tabView tabViewItems])
+ {
+ SPDatabaseDocument *eachDocument = [eachItem identifier];
+
+ if (![eachDocument parentTabShouldClose]) return NO;
}
// Remove global session data if the last window of a session will be closed
@@ -190,6 +186,8 @@
/**
* Called to determine whether a tab view item can be closed
+ *
+ * Note: This is ONLY called when using the "X" button on the tab itself.
*/
- (BOOL)tabView:(NSTabView *)aTabView shouldCloseTabViewItem:(NSTabViewItem *)tabViewItem
{
diff --git a/UnitTests/SPDataAdditionsTests.m b/UnitTests/SPDataAdditionsTests.m
index 4769e499..0491d4aa 100644
--- a/UnitTests/SPDataAdditionsTests.m
+++ b/UnitTests/SPDataAdditionsTests.m
@@ -41,6 +41,8 @@
- (void)testDataDecryptedWithPassword;
- (void)testDataDecryptedWithKey;
- (void)testEnumerateLinesBreakingAt_withBlock;
+- (void)testDataWithHexString;
+- (void)testDataWithHexStringMySQL;
@end
@@ -384,4 +386,78 @@
}
}
+- (void)testDataWithHexString
+{
+ //nil
+ {
+ XCTAssertNil([NSData dataWithHexString:nil], @"nil input");
+ }
+ //empty
+ {
+ XCTAssertTrue([[NSData dataWithHexString:@""] length] == 0, @"empty input");
+ }
+ //single byte 0
+ {
+ const char single[] = {0};
+ XCTAssertEqualObjects([NSData dataWithHexString:@"0"], [NSData dataWithBytes:single length:1], @"single '0'" );
+ }
+ //empty, with 0x
+ {
+ XCTAssertEqualObjects([NSData dataWithHexString:@" 0x "], [NSData data], @"empty input after trimming");
+ }
+ //one lower nibble
+ {
+ const char single[] = { 0xf };
+ XCTAssertEqualObjects([NSData dataWithHexString:@"0xf"], [NSData dataWithBytes:single length:1], @"0x0F");
+ }
+ //full char, uppercase
+ {
+ const char single[] = { 0xcf };
+ XCTAssertEqualObjects([NSData dataWithHexString:@"CF"], [NSData dataWithBytes:single length:1], @"0xCF");
+ }
+ //regular input
+ {
+ NSString *inp = @"0x de AD Be eF\t0102 0304";
+ const char exp[] = {0xde,0xad,0xbe,0xef,0x01,0x02,0x03,0x04};
+ XCTAssertEqualObjects([NSData dataWithHexString:inp], [NSData dataWithBytes:exp length:sizeof(exp)], @"regular input");
+ }
+ //invalid input
+ {
+ XCTAssertNil([NSData dataWithHexString:@"0xaG"], @"invalid char in input");
+ }
+}
+
+- (void)testDataWithHexStringMySQL
+{
+ //empty
+ {
+ XCTAssertEqualObjects([NSData dataWithHexString:@"x''"], [NSData data], @"empty mysql hex literal");
+ }
+ //empty, whitespace around, capital x
+ {
+ XCTAssertEqualObjects([NSData dataWithHexString:@" X''\t "], [NSData data], @"empty mysql hex literal (2)");
+ }
+ //nonempty valid, case-insensitive
+ {
+ const char exp[] = {0xde,0xad,0xbe,0xef};
+ XCTAssertEqualObjects([NSData dataWithHexString:@"X'deADBeeF'"], [NSData dataWithBytes:exp length:sizeof(exp)], @"regular input");
+ }
+ //bad: uneven
+ {
+ XCTAssertNil([NSData dataWithHexString:@"X'aFF'"],@"uneven length in mysql hex literal");
+ }
+ //bad: whitespace inside literal
+ {
+ XCTAssertNil([NSData dataWithHexString:@"x'0A ff'"], @"whitespace inside mysql hex literal");
+ }
+ //bad: non-whitespace after literal
+ {
+ XCTAssertNil([NSData dataWithHexString:@"X'1234' ."], @"garbage at end");
+ }
+ //bad: non hex char in literal
+ {
+ XCTAssertNil([NSData dataWithHexString:@"x'01äß'"], @"non-hex char in literal");
+ }
+}
+
@end
diff --git a/readme.md b/readme.md
index 62d4c19d..c8020a04 100644
--- a/readme.md
+++ b/readme.md
@@ -17,6 +17,13 @@ Build Instructions
* Click the `Run` button in the toolbar
* If the above doesn't work, please file a [bug report](https://github.com/sequelpro/sequelpro/issues/new)
+Contributing
+============
+
+The best way to help the project is to use our [test builds](https://sequelpro.com/test-builds) and report any issues (both bugs and missing features) in [the issue tracker](https://github.com/sequelpro/sequelpro/issues). If you want to get more involved, then you can comment on issues written by other people or send us a pull request.
+
+Please see our [projects page](https://github.com/sequelpro/sequelpro/projects). This lists the issues where we would most like your help. There are simple and difficult tasks there so new contributors should be able to get started.
+
License
=======