From 4144a5e1b78480fd86994a9c255c9a0fb98db48b Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Sun, 9 May 2010 01:00:23 +0000 Subject: Rework alert sheets: - Change MCPConnection.m to no longer use a reference to tableWindow to attach sheets - instead use a delate error display method if available - Rework TableSource and TableContent sheetDidEnd methods into per-task methods rather than overloading contextInfo - Rework SPAlertSheets to perform actions on the main thread, with the loss of (unused) support for a didDismissSelector. This addresses a number of crashes logged by the crash reporter --- Source/TableContent.m | 1234 +++++++++++++++++++++++++------------------------ 1 file changed, 625 insertions(+), 609 deletions(-) (limited to 'Source/TableContent.m') diff --git a/Source/TableContent.m b/Source/TableContent.m index 144f81de..096e18bb 100644 --- a/Source/TableContent.m +++ b/Source/TableContent.m @@ -1321,7 +1321,7 @@ if ( [tableContentView numberOfSelectedRows] < 1 ) return; if ( [tableContentView numberOfSelectedRows] > 1 ) { - SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"You can only copy single rows.", @"message of panel when trying to copy multiple rows")); + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, NSLocalizedString(@"You can only copy single rows.", @"message of panel when trying to copy multiple rows")); return; } @@ -1427,211 +1427,504 @@ [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the selected %ld rows from this table? This action cannot be undone.", @"delete rows informative message"), (long)[tableContentView numberOfSelectedRows]]]; } - [alert beginSheetModalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:contextInfo]; -} - -// Accessors - -/** - * Returns the current result (as shown in table content view) as array, the first object containing the field - * names as array, the following objects containing the rows as array. - */ -- (NSArray *)currentDataResult -{ - NSArray *tableColumns; - NSEnumerator *enumerator; - id tableColumn; - NSMutableArray *currentResult = [NSMutableArray array]; - NSMutableArray *tempRow = [NSMutableArray array]; - NSUInteger i; - - //load table if not already done - if ( ![tablesListInstance contentLoaded] ) { - [self loadTable:[tablesListInstance tableName]]; - } - - tableColumns = [tableContentView tableColumns]; - enumerator = [tableColumns objectEnumerator]; - - //set field names as first line - while ( (tableColumn = [enumerator nextObject]) ) { - [tempRow addObject:[[tableColumn headerCell] stringValue]]; - } - [currentResult addObject:[NSArray arrayWithArray:tempRow]]; - - //add rows - for ( i = 0 ; i < [self numberOfRowsInTableView:nil] ; i++) { - [tempRow removeAllObjects]; - enumerator = [tableColumns objectEnumerator]; - while ( (tableColumn = [enumerator nextObject]) ) { - id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, [[tableColumn identifier] integerValue]); - if([o isNSNull]) - [tempRow addObject:@"NULL"]; - else if ([o isSPNotLoaded]) - [tempRow addObject:NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]; - else if([o isKindOfClass:[NSString class]]) - [tempRow addObject:[o description]]; - else { - NSImage *image = [[NSImage alloc] initWithData:o]; - if(image) { - NSInteger imageWidth = [image size].width; - if (imageWidth > 100) imageWidth = 100; - [tempRow addObject:[NSString stringWithFormat: - @"", - (long)imageWidth, - [[image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0]]]; - } else { - [tempRow addObject:@"<BLOB>"]; - } - if(image) [image release]; - } - } - [currentResult addObject:[NSArray arrayWithArray:tempRow]]; - } - return currentResult; -} - -/** - * Returns the current result (as shown in table content view) as array, the first object containing the field - * names as array, the following objects containing the rows as array. - */ -- (NSArray *)currentResult -{ - NSArray *tableColumns; - NSEnumerator *enumerator; - id tableColumn; - NSMutableArray *currentResult = [NSMutableArray array]; - NSMutableArray *tempRow = [NSMutableArray array]; - NSUInteger i; - - //load table if not already done - if ( ![tablesListInstance contentLoaded] ) { - [self loadTable:[tablesListInstance tableName]]; - } - - tableColumns = [tableContentView tableColumns]; - enumerator = [tableColumns objectEnumerator]; - - //set field names as first line - while ( (tableColumn = [enumerator nextObject]) ) { - [tempRow addObject:[[tableColumn headerCell] stringValue]]; - } - [currentResult addObject:[NSArray arrayWithArray:tempRow]]; - - //add rows - for ( i = 0 ; i < [self numberOfRowsInTableView:nil] ; i++) { - [tempRow removeAllObjects]; - enumerator = [tableColumns objectEnumerator]; - while ( (tableColumn = [enumerator nextObject]) ) { - [tempRow addObject:[self tableView:nil objectValueForTableColumn:tableColumn row:i]]; - } - [currentResult addObject:[NSArray arrayWithArray:tempRow]]; - } - return currentResult; -} - -// Additional methods - -/** - * Sets the connection (received from TableDocument) and makes things that have to be done only once - */ -- (void)setConnection:(MCPConnection *)theConnection -{ - mySQLConnection = theConnection; - - [tableContentView setVerticalMotionCanBeginDrag:NO]; + [alert beginSheetModalForWindow:tableWindow modalDelegate:self didEndSelector:@selector(removeRowSheetDidEnd:returnCode:contextInfo:) contextInfo:contextInfo]; } /** - * Performs the requested action - switching to another table - * with the appropriate filter settings - when a link arrow is - * selected. + * Perform the requested row deletion action. */ -- (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell +- (void)removeRowSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { - if ([tableDocumentInstance isWorking]) return; - - if ([theArrowCell getClickedColumn] == NSNotFound || [theArrowCell getClickedRow] == NSNotFound) return; - // Check whether a save of the current row is required. - if ( ![self saveRowOnDeselect] ) return; + NSMutableIndexSet *selectedRows = [NSMutableIndexSet indexSet]; + NSString *wherePart; + NSInteger i, errors; + BOOL consoleUpdateStatus; + BOOL reloadAfterRemovingRow = [prefs boolForKey:SPReloadAfterRemovingRow]; - // If on the main thread, fire up a thread to perform the load while keeping the modification flag - [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Loading reference...", @"Loading referece task string")]; - if ([NSThread isMainThread]) { - [NSThread detachNewThreadSelector:@selector(clickLinkArrowTask:) toTarget:self withObject:theArrowCell]; - } else { - [self clickLinkArrowTask:theArrowCell]; - } -} -- (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell -{ - NSAutoreleasePool *linkPool = [[NSAutoreleasePool alloc] init]; - NSUInteger dataColumnIndex = [[[[tableContentView tableColumns] objectAtIndex:[theArrowCell getClickedColumn]] identifier] integerValue]; - BOOL tableFilterRequired = NO; + // Order out current sheet to suppress overlapping of sheets + [[alert window] orderOut:nil]; - // Ensure the clicked cell has foreign key details available - NSDictionary *refDictionary = [[dataColumns objectAtIndex:dataColumnIndex] objectForKey:@"foreignkeyreference"]; - if (!refDictionary) { - [linkPool release]; - return; - } + if ( [contextInfo isEqualToString:@"removeallrows"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + //check if the user is currently editing a row + if (isEditingRow) { + //cancel the edit + isEditingRow = NO; + // in case the delete fails, make sure we at least stay in a somewhat consistent state + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + currentlyEditingRow = -1; + } + + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@", [selectedTable backtickQuotedString]]]; + if ( ![mySQLConnection queryErrored] ) { - // Save existing scroll position and details and mark that state is being modified - [spHistoryControllerInstance updateHistoryEntries]; - [spHistoryControllerInstance setModifyingState:YES]; + // Reset auto increment if suppression button was ticked + if([[alert suppressionButton] state] == NSOnState) + [tableSourceInstance setAutoIncrementTo:@"1"]; - NSString *targetFilterValue = [tableValues cellDataAtRow:[theArrowCell getClickedRow] column:dataColumnIndex]; + [self reloadTable:self]; - // If the link is within the current table, apply filter settings manually - if ([[refDictionary objectForKey:@"table"] isEqualToString:selectedTable]) { - [fieldField selectItemWithTitle:[refDictionary objectForKey:@"column"]]; - [self setCompareTypes:self]; - if ([targetFilterValue isNSNull]) { - [compareField selectItemWithTitle:@"IS NULL"]; - } else { - [argumentField setStringValue:targetFilterValue]; + } else { + [self performSelector:@selector(showErrorSheetWith:) + withObject:[NSArray arrayWithObjects:NSLocalizedString(@"Error", @"error"), + [NSString stringWithFormat:NSLocalizedString(@"Couldn't delete rows.\n\nMySQL said: %@", @"message when deleteing all rows failed"), + [mySQLConnection getLastErrorMessage]], + nil] + afterDelay:0.3]; + } } - tableFilterRequired = YES; - } else { + } else if ( [contextInfo isEqualToString:@"removerow"] ) { + if ( returnCode == NSAlertDefaultReturn ) { + [selectedRows addIndexes:[tableContentView selectedRowIndexes]]; - // Store the filter details to use when loading the target table - NSDictionary *filterSettings = [NSDictionary dictionaryWithObjectsAndKeys: - [refDictionary objectForKey:@"column"], @"filterField", - targetFilterValue, @"filterValue", - ([targetFilterValue isNSNull]?@"IS NULL":nil), @"filterComparison", - nil]; - [self setFiltersToRestore:filterSettings]; + //check if the user is currently editing a row + if (isEditingRow) { + //make sure that only one row is selected. This should never happen + if ([selectedRows count]!=1) { + NSLog(@"Expected only one selected row, but found %d",[selectedRows count]); + } + // this code is pretty much taken from the escape key handler + if ( isEditingNewRow ) { + // since the user is currently editing a new row, we don't actually have to delete any rows from the database + // we just have to remove the row from the view (and the store) + isEditingRow = NO; + isEditingNewRow = NO; + tableRowsCount--; + [tableValues removeRowAtIndex:currentlyEditingRow]; + currentlyEditingRow = -1; + [self updateCountText]; + [tableContentView reloadData]; + + //deselect the row + [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; - // Attempt to switch to the target table - if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) { - NSBeep(); - [self setFiltersToRestore:nil]; - } - } + // we also don't have to reload the table, since no query went to the database + return; + } else { + //cancel the edit + isEditingRow = NO; + // in case the delete fails, make sure we at least stay in a somewhat consistent state + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + currentlyEditingRow = -1; + } - // End state and ensure a new history entry - [spHistoryControllerInstance setModifyingState:NO]; - [spHistoryControllerInstance updateHistoryEntries]; + } + [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; + + errors = 0; - // End the task - [tableDocumentInstance endTask]; + // Disable updating of the Console Log window for large number of queries + // to speed the deletion + consoleUpdateStatus = [[SPQueryController sharedQueryController] allowConsoleUpdate]; + if([selectedRows count] > 10) + [[SPQueryController sharedQueryController] setAllowConsoleUpdate:NO]; - // If the same table is the target, trigger a filter task on the main thread - if (tableFilterRequired) - [self performSelectorOnMainThread:@selector(filterTable:) withObject:self waitUntilDone:NO]; - - // Empty the loading pool and exit the thread - [linkPool drain]; -} + NSUInteger index = [selectedRows firstIndex]; -/** - * Sets the compare types for the filter and the appropriate formatter for the textField - */ -- (IBAction)setCompareTypes:(id)sender -{ + NSArray *primaryKeyFieldNames = [tableDataInstance primaryKeyColumnNames]; - if(contentFilters == nil + // If no PRIMARY KEY is found and numberOfSelectedRows > 3 then + // check for uniqueness of rows via combining all column values; + // if unique then use the all columns as 'primary keys' + if([selectedRows count] >3 && primaryKeyFieldNames == nil) { + primaryKeyFieldNames = [tableDataInstance columnNames]; + + NSInteger numberOfRows = 0; + // Get the number of rows in the table + MCPResult *r; + r = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]]; + if (![mySQLConnection queryErrored]) { + NSArray *a = [r fetchRowAsArray]; + if([a count]) + numberOfRows = [[a objectAtIndex:0] integerValue]; + } + // Check for uniqueness via LIMIT numberOfRows-1,numberOfRows for speed + if(numberOfRows > 0) { + [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@ GROUP BY %@ LIMIT %ld,%ld", [selectedTable backtickQuotedString], [primaryKeyFieldNames componentsJoinedAndBacktickQuoted], (long)(numberOfRows-1), (long)numberOfRows]]; + if([mySQLConnection affectedRows] == 0) + primaryKeyFieldNames = nil; + } else { + primaryKeyFieldNames = nil; + } + } + + if(primaryKeyFieldNames == nil) { + // delete row by row + while (index != NSNotFound) { + + wherePart = [NSString stringWithString:[self argumentForRow:index]]; + + //argumentForRow might return empty query, in which case we shouldn't execute the partial query + if([wherePart length]) { + [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@", [selectedTable backtickQuotedString], wherePart]]; + + // Check for errors + if ( ![mySQLConnection affectedRows] || [mySQLConnection queryErrored]) { + // If error delete that index from selectedRows for reloading table if + // "ReloadAfterRemovingRow" is disbaled + if(!reloadAfterRemovingRow) + [selectedRows removeIndex:index]; + errors++; + } + } else { + if(!reloadAfterRemovingRow) + [selectedRows removeIndex:index]; + errors++; + } + index = [selectedRows indexGreaterThanIndex:index]; + } + } else if ([primaryKeyFieldNames count] == 1) { + // if table has only one PRIMARY KEY + // delete the fast way by using the PRIMARY KEY in an IN clause + NSMutableString *deleteQuery = [NSMutableString string]; + NSInteger affectedRows = 0; + + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; + + while (index != NSNotFound) { + + id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(primaryKeyFieldNames,0)] objectForKey:@"datacolumnindex"] integerValue]]; + + if([keyValue isKindOfClass:[NSData class]]) + [deleteQuery appendString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:keyValue]]]; + else + [deleteQuery appendString:[NSString stringWithFormat:@"'%@'", [keyValue description]]]; + + // Split deletion query into 256k chunks + if([deleteQuery length] > 256000) { + [deleteQuery appendString:@")"]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + // Reinit a new deletion query + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; + } else { + [deleteQuery appendString:@","]; + } + + index = [selectedRows indexGreaterThanIndex:index]; + } + + // Check if deleteQuery's maximal length was reached for the last index + // if yes omit the empty query + if(![deleteQuery hasSuffix:@"("]) { + // Replace final , by ) and delete the remaining rows + [deleteQuery setString:[NSString stringWithFormat:@"%@)", [deleteQuery substringToIndex:([deleteQuery length]-1)]]]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + } + + errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; + } else { + // if table has more than one PRIMARY KEY + // delete the row by using all PRIMARY KEYs in an OR clause + NSMutableString *deleteQuery = [NSMutableString string]; + NSInteger affectedRows = 0; + + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; + + while (index != NSNotFound) { + + // Build the AND clause of PRIMARY KEYS + [deleteQuery appendString:@"("]; + for(NSString *primaryKeyFieldName in primaryKeyFieldNames) { + + id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:primaryKeyFieldName] objectForKey:@"datacolumnindex"] integerValue]]; + + [deleteQuery appendString:[primaryKeyFieldName backtickQuotedString]]; + if ([keyValue isKindOfClass:[NSData class]]) { + [deleteQuery appendString:@"=X'"]; + [deleteQuery appendString:[mySQLConnection prepareBinaryData:keyValue]]; + } else { + [deleteQuery appendString:@"='"]; + [deleteQuery appendString:[mySQLConnection prepareString:[keyValue description]]]; + } + [deleteQuery appendString:@"' AND "]; + } + + // Remove the trailing AND and add the closing bracket + [deleteQuery deleteCharactersInRange:NSMakeRange([deleteQuery length]-5, 5)]; + [deleteQuery appendString:@")"]; + + // Split deletion query into 64k chunks + if([deleteQuery length] > 64000) { + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + // Reinit a new deletion query + [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; + } else { + [deleteQuery appendString:@" OR "]; + } + + index = [selectedRows indexGreaterThanIndex:index]; + } + + // Check if deleteQuery's maximal length was reached for the last index + // if yes omit the empty query + if(![deleteQuery hasSuffix:@"WHERE "]) { + // Remove final ' OR ' and delete the remaining rows + [deleteQuery setString:[deleteQuery substringToIndex:([deleteQuery length]-4)]]; + [mySQLConnection queryString:deleteQuery]; + // Remember affected rows for error checking + affectedRows += [mySQLConnection affectedRows]; + } + + errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; + } + + // Restore Console Log window's updating bahaviour + [[SPQueryController sharedQueryController] setAllowConsoleUpdate:consoleUpdateStatus]; + + if (errors) { + NSArray *message; + //TODO: The following three messages are NOT localisable! + if (errors < 0) { + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ more %@ deleted! Please check the Console and inform the Sequel Pro team!", @"message of panel when more rows were deleted"), (long)(errors*-1), ((errors*-1)>1)?@"s":@"", (errors>1)?@"were":@"was"], + nil]; + } + else { + if (primaryKeyFieldNames == nil) + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and use a primary key for your table.", @"message of panel when not all selected fields have been deleted"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s"], + nil]; + else + message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), + [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and check the Console for possible errors inside the primary key%@ for your table.", @"message of panel when not all selected fields have been deleted by using primary keys"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s", (errors>1)?@"s":@""], + nil]; + } + + [self performSelector:@selector(showErrorSheetWith:) + withObject:message + afterDelay:0.3]; + } + + // Refresh table content + if ( errors || reloadAfterRemovingRow ) { + previousTableRowsCount = tableRowsCount; + [self loadTableValues]; + } else { + for ( i = tableRowsCount - 1 ; i >= 0 ; i-- ) { + if ([selectedRows containsIndex:i]) [tableValues removeRowAtIndex:i]; + } + tableRowsCount = [tableValues count]; + [tableContentView reloadData]; + } + [tableContentView deselectAll:self]; + } else { + // The user clicked cancel in the "sure you wanna delete" message + // restore editing or whatever + } + + } +} + + +// Accessors + +/** + * Returns the current result (as shown in table content view) as array, the first object containing the field + * names as array, the following objects containing the rows as array. + */ +- (NSArray *)currentDataResult +{ + NSArray *tableColumns; + NSEnumerator *enumerator; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + NSUInteger i; + + //load table if not already done + if ( ![tablesListInstance contentLoaded] ) { + [self loadTable:[tablesListInstance tableName]]; + } + + tableColumns = [tableContentView tableColumns]; + enumerator = [tableColumns objectEnumerator]; + + //set field names as first line + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[[tableColumn headerCell] stringValue]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + + //add rows + for ( i = 0 ; i < [self numberOfRowsInTableView:nil] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, [[tableColumn identifier] integerValue]); + if([o isNSNull]) + [tempRow addObject:@"NULL"]; + else if ([o isSPNotLoaded]) + [tempRow addObject:NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]; + else if([o isKindOfClass:[NSString class]]) + [tempRow addObject:[o description]]; + else { + NSImage *image = [[NSImage alloc] initWithData:o]; + if(image) { + NSInteger imageWidth = [image size].width; + if (imageWidth > 100) imageWidth = 100; + [tempRow addObject:[NSString stringWithFormat: + @"", + (long)imageWidth, + [[image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0]]]; + } else { + [tempRow addObject:@"<BLOB>"]; + } + if(image) [image release]; + } + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + +/** + * Returns the current result (as shown in table content view) as array, the first object containing the field + * names as array, the following objects containing the rows as array. + */ +- (NSArray *)currentResult +{ + NSArray *tableColumns; + NSEnumerator *enumerator; + id tableColumn; + NSMutableArray *currentResult = [NSMutableArray array]; + NSMutableArray *tempRow = [NSMutableArray array]; + NSUInteger i; + + //load table if not already done + if ( ![tablesListInstance contentLoaded] ) { + [self loadTable:[tablesListInstance tableName]]; + } + + tableColumns = [tableContentView tableColumns]; + enumerator = [tableColumns objectEnumerator]; + + //set field names as first line + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[[tableColumn headerCell] stringValue]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + + //add rows + for ( i = 0 ; i < [self numberOfRowsInTableView:nil] ; i++) { + [tempRow removeAllObjects]; + enumerator = [tableColumns objectEnumerator]; + while ( (tableColumn = [enumerator nextObject]) ) { + [tempRow addObject:[self tableView:nil objectValueForTableColumn:tableColumn row:i]]; + } + [currentResult addObject:[NSArray arrayWithArray:tempRow]]; + } + return currentResult; +} + +// Additional methods + +/** + * Sets the connection (received from TableDocument) and makes things that have to be done only once + */ +- (void)setConnection:(MCPConnection *)theConnection +{ + mySQLConnection = theConnection; + + [tableContentView setVerticalMotionCanBeginDrag:NO]; +} + +/** + * Performs the requested action - switching to another table + * with the appropriate filter settings - when a link arrow is + * selected. + */ +- (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell +{ + if ([tableDocumentInstance isWorking]) return; + + if ([theArrowCell getClickedColumn] == NSNotFound || [theArrowCell getClickedRow] == NSNotFound) return; + + // Check whether a save of the current row is required. + if ( ![self saveRowOnDeselect] ) return; + + // If on the main thread, fire up a thread to perform the load while keeping the modification flag + [tableDocumentInstance startTaskWithDescription:NSLocalizedString(@"Loading reference...", @"Loading referece task string")]; + if ([NSThread isMainThread]) { + [NSThread detachNewThreadSelector:@selector(clickLinkArrowTask:) toTarget:self withObject:theArrowCell]; + } else { + [self clickLinkArrowTask:theArrowCell]; + } +} +- (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell +{ + NSAutoreleasePool *linkPool = [[NSAutoreleasePool alloc] init]; + NSUInteger dataColumnIndex = [[[[tableContentView tableColumns] objectAtIndex:[theArrowCell getClickedColumn]] identifier] integerValue]; + BOOL tableFilterRequired = NO; + + // Ensure the clicked cell has foreign key details available + NSDictionary *refDictionary = [[dataColumns objectAtIndex:dataColumnIndex] objectForKey:@"foreignkeyreference"]; + if (!refDictionary) { + [linkPool release]; + return; + } + + // Save existing scroll position and details and mark that state is being modified + [spHistoryControllerInstance updateHistoryEntries]; + [spHistoryControllerInstance setModifyingState:YES]; + + NSString *targetFilterValue = [tableValues cellDataAtRow:[theArrowCell getClickedRow] column:dataColumnIndex]; + + // If the link is within the current table, apply filter settings manually + if ([[refDictionary objectForKey:@"table"] isEqualToString:selectedTable]) { + [fieldField selectItemWithTitle:[refDictionary objectForKey:@"column"]]; + [self setCompareTypes:self]; + if ([targetFilterValue isNSNull]) { + [compareField selectItemWithTitle:@"IS NULL"]; + } else { + [argumentField setStringValue:targetFilterValue]; + } + tableFilterRequired = YES; + } else { + + // Store the filter details to use when loading the target table + NSDictionary *filterSettings = [NSDictionary dictionaryWithObjectsAndKeys: + [refDictionary objectForKey:@"column"], @"filterField", + targetFilterValue, @"filterValue", + ([targetFilterValue isNSNull]?@"IS NULL":nil), @"filterComparison", + nil]; + [self setFiltersToRestore:filterSettings]; + + // Attempt to switch to the target table + if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) { + NSBeep(); + [self setFiltersToRestore:nil]; + } + } + + // End state and ensure a new history entry + [spHistoryControllerInstance setModifyingState:NO]; + [spHistoryControllerInstance updateHistoryEntries]; + + // End the task + [tableDocumentInstance endTask]; + + // If the same table is the target, trigger a filter task on the main thread + if (tableFilterRequired) + [self performSelectorOnMainThread:@selector(filterTable:) withObject:self waitUntilDone:NO]; + + // Empty the loading pool and exit the thread + [linkPool drain]; +} + +/** + * Sets the compare types for the filter and the appropriate formatter for the textField + */ +- (IBAction)setCompareTypes:(id)sender +{ + + if(contentFilters == nil || ![contentFilters objectForKey:@"number"] || ![contentFilters objectForKey:@"string"] || ![contentFilters objectForKey:@"date"]) { @@ -1908,7 +2201,7 @@ // If no rows have been changed, show error if appropriate. if ( ![mySQLConnection affectedRows] && ![mySQLConnection queryErrored] ) { if ( [prefs boolForKey:SPShowNoAffectedRowsError] ) { - SPBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + SPBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, NSLocalizedString(@"The row was not written to the MySQL database. You probably haven't changed anything.\nReload the table to be sure that the row exists and use a primary key for your table.\n(This error can be turned off in the preferences.)", @"message of panel when no rows have been affected after writing to the db")); } else { NSBeep(); @@ -1955,15 +2248,43 @@ return YES; - // Report errors which have occurred + // Report errors which have occurred + } else { + SPBeginAlertSheet(NSLocalizedString(@"Couldn't write row", @"Couldn't write row error"), NSLocalizedString(@"Edit row", @"Edit row button"), NSLocalizedString(@"Discard changes", @"discard changes button"), nil, tableWindow, self, @selector(addRowErrorSheetDidEnd:returnCode:contextInfo:), nil, + [NSString stringWithFormat:NSLocalizedString(@"MySQL said:\n\n%@", @"message of panel when error while adding row to db"), [mySQLConnection getLastErrorMessage]]); + return NO; + } +} + +/* + * Handle the user decision as a result of an addRow error. + */ +- (void) addRowErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + // Order out current sheet to suppress overlapping of sheets + [[alert window] orderOut:nil]; + + // Edit row selected - reselect the row, and start editing. + if ( returnCode == NSAlertDefaultReturn ) { + [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:currentlyEditingRow] byExtendingSelection:NO]; + [tableContentView performSelector:@selector(keyDown:) withObject:[NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:[tableWindow windowNumber] context:[NSGraphicsContext currentContext] characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:0x24] afterDelay:0.0]; + + // Discard changes selected } else { - SPBeginAlertSheet(NSLocalizedString(@"Couldn't write row", @"Couldn't write row error"), NSLocalizedString(@"Edit row", @"Edit row button"), NSLocalizedString(@"Discard changes", @"discard changes button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addrow", - [NSString stringWithFormat:NSLocalizedString(@"MySQL said:\n\n%@", @"message of panel when error while adding row to db"), [mySQLConnection getLastErrorMessage]]); - return NO; + if ( !isEditingNewRow ) { + [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; + isEditingRow = NO; + } else { + tableRowsCount--; + [tableValues removeRowAtIndex:currentlyEditingRow]; + isEditingRow = NO; + isEditingNewRow = NO; + } + currentlyEditingRow = -1; } + [tableContentView reloadData]; } - /* * A method to be called whenever the table selection changes; checks whether the current * row is being edited, and if so attempts to save it. Returns YES if no save was necessary @@ -2011,436 +2332,131 @@ // Retrieve the field names for this table from the data cache. This is used when requesting all data as part // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. - columnNames = [tableDataInstance columnNames]; - - // Get the primary key if there is one - if ( !keys ) { - setLimit = NO; - keys = [[NSMutableArray alloc] init]; - theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]]; - [theResult setReturnDataAsStrings:YES]; - if ([theResult numOfRows]) [theResult dataSeek:0]; - for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { - theRow = [theResult fetchRowAsDictionary]; - if ( [[theRow objectForKey:@"Key"] isEqualToString:@"PRI"] ) { - [keys addObject:[theRow objectForKey:@"Field"]]; - } - } - } - - // If there is no primary key, all the fields are used in the argument. - if ( ![keys count] ) { - [keys setArray:columnNames]; - setLimit = YES; - - // When the option to not show blob or text options is set, we have a problem - we don't have - // the right values to use in the WHERE statement. Throw an error if this is the case. - if ( [prefs boolForKey:SPLoadBlobsAsNeeded] && [self tableContainsBlobOrTextColumns] ) { - SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, - NSLocalizedString(@"You can't hide blob and text fields when working with tables without index.", @"message of panel when trying to edit tables without index and with hidden blob/text fields")); - [keys removeAllObjects]; - [tableContentView deselectAll:self]; - return @""; - } - } - - // Walk through the keys list constructing the argument list - for ( i = 0 ; i < [keys count] ; i++ ) { - if ( i ) - [argument appendString:@" AND "]; - - // Use the selected row if appropriate - if ( row >= 0 ) { - tempValue = [tableValues cellDataAtRow:row column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; - - // Otherwise use the oldRow - } else { - tempValue = [oldRow objectAtIndex:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; - } - - if ( [tempValue isNSNull] ) { - [argument appendString:[NSString stringWithFormat:@"%@ IS NULL", [NSArrayObjectAtIndex(keys, i) backtickQuotedString]]]; - } else if ( [tempValue isSPNotLoaded] ) { - NSLog(@"Exceptional case: SPNotLoaded object found for method “argumentForRow:”!"); - return @""; - } else { - - if ( [tempValue isKindOfClass:[NSData class]] ) - [value setString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:tempValue]]]; - else - [value setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:tempValue]]]; - - [argument appendString:[NSString stringWithFormat:@"%@ = %@", [NSArrayObjectAtIndex(keys, i) backtickQuotedString], value]]; - } - } - - if (setLimit) [argument appendString:@" LIMIT 1"]; - - return argument; -} - - -/* - * Returns YES if the table contains any columns which are of any of the blob or text types, - * NO otherwise. - */ -- (BOOL)tableContainsBlobOrTextColumns -{ - NSInteger i; - - for ( i = 0 ; i < [dataColumns count]; i++ ) { - if ( [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { - return YES; - } - } - - return NO; -} - -/* - * Returns a string controlling which fields to retrieve for a query. Returns * (all fields) if the preferences - * option dontShowBlob isn't set; otherwise, returns a comma-separated list of all non-blob/text fields. - */ -- (NSString *)fieldListForQuery -{ - NSInteger i; - NSMutableArray *fields = [NSMutableArray array]; - - if (([prefs boolForKey:SPLoadBlobsAsNeeded]) && ([dataColumns count] > 0)) { - - NSArray *columnNames = [tableDataInstance columnNames]; - - for (i = 0 ; i < [columnNames count]; i++) - { - if (![tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { - [fields addObject:[NSArrayObjectAtIndex(columnNames, i) backtickQuotedString]]; - } - else { - // For blob/text fields, select a null placeholder so the column count is still correct - [fields addObject:@"NULL"]; - } - } - - return [fields componentsJoinedByString:@","]; - } - else { - return @"*"; - } -} - -- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo -/* - if contextInfo == addrow: remain in edit-mode if user hits OK, otherwise cancel editing - if contextInfo == removerow: removes row if user hits OK - */ -{ - - NSMutableIndexSet *selectedRows = [NSMutableIndexSet indexSet]; - NSString *wherePart; - NSInteger i, errors; - BOOL consoleUpdateStatus; - BOOL reloadAfterRemovingRow = [prefs boolForKey:SPReloadAfterRemovingRow]; - - if([sheet respondsToSelector:@selector(orderOut:)]) - [sheet orderOut:self]; - else if([sheet window] && [[sheet window] respondsToSelector:@selector(orderOut:)]) - [[sheet window] orderOut:self]; - - if ( [contextInfo isEqualToString:@"addrow"] ) { - - if ( returnCode == NSAlertDefaultReturn ) { - [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:currentlyEditingRow] byExtendingSelection:NO]; - [tableContentView performSelector:@selector(keyDown:) withObject:[NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:[tableWindow windowNumber] context:[NSGraphicsContext currentContext] characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:0x24] afterDelay:0.0]; - } else { - if ( !isEditingNewRow ) { - [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; - isEditingRow = NO; - } else { - tableRowsCount--; - [tableValues removeRowAtIndex:currentlyEditingRow]; - isEditingRow = NO; - isEditingNewRow = NO; - } - currentlyEditingRow = -1; - } - [tableContentView reloadData]; - } else if ( [contextInfo isEqualToString:@"removeallrows"] ) { - if ( returnCode == NSAlertDefaultReturn ) { - //check if the user is currently editing a row - if (isEditingRow) { - //cancel the edit - isEditingRow = NO; - // in case the delete fails, make sure we at least stay in a somewhat consistent state - [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; - currentlyEditingRow = -1; - } - - [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@", [selectedTable backtickQuotedString]]]; - if ( ![mySQLConnection queryErrored] ) { - - // Reset auto increment if suppression button was ticked - if([[sheet suppressionButton] state] == NSOnState) - [tableSourceInstance setAutoIncrementTo:@"1"]; - - [self reloadTable:self]; - - } else { - [self performSelector:@selector(showErrorSheetWith:) - withObject:[NSArray arrayWithObjects:NSLocalizedString(@"Error", @"error"), - [NSString stringWithFormat:NSLocalizedString(@"Couldn't delete rows.\n\nMySQL said: %@", @"message when deleteing all rows failed"), - [mySQLConnection getLastErrorMessage]], - nil] - afterDelay:0.3]; - } - } - } else if ( [contextInfo isEqualToString:@"removerow"] ) { - if ( returnCode == NSAlertDefaultReturn ) { - [selectedRows addIndexes:[tableContentView selectedRowIndexes]]; - - //check if the user is currently editing a row - if (isEditingRow) { - //make sure that only one row is selected. This should never happen - if ([selectedRows count]!=1) { - NSLog(@"Expected only one selected row, but found %d",[selectedRows count]); - } - // this code is pretty much taken from the escape key handler - if ( isEditingNewRow ) { - // since the user is currently editing a new row, we don't actually have to delete any rows from the database - // we just have to remove the row from the view (and the store) - isEditingRow = NO; - isEditingNewRow = NO; - tableRowsCount--; - [tableValues removeRowAtIndex:currentlyEditingRow]; - currentlyEditingRow = -1; - [self updateCountText]; - [tableContentView reloadData]; - - //deselect the row - [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; - - // we also don't have to reload the table, since no query went to the database - return; - } else { - //cancel the edit - isEditingRow = NO; - // in case the delete fails, make sure we at least stay in a somewhat consistent state - [tableValues replaceRowAtIndex:currentlyEditingRow withRowContents:oldRow]; - currentlyEditingRow = -1; - } - - } - [tableContentView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO]; - - errors = 0; - - // Disable updating of the Console Log window for large number of queries - // to speed the deletion - consoleUpdateStatus = [[SPQueryController sharedQueryController] allowConsoleUpdate]; - if([selectedRows count] > 10) - [[SPQueryController sharedQueryController] setAllowConsoleUpdate:NO]; - - NSUInteger index = [selectedRows firstIndex]; - - NSArray *primaryKeyFieldNames = [tableDataInstance primaryKeyColumnNames]; - - // If no PRIMARY KEY is found and numberOfSelectedRows > 3 then - // check for uniqueness of rows via combining all column values; - // if unique then use the all columns as 'primary keys' - if([selectedRows count] >3 && primaryKeyFieldNames == nil) { - primaryKeyFieldNames = [tableDataInstance columnNames]; - - NSInteger numberOfRows = 0; - // Get the number of rows in the table - MCPResult *r; - r = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [selectedTable backtickQuotedString]]]; - if (![mySQLConnection queryErrored]) { - NSArray *a = [r fetchRowAsArray]; - if([a count]) - numberOfRows = [[a objectAtIndex:0] integerValue]; - } - // Check for uniqueness via LIMIT numberOfRows-1,numberOfRows for speed - if(numberOfRows > 0) { - [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM %@ GROUP BY %@ LIMIT %ld,%ld", [selectedTable backtickQuotedString], [primaryKeyFieldNames componentsJoinedAndBacktickQuoted], (long)(numberOfRows-1), (long)numberOfRows]]; - if([mySQLConnection affectedRows] == 0) - primaryKeyFieldNames = nil; - } else { - primaryKeyFieldNames = nil; - } - } - - if(primaryKeyFieldNames == nil) { - // delete row by row - while (index != NSNotFound) { - - wherePart = [NSString stringWithString:[self argumentForRow:index]]; + columnNames = [tableDataInstance columnNames]; - //argumentForRow might return empty query, in which case we shouldn't execute the partial query - if([wherePart length]) { - [mySQLConnection queryString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@", [selectedTable backtickQuotedString], wherePart]]; + // Get the primary key if there is one + if ( !keys ) { + setLimit = NO; + keys = [[NSMutableArray alloc] init]; + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]]; + [theResult setReturnDataAsStrings:YES]; + if ([theResult numOfRows]) [theResult dataSeek:0]; + for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { + theRow = [theResult fetchRowAsDictionary]; + if ( [[theRow objectForKey:@"Key"] isEqualToString:@"PRI"] ) { + [keys addObject:[theRow objectForKey:@"Field"]]; + } + } + } + + // If there is no primary key, all the fields are used in the argument. + if ( ![keys count] ) { + [keys setArray:columnNames]; + setLimit = YES; + + // When the option to not show blob or text options is set, we have a problem - we don't have + // the right values to use in the WHERE statement. Throw an error if this is the case. + if ( [prefs boolForKey:SPLoadBlobsAsNeeded] && [self tableContainsBlobOrTextColumns] ) { + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, + NSLocalizedString(@"You can't hide blob and text fields when working with tables without index.", @"message of panel when trying to edit tables without index and with hidden blob/text fields")); + [keys removeAllObjects]; + [tableContentView deselectAll:self]; + return @""; + } + } - // Check for errors - if ( ![mySQLConnection affectedRows] || [mySQLConnection queryErrored]) { - // If error delete that index from selectedRows for reloading table if - // "ReloadAfterRemovingRow" is disbaled - if(!reloadAfterRemovingRow) - [selectedRows removeIndex:index]; - errors++; - } - } else { - if(!reloadAfterRemovingRow) - [selectedRows removeIndex:index]; - errors++; - } - index = [selectedRows indexGreaterThanIndex:index]; - } - } else if ([primaryKeyFieldNames count] == 1) { - // if table has only one PRIMARY KEY - // delete the fast way by using the PRIMARY KEY in an IN clause - NSMutableString *deleteQuery = [NSMutableString string]; - NSInteger affectedRows = 0; + // Walk through the keys list constructing the argument list + for ( i = 0 ; i < [keys count] ; i++ ) { + if ( i ) + [argument appendString:@" AND "]; - [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; + // Use the selected row if appropriate + if ( row >= 0 ) { + tempValue = [tableValues cellDataAtRow:row column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; - while (index != NSNotFound) { + // Otherwise use the oldRow + } else { + tempValue = [oldRow objectAtIndex:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(keys, i)] objectForKey:@"datacolumnindex"] integerValue]]; + } - id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:NSArrayObjectAtIndex(primaryKeyFieldNames,0)] objectForKey:@"datacolumnindex"] integerValue]]; + if ( [tempValue isNSNull] ) { + [argument appendString:[NSString stringWithFormat:@"%@ IS NULL", [NSArrayObjectAtIndex(keys, i) backtickQuotedString]]]; + } else if ( [tempValue isSPNotLoaded] ) { + NSLog(@"Exceptional case: SPNotLoaded object found for method “argumentForRow:”!"); + return @""; + } else { - if([keyValue isKindOfClass:[NSData class]]) - [deleteQuery appendString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:keyValue]]]; - else - [deleteQuery appendString:[NSString stringWithFormat:@"'%@'", [keyValue description]]]; + if ( [tempValue isKindOfClass:[NSData class]] ) + [value setString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:tempValue]]]; + else + [value setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:tempValue]]]; + + [argument appendString:[NSString stringWithFormat:@"%@ = %@", [NSArrayObjectAtIndex(keys, i) backtickQuotedString], value]]; + } + } + + if (setLimit) [argument appendString:@" LIMIT 1"]; + + return argument; +} - // Split deletion query into 256k chunks - if([deleteQuery length] > 256000) { - [deleteQuery appendString:@")"]; - [mySQLConnection queryString:deleteQuery]; - // Remember affected rows for error checking - affectedRows += [mySQLConnection affectedRows]; - // Reinit a new deletion query - [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (", [selectedTable backtickQuotedString], [NSArrayObjectAtIndex(primaryKeyFieldNames,0) backtickQuotedString]]]; - } else { - [deleteQuery appendString:@","]; - } - index = [selectedRows indexGreaterThanIndex:index]; - } +/* + * Returns YES if the table contains any columns which are of any of the blob or text types, + * NO otherwise. + */ +- (BOOL)tableContainsBlobOrTextColumns +{ + NSInteger i; - // Check if deleteQuery's maximal length was reached for the last index - // if yes omit the empty query - if(![deleteQuery hasSuffix:@"("]) { - // Replace final , by ) and delete the remaining rows - [deleteQuery setString:[NSString stringWithFormat:@"%@)", [deleteQuery substringToIndex:([deleteQuery length]-1)]]]; - [mySQLConnection queryString:deleteQuery]; - // Remember affected rows for error checking - affectedRows += [mySQLConnection affectedRows]; - } - - errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; - } else { - // if table has more than one PRIMARY KEY - // delete the row by using all PRIMARY KEYs in an OR clause - NSMutableString *deleteQuery = [NSMutableString string]; - NSInteger affectedRows = 0; - - [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; - - while (index != NSNotFound) { - - // Build the AND clause of PRIMARY KEYS - [deleteQuery appendString:@"("]; - for(NSString *primaryKeyFieldName in primaryKeyFieldNames) { + for ( i = 0 ; i < [dataColumns count]; i++ ) { + if ( [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { + return YES; + } + } - id keyValue = [tableValues cellDataAtRow:index column:[[[tableDataInstance columnWithName:primaryKeyFieldName] objectForKey:@"datacolumnindex"] integerValue]]; - - [deleteQuery appendString:[primaryKeyFieldName backtickQuotedString]]; - if ([keyValue isKindOfClass:[NSData class]]) { - [deleteQuery appendString:@"=X'"]; - [deleteQuery appendString:[mySQLConnection prepareBinaryData:keyValue]]; - } else { - [deleteQuery appendString:@"='"]; - [deleteQuery appendString:[mySQLConnection prepareString:[keyValue description]]]; - } - [deleteQuery appendString:@"' AND "]; - } - - // Remove the trailing AND and add the closing bracket - [deleteQuery deleteCharactersInRange:NSMakeRange([deleteQuery length]-5, 5)]; - [deleteQuery appendString:@")"]; - - // Split deletion query into 64k chunks - if([deleteQuery length] > 64000) { - [mySQLConnection queryString:deleteQuery]; - // Remember affected rows for error checking - affectedRows += [mySQLConnection affectedRows]; - // Reinit a new deletion query - [deleteQuery setString:[NSString stringWithFormat:@"DELETE FROM %@ WHERE ", [selectedTable backtickQuotedString]]]; - } else { - [deleteQuery appendString:@" OR "]; - } - - index = [selectedRows indexGreaterThanIndex:index]; - } - - // Check if deleteQuery's maximal length was reached for the last index - // if yes omit the empty query - if(![deleteQuery hasSuffix:@"WHERE "]) { - // Remove final ' OR ' and delete the remaining rows - [deleteQuery setString:[deleteQuery substringToIndex:([deleteQuery length]-4)]]; - [mySQLConnection queryString:deleteQuery]; - // Remember affected rows for error checking - affectedRows += [mySQLConnection affectedRows]; - } - - errors = (affectedRows > 0) ? [selectedRows count] - affectedRows : [selectedRows count]; - } - - // Restore Console Log window's updating bahaviour - [[SPQueryController sharedQueryController] setAllowConsoleUpdate:consoleUpdateStatus]; - - if (errors) { - NSArray *message; - //TODO: The following three messages are NOT localisable! - if (errors < 0) { - message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), - [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ more %@ deleted! Please check the Console and inform the Sequel Pro team!", @"message of panel when more rows were deleted"), (long)(errors*-1), ((errors*-1)>1)?@"s":@"", (errors>1)?@"were":@"was"], - nil]; - } - else { - if (primaryKeyFieldNames == nil) - message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), - [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and use a primary key for your table.", @"message of panel when not all selected fields have been deleted"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s"], - nil]; - else - message = [NSArray arrayWithObjects:NSLocalizedString(@"Warning", @"warning"), - [NSString stringWithFormat:NSLocalizedString(@"%ld row%@ ha%@ not been deleted. Reload the table to be sure that the rows exist and check the Console for possible errors inside the primary key%@ for your table.", @"message of panel when not all selected fields have been deleted by using primary keys"), (long)errors, (errors>1)?@"s":@"", (errors>1)?@"ve":@"s", (errors>1)?@"s":@""], - nil]; - } - - [self performSelector:@selector(showErrorSheetWith:) - withObject:message - afterDelay:0.3]; - } + return NO; +} - // Refresh table content - if ( errors || reloadAfterRemovingRow ) { - previousTableRowsCount = tableRowsCount; - [self loadTableValues]; - } else { - for ( i = tableRowsCount - 1 ; i >= 0 ; i-- ) { - if ([selectedRows containsIndex:i]) [tableValues removeRowAtIndex:i]; - } - tableRowsCount = [tableValues count]; - [tableContentView reloadData]; +/* + * Returns a string controlling which fields to retrieve for a query. Returns * (all fields) if the preferences + * option dontShowBlob isn't set; otherwise, returns a comma-separated list of all non-blob/text fields. + */ +- (NSString *)fieldListForQuery +{ + NSInteger i; + NSMutableArray *fields = [NSMutableArray array]; + + if (([prefs boolForKey:SPLoadBlobsAsNeeded]) && ([dataColumns count] > 0)) { + + NSArray *columnNames = [tableDataInstance columnNames]; + + for (i = 0 ; i < [columnNames count]; i++) + { + if (![tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]] ) { + [fields addObject:[NSArrayObjectAtIndex(columnNames, i) backtickQuotedString]]; + } + else { + // For blob/text fields, select a null placeholder so the column count is still correct + [fields addObject:@"NULL"]; } - [tableContentView deselectAll:self]; - } else { - // The user clicked cancel in the "sure you wanna delete" message - // restore editing or whatever } + return [fields componentsJoinedByString:@","]; + } + else { + return @"*"; } } +/* + * Close an open sheet. + */ +- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo +{ + [sheet orderOut:self]; +} + /** * Show Error sheet (can be called from inside of a endSheet selector) * via [self performSelector:@selector(showErrorSheetWithTitle:) withObject: afterDelay:] @@ -2449,7 +2465,7 @@ { // error := first object is the title , second the message, only one button OK SPBeginAlertSheet([error objectAtIndex:0], NSLocalizedString(@"OK", @"OK button"), - nil, nil, tableWindow, self, nil, nil, nil, + nil, nil, tableWindow, self, nil, nil, [error objectAtIndex:1]); } @@ -2926,7 +2942,7 @@ [self loadTableValues]; if ([mySQLConnection queryErrored]) { - SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Couldn't sort table. MySQL said: %@", @"message of panel when sorting of table failed"), [mySQLConnection getLastErrorMessage]]); [tableDocumentInstance endTask]; [sortPool drain]; @@ -3021,7 +3037,7 @@ MCPResult *tempResult = [mySQLConnection queryString:query]; if (![tempResult numOfRows]) { - SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, + SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, NSLocalizedString(@"Couldn't load the row. Reload the table to be sure that the row exists and use a primary key for your table.", @"message of panel when loading of row failed")); return NO; } -- cgit v1.2.3