aboutsummaryrefslogtreecommitdiffstats
path: root/Source
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 /Source
parentff1db69283f69b8e9dc7fc373db242c37698c7c2 (diff)
parent1cbc8f7ca081a6538a2df484d89723cf441acb3c (diff)
downloadsequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.tar.gz
sequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.tar.bz2
sequelpro-ebf7d8b7db4144d304bf2224db19d787d631eda0.zip
Merge remote-tracking branch 'sequelpro/master'
Diffstat (limited to 'Source')
-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
24 files changed, 721 insertions, 371 deletions
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
{