diff options
author | rowanbeentje <rowan@beent.je> | 2010-01-30 21:41:18 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2010-01-30 21:41:18 +0000 |
commit | a776a6437e40837cc2e20a9231e3eb8ef1a73ddf (patch) | |
tree | c82215d051a4529a2608a2880526696ba041c263 | |
parent | 12b199d998018affa4e358b0d4e26f7cd878f8cd (diff) | |
download | sequelpro-a776a6437e40837cc2e20a9231e3eb8ef1a73ddf.tar.gz sequelpro-a776a6437e40837cc2e20a9231e3eb8ef1a73ddf.tar.bz2 sequelpro-a776a6437e40837cc2e20a9231e3eb8ef1a73ddf.zip |
Rework CMCopyTable copy functionality and related code:
- Reverse the effects of the r1664/r1690 combination, restoring the original link from CMCopyTable to the underlying data source
- Adapt the code to retrieve information from the new SPDataStorage; update SPDataStorage as necessary if underlying data changes
- Refactor and speed up various checks and loops for speed and logic improvements
- Ensure the binary-encoded TEXT data is hex encoded rather than retrieved as "<123ABC 123ABC...>" NSData representations
- Fix some memory leaks
This resolves Issue #533 and the original "Copy as SQL" issue
-rw-r--r-- | Source/CMCopyTable.h | 12 | ||||
-rw-r--r-- | Source/CMCopyTable.m | 394 | ||||
-rw-r--r-- | Source/CustomQuery.m | 2 | ||||
-rw-r--r-- | Source/TableContent.m | 3 |
4 files changed, 211 insertions, 200 deletions
diff --git a/Source/CMCopyTable.h b/Source/CMCopyTable.h index 95dd0c4b..03fc53cb 100644 --- a/Source/CMCopyTable.h +++ b/Source/CMCopyTable.h @@ -25,6 +25,8 @@ #import <AppKit/AppKit.h> #import "SPTableView.h" +@class SPDataStorage; + /*! @class copyTable @abstract subclassed NSTableView to implement copy & drag-n-drop @@ -39,7 +41,8 @@ id mySQLConnection; // current MySQL connection NSArray* columnDefinitions; // array of NSDictionary containing info about columns NSString* selectedTable; // the name of the current selected table - + SPDataStorage* tableStorage; // the underlying storage array holding the table data + NSUserDefaults *prefs; } @@ -102,7 +105,12 @@ /* * Set all necessary data from the table content view. */ -- (void)setTableInstance:(id)anInstance withColumns:(NSArray *)columnDefs withTableName:(NSString *)aTableName withConnection:(id)aMySqlConnection; +- (void)setTableInstance:(id)anInstance withTableData:(SPDataStorage *)theTableStorage withColumns:(NSArray *)columnDefs withTableName:(NSString *)aTableName withConnection:(id)aMySqlConnection; + +/* + * Update the table storage location if necessary. + */ +- (void)setTableData:(SPDataStorage *)theTableStorage; @end diff --git a/Source/CMCopyTable.m b/Source/CMCopyTable.m index 621fc08f..3a929a6e 100644 --- a/Source/CMCopyTable.m +++ b/Source/CMCopyTable.m @@ -31,6 +31,7 @@ #import "CustomQuery.h" #import "SPNotLoaded.h" #import "SPConstants.h" +#import "SPDataStorage.h" NSInteger MENU_EDIT_COPY_WITH_COLUMN = 2001; NSInteger MENU_EDIT_COPY_AS_SQL = 2002; @@ -39,8 +40,6 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; - (void)copy:(id)sender { - prefs = [[NSUserDefaults standardUserDefaults] retain]; - NSString *tmp = nil; if([sender tag] == MENU_EDIT_COPY_AS_SQL) { @@ -95,78 +94,68 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; //the value in each field is from the objects description method - (NSString *)selectedRowsAsTabStringWithHeaders:(BOOL)withHeaders { - if ( [self numberOfSelectedRows] > 0 ) - { - - NSIndexSet *selectedRows = [self selectedRowIndexes]; + if ([self numberOfSelectedRows] == 0) return nil; + + NSIndexSet *selectedRows = [self selectedRowIndexes]; - NSArray *columns = [self tableColumns]; - NSUInteger numColumns = [columns count]; - id dataSource = [self dataSource]; - - NSMutableString *result = [NSMutableString stringWithCapacity:numColumns]; - - if(withHeaders) { - NSUInteger i; - for( i = 0; i < numColumns; i++ ){ - [result appendString:[NSString stringWithFormat:@"%@\t", [[NSArrayObjectAtIndex(columns, i) headerCell] stringValue]]]; - } - [result appendString:[NSString stringWithFormat:@"\n"]]; + NSArray *columns = [self tableColumns]; + NSUInteger numColumns = [columns count]; + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; + + // Add the table headers if requested to do so + if (withHeaders) { + NSUInteger i; + for( i = 0; i < numColumns; i++ ){ + [result appendString:[NSString stringWithFormat:@"%@\t", [[NSArrayObjectAtIndex(columns, i) headerCell] stringValue]]]; } + [result appendString:[NSString stringWithFormat:@"\n"]]; + } - NSUInteger c; + NSUInteger c; + id cellData = nil; - id rowData = nil; - NSTableColumn *col = nil; - - NSUInteger rowIndex = [selectedRows firstIndex]; - - while ( rowIndex != NSNotFound ) - { - rowData = nil; - for ( c = 0; c < numColumns; c++) - { - col = NSArrayObjectAtIndex(columns, c); - rowData = [dataSource tableView:self - objectValueForTableColumn:col - row:rowIndex ]; - - if ( nil != rowData ) - { - if ([rowData isNSNull]) - [result appendString:[NSString stringWithFormat:@"%@\t", [prefs objectForKey:SPNullValue]]]; - else if ([rowData isSPNotLoaded]) - [result appendString:[NSString stringWithFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]]; - else - [result appendString:[NSString stringWithFormat:@"%@\t", [rowData description] ] ]; - } - else - { - [result appendString:@"\t"]; - } - } //end for each column + // Create an array of table column mappings for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + } + + // Loop through the rows, adding their descriptive contents + NSUInteger rowIndex = [selectedRows firstIndex]; + while ( rowIndex != NSNotFound ) + { + for ( c = 0; c < numColumns; c++) { + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); - if ( [result length] ) - { - [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; + if (cellData) { + if ([cellData isNSNull]) + [result appendString:[NSString stringWithFormat:@"%@\t", [prefs objectForKey:SPNullValue]]]; + else if ([cellData isSPNotLoaded]) + [result appendString:[NSString stringWithFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]]; + else + [result appendString:[NSString stringWithFormat:@"%@\t", [cellData description]]]; + } else { + [result appendString:@"\t"]; } - [result appendString: [ NSString stringWithFormat:@"\n"]]; - - // next selected row - rowIndex = [selectedRows indexGreaterThanIndex: rowIndex]; - - } //end for each row + } - if ( [result length] ) - { + if ([result length]){ [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; } - return result; + [result appendString:[NSString stringWithFormat:@"\n"]]; + + // Select the next row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; } - else - { - return nil; + + // Remove the trailing line end + if ([result length]) { + [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; } + + free(columnMappings); + + return result; } /* @@ -184,134 +173,121 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; NSIndexSet *selectedRows = [self selectedRowIndexes]; NSMutableString *value = [NSMutableString stringWithCapacity:10]; - NSArray *dbDataRow; - NSMutableArray *columnMappings; - id rowData = nil; + id cellData = nil; NSUInteger rowCounter = 0; NSUInteger penultimateRowIndex = [selectedRows count]; NSUInteger c; NSUInteger valueLength = 0; - NSMutableString *result = [NSMutableString stringWithCapacity:numColumns]; + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; - // Create array of types according to the column order - NSMutableArray *types = [NSMutableArray arrayWithCapacity:numColumns]; // Create an array of table column names NSMutableArray *tbHeader = [NSMutableArray arrayWithCapacity:numColumns]; - for(id enumObj in columns) - { + for (id enumObj in columns) { [tbHeader addObject:[[enumObj headerCell] stringValue]]; - NSString *t = [[columnDefinitions objectAtIndex:[[enumObj identifier] integerValue]] objectForKey:@"typegrouping"]; - if([t isEqualToString:@"bit"] || [t isEqualToString:@"integer"] || [t isEqualToString:@"float"]) - [types addObject:[NSNumber numberWithInteger:0]]; // numeric - else if([t isEqualToString:@"blobdata"]) - [types addObject:[NSNumber numberWithInteger:2]]; // blob data - else if([t isEqualToString:@"textdata"]) - [types addObject:[NSNumber numberWithInteger:3]]; // long text data + } + + // Create arrays of table column mappings and types for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + NSUInteger *columnTypes = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + + NSString *t = [[columnDefinitions objectAtIndex:columnMappings[c]] objectForKey:@"typegrouping"]; + + // Numeric data + if ([t isEqualToString:@"bit"] || [t isEqualToString:@"integer"] || [t isEqualToString:@"float"]) + columnTypes[c] = 0; + + // Blob data or long text data + else if ([t isEqualToString:@"blobdata"] || [t isEqualToString:@"textdata"]) + columnTypes[c] = 2; + + // Default to strings else - [types addObject:[NSNumber numberWithInteger:1]]; // string (fallback coevally) + columnTypes[c] = 1; } + + // Begin the SQL string [result appendString:[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES\n", [(selectedTable == nil)?@"<table>":selectedTable backtickQuotedString], [tbHeader componentsJoinedAndBacktickQuoted]]]; - - // Set up an array of table column mappings - columnMappings = [[NSMutableArray alloc] initWithCapacity:numColumns]; - for ( c = 0; c < numColumns; c++ ) { - [columnMappings addObject:[[columns objectAtIndex:c] identifier]]; - } NSUInteger rowIndex = [selectedRows firstIndex]; NSTableColumn *col = nil; - while ( rowIndex != NSNotFound ) - { + { [value appendString:@"\t("]; - rowData = nil; + cellData = nil; rowCounter++; for ( c = 0; c < numColumns; c++ ) { - col = NSArrayObjectAtIndex(columns, c); - rowData = [dataSource tableView:self - objectValueForTableColumn:col - row:rowIndex ]; + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); + + // If the data is not loaded, attempt to fetch the value + if ([cellData isSPNotLoaded] && [[self delegate] isKindOfClass:[TableContent class]]) { + + // Abort if no table name given, not table content, or if there are no indices on this table + if (!selectedTable || ![[self delegate] isKindOfClass:[TableContent class]] || ![[tableInstance argumentForRow:rowIndex] length]) { + NSBeep(); + free(columnMappings); + free(columnTypes); + return nil; + } + + // Use the argumentForRow to retrieve the missing information + // TODO - this could be preloaded for all selected rows rather than cell-by-cell + cellData = [mySQLConnection getFirstFieldFromQuery: + [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@", + [[tbHeader objectAtIndex:columnMappings[c]] backtickQuotedString], + [selectedTable backtickQuotedString], + [tableInstance argumentForRow:rowIndex]]]; + } // Check for NULL value - if([rowData isNSNull]) { + if ([cellData isNSNull]) { [value appendString:@"NULL, "]; continue; - } - else if ( rowData != nil ) { - // check column type and insert the data accordingly - switch([[types objectAtIndex:c] integerValue]) { - case 0: // numeric - [value appendString:[NSString stringWithFormat:@"%@, ", [rowData description]]]; - break; - case 1: // string - if ([rowData isKindOfClass:[NSData class]]) { - [value appendString:[NSString stringWithFormat:@"X'%@', ", - [mySQLConnection prepareBinaryData:rowData]]]; - } else { - [value appendString:[NSString stringWithFormat:@"'%@', ", - [mySQLConnection prepareString:[rowData description]]]]; - } - break; - case 2: // blob - if (![[self delegate] isKindOfClass:[CustomQuery class]] && [rowData isSPNotLoaded]) { - - // Abort if there are no indices on this table or if there's no table name given. - if (![[tableInstance argumentForRow:rowIndex] length] || selectedTable == nil) { - [columnMappings release]; - return nil; - } - - //if we have indexes, use argumentForRow - dbDataRow = [[mySQLConnection queryString: - [NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@", - [selectedTable backtickQuotedString], [tableInstance argumentForRow:rowIndex]]] fetchRowAsArray]; - if([[dbDataRow objectAtIndex:[[columnMappings objectAtIndex:c] integerValue]] isNSNull]) - [value appendString:@"NULL, "]; - else - [value appendString:[NSString stringWithFormat:@"X'%@', ", - [mySQLConnection prepareBinaryData:[dbDataRow objectAtIndex:[[columnMappings objectAtIndex:c] integerValue]]]]]; - } else { - [value appendString:[NSString stringWithFormat:@"X'%@', ", [mySQLConnection prepareBinaryData:rowData]]]; - } + + } else if (cellData) { + + // Check column type and insert the data accordingly + switch(columnTypes[c]) { + + // Convert numeric types to unquoted strings + case 0: + [value appendString:[NSString stringWithFormat:@"%@, ", [cellData description]]]; break; - case 3: // long text data - if (![[self delegate] isKindOfClass:[CustomQuery class]] && [prefs boolForKey:SPLoadBlobsAsNeeded]) { - - // Abort if there are no indices on this table or if there's no table name given. - if (![[tableInstance argumentForRow:rowIndex] length] || selectedTable == nil) - return nil; - - //if we have indexes, use argumentForRow - dbDataRow = [[mySQLConnection queryString: - [NSString stringWithFormat:@"SELECT * FROM %@ WHERE %@", - [selectedTable backtickQuotedString], [tableInstance argumentForRow:rowIndex]]] fetchRowAsArray]; - if([[dbDataRow objectAtIndex:[[columnMappings objectAtIndex:c] integerValue]] isKindOfClass:[NSNull class]]) - [value appendString:@"NULL, "]; - else - [value appendString:[NSString stringWithFormat:@"'%@', ", - [mySQLConnection prepareString:[[dbDataRow objectAtIndex:[[columnMappings objectAtIndex:c] integerValue]] description]]]]; + + // Quote string, text and blob types appropriately + case 1: + case 2: + if ([cellData isKindOfClass:[NSData class]]) { + [value appendString:[NSString stringWithFormat:@"X'%@', ", [mySQLConnection prepareBinaryData:cellData]]]; } else { - [value appendString:[NSString stringWithFormat:@"'%@', ", - [[rowData description] stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"] ] ]; + [value appendString:[NSString stringWithFormat:@"'%@', ", [mySQLConnection prepareString:[cellData description]]]]; } break; + + // Unhandled cases - abort default: - [columnMappings release]; + NSBeep(); + free(columnMappings); + free(columnTypes); return nil; } - } - else - // TODO is this necessary? or better to return nil? - [value appendString:@"'', "]; - } //end for each column + // If nil is encountered, abort + } else { + NSBeep(); + free(columnMappings); + free(columnTypes); + return nil; + } + } - // delete last ', ' + // Remove the trailing ', ' from the query if ( [value length] > 2 ) [value deleteCharactersInRange:NSMakeRange([value length]-2, 2)]; @@ -319,6 +295,7 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; // Close this VALUES group and set up the next one if appropriate if ( rowCounter != penultimateRowIndex ) { + // Add a new INSERT starter command every ~250k of data. if ( valueLength > 250000 ) { [result appendString:value]; @@ -329,23 +306,25 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; } else { [value appendString:@"),\n"]; } + } else { [value appendString:@"),\n"]; [result appendString:value]; } - // next selected row - rowIndex = [selectedRows indexGreaterThanIndex: rowIndex]; + // Get the next selected row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; - } //end for each row + } - // delete last ",/n" + // Remove the trailing ",\n" from the query string if ( [result length] > 3 ) [result deleteCharactersInRange:NSMakeRange([result length]-2, 2)]; [result appendString:@";\n"]; - - [columnMappings release]; + + free(columnMappings); + free(columnTypes); return result; } @@ -360,73 +339,78 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; NSIndexSet *selectedRows = [self selectedRowIndexes]; id dataSource = [self dataSource]; - NSMutableString *result = [NSMutableString stringWithCapacity:numColumns]; - + NSMutableString *result = [NSMutableString stringWithCapacity:2000]; NSUInteger c; - - id rowData = nil; - NSTableColumn *col = nil; + id cellData = nil; - NSUInteger rowIndex = [selectedRows firstIndex]; + // Create an array of table column mappings for fast iteration + NSUInteger *columnMappings = malloc(numColumns * sizeof(NSUInteger)); + for ( c = 0; c < numColumns; c++) { + columnMappings[c] = [[[columns objectAtIndex:c] identifier] unsignedIntValue]; + } + // Loop through the rows, adding their descriptive contents + NSUInteger rowIndex = [selectedRows firstIndex]; while ( rowIndex != NSNotFound ) { - rowData = nil; - for ( c = 0; c < numColumns; c++) - { - col = [columns objectAtIndex:c]; - rowData = [dataSource tableView:self - objectValueForTableColumn:col - row:rowIndex ]; + for ( c = 0; c < numColumns; c++) { + cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]); - if ( nil != rowData ) - { - if ([rowData isNSNull]) + if (cellData) { + if ([cellData isNSNull]) [result appendString:[NSString stringWithFormat:@"%@\t", [prefs objectForKey:SPNullValue]]]; - else if ([rowData isSPNotLoaded]) + else if ([cellData isSPNotLoaded]) [result appendString:[NSString stringWithFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")]]; else - [result appendString:[NSString stringWithFormat:@"%@\t", [rowData description] ] ]; - } - else - { + [result appendString:[NSString stringWithFormat:@"%@\t", [cellData description]]]; + } else { [result appendString:@"\t"]; } - } //end for each column - - if ( [result length] ) - { + } + + if ([result length]) { [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; } - [result appendString: [ NSString stringWithFormat:@"\n"]]; + + [result appendString:[NSString stringWithFormat:@"\n"]]; - // next selected row - rowIndex = [selectedRows indexGreaterThanIndex: rowIndex]; + // Retrieve the next selected row index + rowIndex = [selectedRows indexGreaterThanIndex:rowIndex]; + } - } //end for each row - - if ( [result length] ) - { + // Trim the trailing line ending + if ([result length]) { [result deleteCharactersInRange:NSMakeRange([result length]-1, 1)]; } + free(columnMappings); + return result; } -/* +/** * Init self with data coming from the table content view. Mainly used for copying data properly. */ -- (void)setTableInstance:(id)anInstance withColumns:(NSArray *)columnDefs withTableName:(NSString *)aTableName withConnection:(id)aMySqlConnection +- (void)setTableInstance:(id)anInstance withTableData:(SPDataStorage *)theTableStorage withColumns:(NSArray *)columnDefs withTableName:(NSString *)aTableName withConnection:(id)aMySqlConnection { selectedTable = aTableName; mySQLConnection = aMySqlConnection; tableInstance = anInstance; + tableStorage = theTableStorage; if (columnDefinitions) [columnDefinitions release]; - columnDefinitions = [[NSArray alloc] initWithArray:columnDefs]; } +/* + * Update the table storage location if necessary. + */ +- (void)setTableData:(SPDataStorage *)theTableStorage +{ + tableStorage = theTableStorage; +} + + - (void)keyDown:(NSEvent *)theEvent { // RETURN or ENTER invoke editing mode for selected row @@ -460,4 +444,22 @@ NSInteger MENU_EDIT_COPY_AS_SQL = 2002; [super keyDown:theEvent]; } +#pragma mark - + +- (void) awakeFromNib +{ + columnDefinitions = nil; + prefs = [[NSUserDefaults standardUserDefaults] retain]; + + [super awakeFromNib]; +} + +- (void) dealloc +{ + if (columnDefinitions) [columnDefinitions release]; + [prefs release]; + + [super dealloc]; +} + @end diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index 2a93f425..1bc06e48 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -854,7 +854,7 @@ } // Init copyTable with necessary information for copying selected rows as SQL INSERT - [customQueryView setTableInstance:self withColumns:cqColumnDefinition withTableName:resultTableName withConnection:mySQLConnection]; + [customQueryView setTableInstance:self withTableData:resultData withColumns:cqColumnDefinition withTableName:resultTableName withConnection:mySQLConnection]; //query finished [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; diff --git a/Source/TableContent.m b/Source/TableContent.m index e220a4b1..64996349 100644 --- a/Source/TableContent.m +++ b/Source/TableContent.m @@ -223,7 +223,7 @@ [tableContentView performSelectorOnMainThread:@selector(displayIfNeeded) withObject:nil waitUntilDone:NO]; // Init copyTable with necessary information for copying selected rows as SQL INSERT - [tableContentView setTableInstance:self withColumns:dataColumns withTableName:selectedTable withConnection:mySQLConnection]; + [tableContentView setTableInstance:self withTableData:tableValues withColumns:dataColumns withTableName:selectedTable withConnection:mySQLConnection]; // Post the notification that the query is finished [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:tableDocumentInstance]; @@ -531,6 +531,7 @@ pthread_mutex_lock(&tableValuesLock); tableRowsCount = 0; tableValues = [[SPDataStorage alloc] init]; + [tableContentView setTableData:tableValues]; pthread_mutex_unlock(&tableValuesLock); [tableValuesTransition release]; } |