diff options
author | rowanbeentje <rowan@beent.je> | 2013-08-13 23:49:31 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2013-08-13 23:49:31 +0000 |
commit | ef60b2022d50b99e6de78cc301bf71e8b336ae0e (patch) | |
tree | 175e38fc968dec070ca8a872f7b87502b62e8c82 /Source | |
parent | 80c152501303c0ed7bd530f5e05bc7e5a6fba7f5 (diff) | |
download | sequelpro-ef60b2022d50b99e6de78cc301bf71e8b336ae0e.tar.gz sequelpro-ef60b2022d50b99e6de78cc301bf71e8b336ae0e.tar.bz2 sequelpro-ef60b2022d50b99e6de78cc301bf71e8b336ae0e.zip |
Rework table content and custom query data loading and storage for speed increases and lower memory usage:
- Add a new SPMySQLStreamingResultStore class to SPMySQL.framework. This class acts as both a result set and a data store for the accompanying data, storing the row information in a custom format in a custom malloc zone.
- Amend SPDataStorage to wrap the new class, so original result information is stored in the one location in the custom format. Any edited information is handled by SPDataStorage for clean separation
- Rework table content and custom query data data stores to use the new class. This significantly speeds up data loading, resulting in faster data loads if they weren't previously network constrained, or lower CPU usage otherwise. The memory usage is also lowered, with the memory overhead for many small cells being enormously reduced.
Diffstat (limited to 'Source')
-rw-r--r-- | Source/SPCopyTable.m | 4 | ||||
-rw-r--r-- | Source/SPCustomQuery.h | 5 | ||||
-rw-r--r-- | Source/SPCustomQuery.m | 189 | ||||
-rw-r--r-- | Source/SPDataCellFormatter.h | 2 | ||||
-rw-r--r-- | Source/SPDataCellFormatter.m | 14 | ||||
-rw-r--r-- | Source/SPDataStorage.h | 68 | ||||
-rw-r--r-- | Source/SPDataStorage.m | 478 | ||||
-rw-r--r-- | Source/SPPreferencesUpgrade.m | 6 | ||||
-rw-r--r-- | Source/SPTableContent.h | 5 | ||||
-rw-r--r-- | Source/SPTableContent.m | 139 | ||||
-rw-r--r-- | Source/SPTableContentDataSource.m | 26 | ||||
-rw-r--r-- | Source/SPTableContentDelegate.m | 33 |
12 files changed, 474 insertions, 495 deletions
diff --git a/Source/SPCopyTable.m b/Source/SPCopyTable.m index 212a545c..0fa58711 100644 --- a/Source/SPCopyTable.m +++ b/Source/SPCopyTable.m @@ -800,8 +800,8 @@ static const NSInteger kBlobAsImageFile = 4; maxCellWidth = 0; for (i = 0; i < rowsToCheck; i += rowStep) { - // Retrieve the cell's content - contentString = [tableStorage cellDataAtRow:i column:columnIndex]; + // Retrieve part of the cell's content to get widths, topping out at a maximum length + contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500); // If the cell hasn't loaded yet, skip processing if (!contentString) diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h index c0e309af..528b8209 100644 --- a/Source/SPCustomQuery.h +++ b/Source/SPCustomQuery.h @@ -57,7 +57,7 @@ @class SPSplitView; @class SPFieldEditorController; @class SPMySQLConnection; -@class SPMySQLFastStreamingResult; +@class SPMySQLStreamingResultStore; @class SPTextView; #ifdef SP_CODA @@ -162,6 +162,7 @@ SPDataStorage *resultData; pthread_mutex_t resultDataLock; + NSCondition *resultLoadingCondition; NSInteger resultDataCount; NSArray *cqColumnDefinition; NSString *lastExecutedQuery; @@ -257,7 +258,7 @@ - (NSArray *)currentResult; - (NSArray *)currentDataResultWithNULLs:(BOOL)includeNULLs truncateDataFields:(BOOL)truncate; - (NSUInteger)currentResultRowCount; -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult; +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore; // Retrieving and setting table state - (void)updateTableView; diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m index 686e700b..27fc11c6 100644 --- a/Source/SPCustomQuery.m +++ b/Source/SPCustomQuery.m @@ -69,8 +69,7 @@ @interface SPCustomQuery (PrivateAPI) -- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column; -- (id)_convertResultDataValueToDisplayableRepresentation:(id)value whilePreservingNULLs:(BOOL)preserveNULLs truncateDataFields:(BOOL)truncate; +- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; + (NSString *)linkToHelpTopic:(NSString *)aTopic; @end @@ -581,7 +580,7 @@ { NSAutoreleasePool *queryRunningPool = [[NSAutoreleasePool alloc] init]; NSArray *queries = [taskArguments objectForKey:@"queries"]; - SPMySQLFastStreamingResult *streamingResult = nil; + SPMySQLStreamingResultStore *resultStore = nil; NSMutableString *errors = [NSMutableString string]; SEL callbackMethod = NULL; NSString *taskButtonString; @@ -652,8 +651,8 @@ [tempQueries addObject:query]; // Run the query, timing execution (note this also includes network and overhead) - streamingResult = [[mySQLConnection streamingQueryString:query] retain]; - executionTime += [streamingResult queryExecutionTime]; + resultStore = [[mySQLConnection resultStoreFromQueryString:query] retain]; + executionTime += [resultStore queryExecutionTime]; totalQueriesRun++; // If this is the last query, retrieve and store the result; otherwise, @@ -662,7 +661,7 @@ // Retrieve and cache the column definitions for the result array if (cqColumnDefinition) [cqColumnDefinition release]; - cqColumnDefinition = [[streamingResult fieldDefinitions] retain]; + cqColumnDefinition = [[resultStore fieldDefinitions] retain]; if(!reloadingExistingResult) { [[self onMainThread] updateTableView]; @@ -683,18 +682,18 @@ // Init copyTable with necessary information for copying selected rows as SQL INSERT [customQueryView setTableInstance:self withTableData:resultData withColumns:cqColumnDefinition withTableName:resultTableName withConnection:mySQLConnection]; - [self processResultIntoDataStorage:streamingResult]; + [self updateResultStore:resultStore]; } else { - [streamingResult cancelResultLoad]; + [resultStore cancelResultLoad]; } // Record any affected rows if ( [mySQLConnection rowsAffectedByLastQuery] != (unsigned long long)~0 ) totalAffectedRows += (NSUInteger)[mySQLConnection rowsAffectedByLastQuery]; - else if ( [streamingResult numberOfRows] ) - totalAffectedRows += (NSUInteger)[streamingResult numberOfRows]; + else if ( [resultStore numberOfRows] ) + totalAffectedRows += (NSUInteger)[resultStore numberOfRows]; - [streamingResult release]; + [resultStore release]; // Store any error messages if ([mySQLConnection queryErrored] || [mySQLConnection lastQueryWasCancelled]) { @@ -804,8 +803,8 @@ // Perform empty query if no query is given if ( !queryCount ) { - streamingResult = [mySQLConnection streamingQueryString:@""]; - [streamingResult cancelResultLoad]; + resultStore = [mySQLConnection resultStoreFromQueryString:@""]; + [resultStore cancelResultLoad]; [errors setString:[mySQLConnection lastErrorMessage]]; } @@ -944,55 +943,38 @@ } /** - * Processes a supplied streaming result set, loading it into the data array. + * Processes a supplied streaming result store, monitoring the load and updating + * the data displayed during download. */ -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore { - NSAutoreleasePool *dataLoadingPool; // Remove all items from the table resultDataCount = 0; [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; pthread_mutex_lock(&resultDataLock); [resultData removeAllRows]; + + // Add the new store + [resultData setDataStorage:theResultStore updatingExisting:NO]; pthread_mutex_unlock(&resultDataLock); - // Set the column count on the data store before setting up anything else - - // ensures that SPDataStorage is set up for timer-driven data loads - [resultData setColumnCount:[theResult numberOfFields]]; + // Start the data downloading + [theResultStore startDownload]; - // Set up the table updates timer + // Set up the table updates timer and wait for it to notify this thread about completion [[self onMainThread] initQueryLoadTimer]; - // Set up an autorelease pool for row processing - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - - // Loop through the result rows as they become available - for (NSArray *eachRow in theResult) { - - pthread_mutex_lock(&resultDataLock); - SPDataStorageAddRow(resultData, eachRow); - resultDataCount++; - pthread_mutex_unlock(&resultDataLock); - - // Drain and reset the autorelease pool every ~1024 rows - if (!(resultDataCount % 1024)) { - [dataLoadingPool drain]; - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - } + [resultLoadingCondition lock]; + while (![resultData dataDownloaded]) { + [resultLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; } - - // Clean up the interface update timer - [[self onMainThread] clearQueryLoadTimer]; + [resultLoadingCondition unlock]; // If the final column autoresize wasn't performed, perform it if (queryLoadLastRowCount < 200) [[self onMainThread] autosizeColumns]; [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:NO]; - [customQueryView setNeedsDisplay:YES]; - - // Clean up the autorelease pool - [dataLoadingPool drain]; } /** @@ -1483,11 +1465,20 @@ */ - (void) queryLoadUpdate:(NSTimer *)theTimer { + resultDataCount = [resultData count]; + if (queryLoadTimerTicksSinceLastUpdate < queryLoadInterfaceUpdateInterval) { queryLoadTimerTicksSinceLastUpdate++; return; } + if ([resultData dataDownloaded]) { + [resultLoadingCondition lock]; + [resultLoadingCondition signal]; + [self clearQueryLoadTimer]; + [resultLoadingCondition unlock]; + } + // Check whether a table update is required, based on whether new rows are // available to display. if (resultDataCount == (NSInteger)queryLoadLastRowCount) { @@ -1496,7 +1487,6 @@ // Update the table display [customQueryView noteNumberOfRowsChanged]; - if (!queryLoadLastRowCount) [customQueryView setNeedsDisplay:YES]; // Update column widths in two cases: on very first rows displayed, and once // more than 200 rows are present. @@ -1574,9 +1564,7 @@ while ((tableColumn = [enumerator nextObject])) { - id value = [self _resultDataItemAtRow:i columnIndex:[[tableColumn identifier] integerValue]]; - - [tempRow addObject:[self _convertResultDataValueToDisplayableRepresentation:value whilePreservingNULLs:includeNULLs truncateDataFields:truncate]]; + [tempRow addObject:[self _resultDataItemAtRow:i columnIndex:[[tableColumn identifier] integerValue] preserveNULLs:includeNULLs asPreview:truncate]]; } [currentResult addObject:[NSArray arrayWithArray:tempRow]]; @@ -1682,7 +1670,6 @@ [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; - [[dataCell formatter] setDisplayLimit:150]; // Set field length limit if field is a varchar to match varchar length if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"] @@ -2045,7 +2032,7 @@ } else { #endif // otherwise, just update the data in the data storage - SPDataStorageReplaceObjectAtRowAndColumn(resultData, rowIndex, [[aTableColumn identifier] intValue], anObject); + [resultData replaceObjectInRow:rowIndex column:[[aTableColumn identifier] intValue] withObject:anObject]; #ifndef SP_CODA } #endif @@ -2075,35 +2062,34 @@ { if (aTableView == customQueryView) { + if (![cell respondsToSelector:@selector(setTextColor:)]) { + return; + } + // For NULL cell's display the user's NULL value placeholder in grey to easily distinguish it from other values - if ([cell respondsToSelector:@selector(setTextColor:)]) { - - id value = nil; - NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; - - // While the table is being loaded, additional validation is required - data - // locks must be used to avoid crashes, and indexes higher than the available - // rows or columns may be requested. Use gray to show loading in these cases. - if (isWorking) { - pthread_mutex_lock(&resultDataLock); - - if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { - value = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); - } - - pthread_mutex_unlock(&resultDataLock); + BOOL showCellAsGray = NO; - if (!value) { - [cell setTextColor:[NSColor lightGrayColor]]; - return; - } - } - else { - value = SPDataStorageObjectAtRowAndColumn(resultData, rowIndex, columnIndex); + NSUInteger columnIndex = [[aTableColumn identifier] integerValue]; + + // While the table is being loaded, additional validation is required - data + // locks must be used to avoid crashes, and indexes higher than the available + // rows or columns may be requested. Use gray to show loading in these cases. + if (isWorking) { + pthread_mutex_lock(&resultDataLock); + + if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) { + showCellAsGray = [resultData cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; + } else { + showCellAsGray = YES; } - [cell setTextColor:[value isNSNull] ? [NSColor lightGrayColor] : [NSColor blackColor]]; + pthread_mutex_unlock(&resultDataLock); } + else { + showCellAsGray = [resultData cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; + } + + [cell setTextColor:showCellAsGray ? [NSColor lightGrayColor] : [NSColor blackColor]]; } } @@ -2113,8 +2099,7 @@ - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex { if (aTableView == customQueryView) { - - return [self _convertResultDataValueToDisplayableRepresentation:[self _resultDataItemAtRow:rowIndex columnIndex:[[tableColumn identifier] integerValue]] whilePreservingNULLs:NO truncateDataFields:YES]; + return [self _resultDataItemAtRow:rowIndex columnIndex:[[tableColumn identifier] integerValue] preserveNULLs:NO asPreview:YES]; } return @""; @@ -3768,6 +3753,7 @@ runPrimaryActionButtonAsSelection = nil; queryLoadTimer = nil; + resultLoadingCondition = [NSCondition new]; prefs = [NSUserDefaults standardUserDefaults]; @@ -3988,12 +3974,13 @@ /** * Retrieves the value from the underlying data storage at the supplied row and column indices. * - * @param row The row index - * @param column The column index + * @param row The row index + * @param column The column index + * @param preserveNULLs Whether t * * @return The value from the data storage */ -- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column +- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; { id value = nil; @@ -4005,7 +3992,11 @@ pthread_mutex_lock(&resultDataLock); if (row < resultDataCount && column < [resultData columnCount]) { - value = [[SPDataStorageObjectAtRowAndColumn(resultData, row, column) copy] autorelease]; + if (asPreview) { + value = SPDataStoragePreviewAtRowAndColumn(resultData, row, column, 150); + } else { + value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + } } pthread_mutex_unlock(&resultDataLock); @@ -4013,36 +4004,23 @@ if (!value) value = @"..."; } else { - value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + if (asPreview) { + value = SPDataStoragePreviewAtRowAndColumn(resultData, row, column, 150); + } else { + value = SPDataStorageObjectAtRowAndColumn(resultData, row, column); + } } - - return value; -} -/** - * Converts the supplied value into it's displayable representation. - * - * @param value The value to convert - * @param preserveNULLs Whether or not NULLs should be preserved or converted to the - * user's NULL placeholder preference. - * @param truncate Whether or not data fields should be truncates for display purposes. - * - * @return The converted value - */ -- (id)_convertResultDataValueToDisplayableRepresentation:(id)value whilePreservingNULLs:(BOOL)preserveNULLs truncateDataFields:(BOOL)truncate -{ + if ([value isKindOfClass:[SPMySQLGeometryData class]]) + return [value wktString]; + + if ([value isNSNull]) + return preserveNULLs ? value : [prefs objectForKey:SPNullValue]; + if ([value isKindOfClass:[NSData class]]) { - value = truncate ? [value shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]] : [value stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]; + return [value stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]; } - - if ([value isNSNull] && !preserveNULLs) { - value = [prefs objectForKey:SPNullValue]; - } - - if ([value isKindOfClass:[SPMySQLGeometryData class]]) { - value = [value wktString]; - } - + return value; } @@ -4057,6 +4035,7 @@ [NSObject cancelPreviousPerformRequestsWithTarget:customQueryView]; [self clearQueryLoadTimer]; + [resultLoadingCondition release]; [usedQuery release]; [lastExecutedQuery release]; [resultData release]; diff --git a/Source/SPDataCellFormatter.h b/Source/SPDataCellFormatter.h index d1f9c7d4..00cc85bb 100644 --- a/Source/SPDataCellFormatter.h +++ b/Source/SPDataCellFormatter.h @@ -33,12 +33,10 @@ @interface SPDataCellFormatter : NSFormatter { NSInteger textLimit; - NSUInteger displayLimit; NSString *fieldType; } @property (readwrite, assign) NSInteger textLimit; -@property (readwrite, assign) NSUInteger displayLimit; @property (readwrite, retain) NSString* fieldType; @end diff --git a/Source/SPDataCellFormatter.m b/Source/SPDataCellFormatter.m index 951fafbc..616e59db 100644 --- a/Source/SPDataCellFormatter.m +++ b/Source/SPDataCellFormatter.m @@ -36,24 +36,10 @@ @implementation SPDataCellFormatter @synthesize textLimit; -@synthesize displayLimit; @synthesize fieldType; -- (id)init -{ - if ((self = [super init])) { - displayLimit = NSNotFound; - } - return self; -} - - (NSString *)stringForObjectValue:(id)anObject { - // Truncate the string for speed purposes if it's very long - improves table scrolling speed. - if (displayLimit != NSNotFound && [anObject isKindOfClass:[NSString class]] && [(NSString *)anObject length] > displayLimit) { - return ([NSString stringWithFormat:@"%@...", [anObject substringToIndex:displayLimit - 3]]); - } - if (![anObject isKindOfClass:[NSString class]]) { return [anObject description]; } diff --git a/Source/SPDataStorage.h b/Source/SPDataStorage.h index 39d36d5f..d0c1f556 100644 --- a/Source/SPDataStorage.h +++ b/Source/SPDataStorage.h @@ -30,45 +30,54 @@ // // More info at <http://code.google.com/p/sequel-pro/> +#import <SPMySQL/SPMySQLStreamingResultStoreDelegate.h> + +@class SPMySQLStreamingResultStore; + /** - * This class provides a storage mechanism intended to represent tabular - * data, in a 2D array. Data can be added and retrieved either directly - * or via NSArrays; internally, C arrays are used to provide speed and - * memory improvements. - * This class is essentially mutable. + * This class wraps a SPMySQLStreamingResultStore, providing an editable + * data store; on a fresh load all data will be proxied from the underlying + * result store, but if cells or rows are edited, mutable rows are stored + * directly. */ -@interface SPDataStorage : NSObject +@interface SPDataStorage : NSObject <SPMySQLStreamingResultStoreDelegate> { - NSUInteger numColumns; - NSUInteger columnPointerByteSize; - NSUInteger numRows, numRowsCapacity; + SPMySQLStreamingResultStore *dataStorage; + NSPointerArray *editedRows; + BOOL *unloadedColumns; - id **dataStorage; + NSUInteger numberOfColumns; } +/* Setting result store */ +- (void) setDataStorage:(SPMySQLStreamingResultStore *) newDataStorage updatingExisting:(BOOL)updateExistingStore; + /* Retrieving rows and cells */ -- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)index; +- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex; - (id) cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; +- (id) cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength; +- (BOOL) cellIsNullOrUnloadedAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex; /* Adding and amending rows and cells */ -- (void) addRowWithContents:(NSArray *)row; -- (void) insertRowContents:(NSArray *)row atIndex:(NSUInteger)index; -- (void) replaceRowAtIndex:(NSUInteger)index withRowContents:(NSArray *)row; -- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)object; -- (void) removeRowAtIndex:(NSUInteger)index; +- (void) addRowWithContents:(NSMutableArray *)aRow; +- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex; +- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow; +- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)anObject; +- (void) removeRowAtIndex:(NSUInteger)anIndex; - (void) removeRowsInRange:(NSRange)rangeToRemove; - (void) removeAllRows; +/* Unloaded columns */ +- (void) setColumnAsUnloaded:(NSUInteger)columnIndex; + /* Basic information */ - (NSUInteger) count; -- (void) setColumnCount:(NSUInteger)columnCount; - (NSUInteger) columnCount; +- (BOOL) dataDownloaded; -/* Initialisation and teardown */ -#pragma mark - -- (id) init; -- (void) dealloc; +/* Delegate callback methods */ +- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore; @end @@ -91,14 +100,6 @@ static inline void SPDataStorageReplaceRow(SPDataStorage* self, NSUInteger rowIn SPDSReplaceRow(self, @selector(replaceRowAtIndex:withRowContents:), rowIndex, row); } -static inline void SPDataStorageReplaceObjectAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex, id newObject) -{ - typedef void (*SPDSObjectReplaceMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger, id); - static SPDSObjectReplaceMethodPtr SPDSObjectReplace; - if (!SPDSObjectReplace) SPDSObjectReplace = (SPDSObjectReplaceMethodPtr)[self methodForSelector:@selector(replaceObjectInRow:column:withObject:)]; - SPDSObjectReplace(self, @selector(replaceObjectInRow:column:withObject:), rowIndex, colIndex, newObject); -} - static inline id SPDataStorageObjectAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex) { typedef id (*SPDSObjectFetchMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger); @@ -106,3 +107,12 @@ static inline id SPDataStorageObjectAtRowAndColumn(SPDataStorage* self, NSUInteg if (!SPDSObjectFetch) SPDSObjectFetch = (SPDSObjectFetchMethodPtr)[self methodForSelector:@selector(cellDataAtRow:column:)]; return SPDSObjectFetch(self, @selector(cellDataAtRow:column:), rowIndex, colIndex); } + +static inline id SPDataStoragePreviewAtRowAndColumn(SPDataStorage* self, NSUInteger rowIndex, NSUInteger colIndex, NSUInteger previewLength) +{ + typedef id (*SPDSPreviewFetchMethodPtr)(SPDataStorage*, SEL, NSUInteger, NSUInteger, NSUInteger); + static SPDSPreviewFetchMethodPtr SPDSPreviewFetch; + if (!SPDSPreviewFetch) SPDSPreviewFetch = (SPDSPreviewFetchMethodPtr)[self methodForSelector:@selector(cellPreviewAtRow:column:previewLength:)]; + return SPDSPreviewFetch(self, @selector(cellPreviewAtRow:column:previewLength:), rowIndex, colIndex, previewLength); +} + diff --git a/Source/SPDataStorage.m b/Source/SPDataStorage.m index 1b3d1cba..c2119032 100644 --- a/Source/SPDataStorage.m +++ b/Source/SPDataStorage.m @@ -31,45 +31,89 @@ // More info at <http://code.google.com/p/sequel-pro/> #import "SPDataStorage.h" +#import "SPObjectAdditions.h" +#import <SPMySQL/SPMySQLStreamingResultStore.h> -@interface SPDataStorage (PrivateAPI) +@interface SPDataStorage (Private_API) -- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows; -- (void) _increaseCapacity; +- (void) _checkNewRow:(NSMutableArray *)aRow; @end @implementation SPDataStorage -static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorage* self, NSUInteger numExtraRows) +static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore, NSUInteger rowIndex) { - typedef void (*SPDSEnsureCapacityMethodPtr)(SPDataStorage*, SEL, NSUInteger); - static SPDSEnsureCapacityMethodPtr SPDSEnsureCapacity; - if (!SPDSEnsureCapacity) SPDSEnsureCapacity = (SPDSEnsureCapacityMethodPtr)[self methodForSelector:@selector(_ensureCapacityForAdditionalRowCount:)]; - SPDSEnsureCapacity(self, @selector(_ensureCapacityForAdditionalRowCount:), numExtraRows); + typedef NSMutableArray* (*SPDSGetEditedRowMethodPtr)(NSPointerArray*, SEL, NSUInteger); + static SPDSGetEditedRowMethodPtr SPDSGetEditedRow; + if (!SPDSGetEditedRow) SPDSGetEditedRow = (SPDSGetEditedRowMethodPtr)[rowStore methodForSelector:@selector(pointerAtIndex:)]; + return SPDSGetEditedRow(rowStore, @selector(pointerAtIndex:), rowIndex); } +#pragma mark - Setting result store + +/** + * Set the underlying MySQL data storage. + * This will clear all edited rows and unloaded column tracking. + */ +- (void) setDataStorage:(SPMySQLStreamingResultStore *)newDataStorage updatingExisting:(BOOL)updateExistingStore +{ + NSUInteger i; + [editedRows release], editedRows = nil; + if (unloadedColumns) free(unloadedColumns), unloadedColumns = NULL; + + if (dataStorage) { + + // If the table is reloading data, link to the current data store for smoother loads + if (updateExistingStore) { + [newDataStorage replaceExistingResultStore:dataStorage]; + } + + [dataStorage release], dataStorage = nil; + } + + dataStorage = [newDataStorage retain]; + [dataStorage setDelegate:self]; + + numberOfColumns = [dataStorage numberOfFields]; + editedRows = [NSPointerArray new]; + if ([dataStorage dataDownloaded]) { + [self resultStoreDidFinishLoadingData:dataStorage]; + } + + unloadedColumns = malloc(numberOfColumns * sizeof(BOOL)); + for (i = 0; i < numberOfColumns; i++) { + unloadedColumns[i] = NO; + } +} + + #pragma mark - #pragma mark Retrieving rows and cells /** * Return a mutable array containing the data for a specified row. */ -- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)index +- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex { - // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; + // If an edited row exists for the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, anIndex); + if (editedRow != NULL) { + return editedRow; + } - // Construct the NSMutableArray - NSMutableArray *rowArray = [NSMutableArray arrayWithCapacity:numColumns]; - id *row = dataStorage[index]; - NSUInteger i; - for (i = 0; i < numColumns; i++) { - CFArrayAppendValue((CFMutableArrayRef)rowArray, row[i]); + // Otherwise, prepare to return the underlying storage row + NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex); + + // Modify unloaded cells as appropriate + for (NSUInteger i = 0; i < numberOfColumns; i++) { + if (unloadedColumns[i]) { + CFArraySetValueAtIndex((CFMutableArrayRef)dataArray, i, [SPNotLoaded notLoaded]); + } } - return rowArray; + return dataArray; } /** @@ -78,11 +122,78 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag - (id) cellDataAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex { - // Throw an exception if the row or column index is out of bounds - if (rowIndex >= numRows || columnIndex >= numColumns) [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", (unsigned long long)rowIndex, (unsigned long long)columnIndex, (unsigned long long)numRows, (unsigned long long)numColumns]; + // If an edited row exists at the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + return CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex); + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + // If the specified column is not loaded, return a SPNotLoaded reference + if (unloadedColumns[columnIndex]) { + return [SPNotLoaded notLoaded]; + } // Return the content - return dataStorage[rowIndex][columnIndex]; + return SPMySQLResultStoreObjectAtRowAndColumn(dataStorage, rowIndex, columnIndex); +} + +/** + * Return a preview of the data at a specified row and column index, limited + * to approximately the supplied length. + */ +- (id) cellPreviewAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex previewLength:(NSUInteger)previewLength +{ + + // If an edited row exists at the supplied index, return it + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + id anObject = CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex); + if ([anObject isKindOfClass:[NSString class]] && [(NSString *)anObject length] > 150) { + return ([NSString stringWithFormat:@"%@...", [anObject substringToIndex:147]]); + } + return anObject; + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + // If the specified column is not loaded, return a SPNotLoaded reference + if (unloadedColumns[columnIndex]) { + return [SPNotLoaded notLoaded]; + } + + // Return the content + return SPMySQLResultStorePreviewAtRowAndColumn(dataStorage, rowIndex, columnIndex, previewLength); +} + +/** + * Returns whether the data at a specified row and column index is NULL or unloaded + */ +- (BOOL) cellIsNullOrUnloadedAtRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex +{ + // If an edited row exists at the supplied index, check it for a NULL. + NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editedRow != NULL) { + return [(id)CFArrayGetValueAtIndex((CFArrayRef)editedRow, columnIndex) isNSNull]; + } + + // Throw an exception if the column index is out of bounds + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Requested storage column (col %llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + + if (unloadedColumns[columnIndex]) { + return YES; + } + + return [dataStorage cellIsNullAtRow:rowIndex column:columnIndex]; } #pragma mark - @@ -90,38 +201,37 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag /** * Implementation of the NSFastEnumeration protocol. - * Note that this currently doesn't implement mutation guards. + * Note that rows are currently retrieved individually to avoid mutation and locking issues, + * although this could be improved on. */ - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { // If the start index is out of bounds, return 0 to indicate end of results - if (state->state >= numRows) return 0; - - // Determine how many objects to return - 128, len, or all items remaining - NSUInteger itemsToReturn = 128; - if (len < 128) itemsToReturn = len; - if (numRows - state->state < itemsToReturn) { - itemsToReturn = numRows - state->state; - } - - // Construct the arrays to return - NSUInteger i, j; - NSMutableArray *rowArray; - id *row; - for (i = 0; i < itemsToReturn; i++) { - row = dataStorage[state->state + i]; - rowArray = [NSMutableArray arrayWithCapacity:numColumns]; - for (j = 0; j < numColumns; j++) { - CFArrayAppendValue((CFMutableArrayRef)rowArray, row[j]); + if (state->state >= SPMySQLResultStoreGetRowCount(dataStorage)) return 0; + + // If an edited row exists for the supplied index, use that; otherwise use the underlying + // storage row + NSMutableArray *targetRow = SPDataStorageGetEditedRow(editedRows, state->state); + if (targetRow == NULL) { + targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state); + + // Modify unloaded cells as appropriate + for (NSUInteger i = 0; i < numberOfColumns; i++) { + if (unloadedColumns[i]) { + CFArraySetValueAtIndex((CFMutableArrayRef)targetRow, i, [SPNotLoaded notLoaded]); + } } - stackbuf[i] = rowArray; } - state->state += itemsToReturn; + // Add the item to the buffer and return the appropriate state + stackbuf[0] = targetRow; + + state->state += 1; state->itemsPtr = stackbuf; - state->mutationsPtr = (unsigned long *)&numRows; - return itemsToReturn; + state->mutationsPtr = (unsigned long *)self; + + return 1; } #pragma mark - @@ -132,29 +242,17 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag * of objects. Note that the supplied objects are retained as a reference * rather than copied. */ -- (void) addRowWithContents:(NSArray *)row +- (void) addRowWithContents:(NSMutableArray *)aRow { - - // Ensure that sufficient capacity is available - SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1); - - // Add an empty row array to the data store - id *newRow = (id *)malloc(columnPointerByteSize); - dataStorage[numRows] = newRow; - numRows++; - - // Copy over references to the array contents, and retain the objects - NSUInteger cellsCopied = 0; - for (id cellData in row) { - if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData); - else newRow[cellsCopied] = nil; - if (++cellsCopied == numColumns) break; - } - // If an array shorter than the row width was added, pad with nils - if (cellsCopied < numColumns) { - for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil; - } + // Verify the row is of the correct length + [self _checkNewRow:aRow]; + + // Add the new row to the editable store + [editedRows addPointer:aRow]; + + // Update the underlying store as well to keep counts correct + [dataStorage addDummyRow]; } /** @@ -162,109 +260,70 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag * all later rows the next index. Note that the supplied objects within the * array are retained as a reference rather than copied. */ -- (void) insertRowContents:(NSArray *)row atIndex:(NSUInteger)index +- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex { + 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 (index > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; + 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 (index == numRows) return SPDataStorageAddRow(self, row); - - // Ensure that sufficient capacity is available to hold all the rows - SPDataStorageEnsureCapacityForAdditionalRowCount(self, 1); - - // Renumber the specified index, and all subsequent indices, to create a gap - for (NSUInteger j = numRows - 1; j >= index; j--) { - dataStorage[j + 1] = dataStorage[j]; + if (anIndex == numberOfRows) { + return [self addRowWithContents:aRow]; } - // Add a new instantiated row array to the data store at the specified point - id *newRow = (id *)malloc(columnPointerByteSize); - dataStorage[index] = newRow; - numRows++; - - // Copy over references to the array contents, and retain the objects - NSUInteger cellsCopied = 0; - for (id cellData in row) { - if (cellData) newRow[cellsCopied] = (id)CFRetain(cellData); - else newRow[cellsCopied] = nil; - if (++cellsCopied == numColumns) break; - } + // Add the new row to the editable store + [editedRows insertPointer:aRow atIndex:anIndex]; - // If an array shorter than the row width was inserted, pad with nils - if (cellsCopied < numColumns) { - for ( ; cellsCopied <= numColumns; cellsCopied++) newRow[cellsCopied] = nil; - } + // Update the underlying store to keep counts and indices correct + [dataStorage insertDummyRowAtIndex:anIndex]; } /** * Replace a row with contents of the supplied NSArray. */ -- (void) replaceRowAtIndex:(NSUInteger)index withRowContents:(NSArray *)row +- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow { - NSUInteger cellsProcessed = 0; - - // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; - - id *storageRow = dataStorage[index]; - - // Iterate through the row replacing the objects - for (id cellData in row) { - if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]); - if (cellData) storageRow[cellsProcessed] = (id)CFRetain(cellData); - else storageRow[cellsProcessed] = nil; - if (++cellsProcessed == numColumns) break; - } - - // Ensure all cells are correctly updated if an array shorter than the row width was supplied - if (cellsProcessed < numColumns) { - for ( ; cellsProcessed <= numColumns; cellsProcessed++) { - if (storageRow[cellsProcessed]) CFRelease(storageRow[cellsProcessed]); - storageRow[cellsProcessed] = nil; - } - } + [self _checkNewRow:aRow]; + [editedRows replacePointerAtIndex:anIndex withPointer:aRow]; } /** * Replace the contents of a single cell with a supplied object. */ -- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)object +- (void) replaceObjectInRow:(NSUInteger)rowIndex column:(NSUInteger)columnIndex withObject:(id)anObject { - // Throw an exception of either index is out of bounds - if (rowIndex >= numRows || columnIndex >= numColumns) [NSException raise:NSRangeException format:@"Requested storage index (row %llu, col %llu) beyond bounds (%llu, %llu)", (unsigned long long)rowIndex, (unsigned long long)columnIndex, (unsigned long long)numRows, (unsigned long long)numColumns]; + // Make sure that the row in question is editable + NSMutableArray *editableRow = SPDataStorageGetEditedRow(editedRows, rowIndex); + if (editableRow == NULL) { + editableRow = [self rowContentsAtIndex:rowIndex]; + [editedRows replacePointerAtIndex:rowIndex withPointer:editableRow]; + } - // Release the old object and retain the new one - if (dataStorage[rowIndex][columnIndex]) CFRelease(dataStorage[rowIndex][columnIndex]); - if (object) dataStorage[rowIndex][columnIndex] = (id)CFRetain(object); - else dataStorage[rowIndex][columnIndex] = nil; + // Modify the cell + [editableRow replaceObjectAtIndex:columnIndex withObject:anObject]; } /** * Remove a row, renumbering all elements beyond index. */ -- (void) removeRowAtIndex:(NSUInteger)index +- (void) removeRowAtIndex:(NSUInteger)anIndex { // Throw an exception if the index is out of bounds - if (index >= numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)index, (unsigned long long)numRows]; - - // Free the row - NSUInteger j = numColumns; - id *row = dataStorage[index]; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); + if (anIndex >= SPMySQLResultStoreGetRowCount(dataStorage)) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, SPMySQLResultStoreGetRowCount(dataStorage)]; } - free(row); - numRows--; - // Renumber all subsequent indices to fill the gap - for (j = index; j < numRows; j++) { - dataStorage[j] = dataStorage[j + 1]; - } - dataStorage[numRows] = NULL; + // Remove the row from the edited list and underlying storage + [editedRows removePointerAtIndex:anIndex]; + [dataStorage removeRowAtIndex:anIndex]; } /** @@ -275,27 +334,16 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag { // Throw an exception if the range is out of bounds - if (rangeToRemove.location + rangeToRemove.length > numRows) [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)(rangeToRemove.location + rangeToRemove.length), (unsigned long long)numRows]; - - // Free rows in the range - NSUInteger i, j = numColumns; - id *row; - for (i = rangeToRemove.location; i < rangeToRemove.location + rangeToRemove.length; i++) { - row = dataStorage[i]; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); - } - free(row); + if (rangeToRemove.location + rangeToRemove.length > SPMySQLResultStoreGetRowCount(dataStorage)) { + [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)(rangeToRemove.location + rangeToRemove.length), SPMySQLResultStoreGetRowCount(dataStorage)]; } - numRows -= rangeToRemove.length; - // Renumber all subsequent indices to fill the gap - for (i = rangeToRemove.location + rangeToRemove.length - 1; i < numRows; i++) { - dataStorage[i] = dataStorage[i + rangeToRemove.length]; - } - for (i = numRows; i < numRows + rangeToRemove.length; i++) { - dataStorage[i] = NULL; + // Remove the rows from the edited list and underlying storage + NSUInteger i = rangeToRemove.location + rangeToRemove.length; + while (--i >= rangeToRemove.location) { + [editedRows removePointerAtIndex:i]; } + [dataStorage removeRowsInRange:rangeToRemove]; } /** @@ -303,81 +351,58 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag */ - (void) removeAllRows { - NSUInteger j; - id *row; - - // Free all the data - while (numRows > 0) { - row = dataStorage[--numRows]; - j = numColumns; - while (j > 0) { - if (row[--j]) CFRelease(row[j]); - } - free(row); - } + [editedRows setCount:0]; + [dataStorage removeAllRows]; +} - numRows = 0; +#pragma mark - Unloaded columns + +/** + * Mark a column as unloaded; SPNotLoaded placeholders will be returned for cells requested + * from this store which haven't had their value updated from elsewhere. + */ +- (void) setColumnAsUnloaded:(NSUInteger)columnIndex +{ + if (columnIndex >= numberOfColumns) { + [NSException raise:NSRangeException format:@"Invalid column set as unloaded; requested column index (%llu) beyond bounds (%llu)", (unsigned long long)columnIndex, (unsigned long long)numberOfColumns]; + } + unloadedColumns[columnIndex] = true; } -#pragma mark - -#pragma mark Basic information +#pragma mark - Basic information /** * Returns the number of rows currently held in data storage. */ - (NSUInteger) count { - return numRows; + return (NSUInteger)[dataStorage numberOfRows]; } /** - * Set the number of columns represented by the data storage. + * Return the number of columns represented by the data storage. */ -- (void) setColumnCount:(NSUInteger)columnCount +- (NSUInteger) columnCount { - columnPointerByteSize = columnCount * sizeof(id); - - // If there are rows present in the storage, and the number of - // columns has changed, amend the existing rows to match. - if (columnCount != numColumns && numRows) { - NSUInteger i = numRows, j; - id *row; - - // If the new column count is higher than the old count, iterate through the existing rows - // and pad with nils - if (columnCount > numColumns) { - while (i-- > 0) { - dataStorage[i] = (id *)realloc(dataStorage[i], columnPointerByteSize); - j = numColumns; - while (j < columnCount) { - dataStorage[i][j++] = nil; - } - } - - // If the new column count is lower than the old count, iterate through the existing rows - // freeing any extra objects - } else { - while (i > 0) { - row = dataStorage[--i]; - j = numColumns; - while (j > columnCount) { - if (row[--j]) CFRelease(row[j]); - } - dataStorage[i] = (id *)realloc(row, columnPointerByteSize); - } - } - } + return numberOfColumns; +} - // Update the column count - numColumns = columnCount; +/** + * Return whether all the data has been downloaded into the underlying result store. + */ +- (BOOL) dataDownloaded +{ + return [dataStorage dataDownloaded]; } +#pragma mark - Delegate callback methods + /** - * Return the number of columns represented by the data storage. + * When the underlying result store finishes downloading, update the row store to match */ -- (NSUInteger) columnCount +- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore { - return numColumns; + [editedRows setCount:(NSUInteger)[resultStore numberOfRows]]; } /** @@ -387,20 +412,20 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag - (id) init { if ((self = [super init])) { - numColumns = 0; - columnPointerByteSize = 0; - numRows = 0; + dataStorage = nil; + editedRows = nil; + unloadedColumns = NULL; - // Initialise the array, initially with space for 100 rows - numRowsCapacity = 100; - dataStorage = (id **)malloc(numRowsCapacity * sizeof(id *)); + numberOfColumns = 0; } return self; } - (void) dealloc { - [self removeAllRows]; - free(dataStorage); + [dataStorage release], dataStorage = nil; + [editedRows release], editedRows = nil; + if (unloadedColumns) free(unloadedColumns), unloadedColumns = NULL; + [super dealloc]; } @@ -408,23 +433,12 @@ static inline void SPDataStorageEnsureCapacityForAdditionalRowCount(SPDataStorag @implementation SPDataStorage (PrivateAPI) -/** - * Private method to ensure the array always has sufficient capacity - * to store any additional rows required. - */ -- (void) _ensureCapacityForAdditionalRowCount:(NSUInteger)numExtraRows +- (void) _checkNewRow:(NSMutableArray *)aRow { - while (numRows + numExtraRows > numRowsCapacity) [self _increaseCapacity]; + if ([aRow count] != numberOfColumns) { + [NSException raise:NSInternalInconsistencyException format:@"New row length (%llu) does not match store column count (%llu)", (unsigned long long)[aRow count], (unsigned long long)numberOfColumns]; + } } -/** - * Private method to increase the storage available for the array; - * currently doubles the capacity as boundaries are reached. - */ -- (void) _increaseCapacity -{ - numRowsCapacity *= 2; - dataStorage = (id **)realloc(dataStorage, numRowsCapacity * sizeof(id *)); -} @end diff --git a/Source/SPPreferencesUpgrade.m b/Source/SPPreferencesUpgrade.m index af639191..55e68157 100644 --- a/Source/SPPreferencesUpgrade.m +++ b/Source/SPPreferencesUpgrade.m @@ -65,11 +65,15 @@ void SPApplyRevisionChanges(void) if ([prefs objectForKey:SPLastUsedVersion]) recordedVersionNumber = [[prefs objectForKey:SPLastUsedVersion] integerValue]; // Skip processing if the current version matches or is less than recorded version - if (currentVersionNumber <= recordedVersionNumber) return; + if (currentVersionNumber <= recordedVersionNumber) { + [importantUpdateNotes release]; + return; + } // If no recorded version, update to current revision and skip processing if (!recordedVersionNumber) { [prefs setObject:[NSNumber numberWithInteger:currentVersionNumber] forKey:SPLastUsedVersion]; + [importantUpdateNotes release]; return; } diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index cb81a706..ab62b87f 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -40,7 +40,7 @@ @class SPTextView; @class SPFieldEditorController; @class SPMySQLConnection; -@class SPMySQLFastStreamingResult; +@class SPMySQLStreamingResultStore; @class SPTableData; @class SPDatabaseDocument; @class SPTablesList; @@ -118,6 +118,7 @@ BOOL _mainNibLoaded; BOOL isWorking; pthread_mutex_t tableValuesLock; + NSCondition *tableLoadingCondition; #ifndef SP_CODA NSMutableArray *nibObjectsToRelease; #endif @@ -261,7 +262,7 @@ - (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell; - (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell; - (IBAction)setCompareTypes:(id)sender; -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult approximateRowCount:(NSUInteger)targetRowCount; +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore approximateRowCount:(NSUInteger)targetRowCount; - (BOOL)saveRowToTable; - (void) addRowErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; - (NSString *)argumentForRow:(NSInteger)row; diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index f093f875..cc0102f2 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -72,6 +72,12 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOperator"; #endif +@interface SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview; + +@end + @interface SPTableContent () - (BOOL)cancelRowEditing; @@ -165,6 +171,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper usedQuery = [[NSString alloc] initWithString:@""]; tableLoadTimer = nil; + tableLoadingCondition = [NSCondition new]; blackColor = [NSColor blackColor]; lightGrayColor = [NSColor lightGrayColor]; @@ -609,10 +616,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper [dataCell setEditable:YES]; - // Set the line break mode and an NSFormatter subclass which truncates long strings for display + // Set the line break mode and an NSFormatter subclass which displays line breaks nicely [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; - [[dataCell formatter] setDisplayLimit:150]; // Set field length limit if field is a varchar to match varchar length if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"] @@ -765,7 +771,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper NSMutableString *queryString; NSString *queryStringBeforeLimit = nil; NSString *filterString; - SPMySQLFastStreamingResult *streamingResult; + SPMySQLStreamingResultStore *resultStore; NSInteger rowsToLoad = [[tableDataInstance statusValueForKey:@"Rows"] integerValue]; #ifndef SP_CODA @@ -828,23 +834,23 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Perform and process the query [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; [self setUsedQuery:queryString]; - streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; + resultStore = [[mySQLConnection resultStoreFromQueryString:queryString] retain]; // Ensure the number of columns are unchanged; if the column count has changed, abort the load // and queue a full table reload. BOOL fullTableReloadRequired = NO; - if (streamingResult && [dataColumns count] != [streamingResult numberOfFields]) { + if (resultStore && [dataColumns count] != [resultStore numberOfFields]) { [tableDocumentInstance disableTaskCancellation]; [mySQLConnection cancelCurrentQuery]; - [streamingResult cancelResultLoad]; + [resultStore cancelResultLoad]; fullTableReloadRequired = YES; } // Process the result into the data store - if (!fullTableReloadRequired && streamingResult) { - [self processResultIntoDataStorage:streamingResult approximateRowCount:rowsToLoad]; + if (!fullTableReloadRequired && resultStore) { + [self updateResultStore:resultStore approximateRowCount:rowsToLoad]; } - if (streamingResult) [streamingResult release]; + if (resultStore) [resultStore release]; // If the result is empty, and a late page is selected, reset the page if (!fullTableReloadRequired && [prefs boolForKey:SPLimitResults] && queryStringBeforeLimit && !tableRowsCount && ![mySQLConnection lastQueryWasCancelled]) { @@ -852,10 +858,10 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper previousTableRowsCount = tableRowsCount; queryString = [NSMutableString stringWithFormat:@"%@ LIMIT 0,%ld", queryStringBeforeLimit, (long)[prefs integerForKey:SPLimitResultsValue]]; [self setUsedQuery:queryString]; - streamingResult = [[mySQLConnection streamingQueryString:queryString] retain]; - if (streamingResult) { - [self processResultIntoDataStorage:streamingResult approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; - [streamingResult release]; + resultStore = [[mySQLConnection resultStoreFromQueryString:queryString] retain]; + if (resultStore) { + [self updateResultStore:resultStore approximateRowCount:[prefs integerForKey:SPLimitResultsValue]]; + [resultStore release]; } } @@ -950,7 +956,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Retrieve and cache the column definitions for editing views if (cqColumnDefinition) [cqColumnDefinition release]; - cqColumnDefinition = [[streamingResult fieldDefinitions] retain]; + cqColumnDefinition = [[resultStore fieldDefinitions] retain]; // Notify listenters that the query has finished @@ -985,100 +991,57 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } /** - * Processes a supplied streaming result set, loading it into the data array. + * Processes a supplied streaming result store, monitoring the load and updating the data + * displayed during download. */ -- (void)processResultIntoDataStorage:(SPMySQLFastStreamingResult *)theResult approximateRowCount:(NSUInteger)targetRowCount +- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore approximateRowCount:(NSUInteger)targetRowCount; { NSUInteger i; NSUInteger dataColumnsCount = [dataColumns count]; - BOOL *columnBlobStatuses = malloc(dataColumnsCount * sizeof(BOOL)); tableLoadTargetRowCount = targetRowCount; - // Set the column count on the data store before setting up anything else - - // ensures that SPDataStorage is set up for timer-driven data loads - [tableValues setColumnCount:dataColumnsCount]; + // Update the data storage, updating the current store if appropriate + pthread_mutex_lock(&tableValuesLock); + [tableValues setDataStorage:theResultStore updatingExisting:!![tableValues count]]; + pthread_mutex_unlock(&tableValuesLock); - // Set up the table updates timer - [[self onMainThread] initTableLoadTimer]; + // Start the data downloading + [theResultStore startDownload]; - NSAutoreleasePool *dataLoadingPool; #ifndef SP_CODA NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance valueForKey:@"queryProgressBar"]; #else NSProgressIndicator *dataLoadingIndicator = [tableDocumentInstance queryProgressBar]; #endif - BOOL prefsLoadBlobsAsNeeded = -#ifndef SP_CODA - [prefs boolForKey:SPLoadBlobsAsNeeded] -#else - NO -#endif - ; - - // Build up an array of which columns are blobs for faster iteration - for ( i = 0; i < dataColumnsCount ; i++ ) { - columnBlobStatuses[i] = [tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]]; - } - - // Set up an autorelease pool for row processing - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - // Loop through the result rows as they become available - tableRowsCount = 0; - for (NSArray *eachRow in theResult) { - pthread_mutex_lock(&tableValuesLock); - - if (tableRowsCount < previousTableRowsCount) { - SPDataStorageReplaceRow(tableValues, tableRowsCount, eachRow); - } else { - SPDataStorageAddRow(tableValues, eachRow); - } - - // Alter the values for hidden blob and text fields if appropriate - if ( prefsLoadBlobsAsNeeded ) { - for ( i = 0 ; i < dataColumnsCount ; i++ ) { - if (columnBlobStatuses[i]) { - SPDataStorageReplaceObjectAtRowAndColumn(tableValues, tableRowsCount, i, [SPNotLoaded notLoaded]); - } +#ifndef SP_CODA + // Set the column load states on the table values store + if ([prefs boolForKey:SPLoadBlobsAsNeeded]) { + for ( i = 0; i < dataColumnsCount ; i++ ) { + if ([tableDataInstance columnIsBlobOrText:[NSArrayObjectAtIndex(dataColumns, i) objectForKey:@"name"]]) { + [tableValues setColumnAsUnloaded:i]; } } - tableRowsCount++; + } +#endif - pthread_mutex_unlock(&tableValuesLock); + // Set up the table updates timer and wait for it to notify this thread about completion + [[self onMainThread] initTableLoadTimer]; - // Drain and reset the autorelease pool every ~1024 rows - if (!(tableRowsCount % 1024)) { - [dataLoadingPool drain]; - dataLoadingPool = [[NSAutoreleasePool alloc] init]; - } + [tableLoadingCondition lock]; + while (![tableValues dataDownloaded]) { + [tableLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; } - - // Clean up the interface update timer - [[self onMainThread] clearTableLoadTimer]; + [tableLoadingCondition unlock]; // If the final column autoresize wasn't performed, perform it if (tableLoadLastRowCount < 200) [[self onMainThread] autosizeColumns]; - // If the reloaded table is shorter than the previous table, remove the extra values from the storage - if (tableRowsCount < [tableValues count]) { - pthread_mutex_lock(&tableValuesLock); - [tableValues removeRowsInRange:NSMakeRange(tableRowsCount, [tableValues count] - tableRowsCount)]; - pthread_mutex_unlock(&tableValuesLock); - } - // Ensure the table is aware of changes - if ([NSThread isMainThread]) { - [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES]; - } else { - [tableContentView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES]; - [tableContentView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO]; - } + [[tableContentView onMainThread] noteNumberOfRowsChanged]; - // Clean up the autorelease pool and reset the progress indicator - [dataLoadingPool drain]; + // Reset the progress indicator [dataLoadingIndicator setIndeterminate:YES]; - - free(columnBlobStatuses); } /** @@ -1397,6 +1360,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper */ - (void) tableLoadUpdate:(NSTimer *)theTimer { + tableRowsCount = [tableValues count]; // Update the task interface as necessary if (!isFiltered && tableLoadTargetRowCount != NSUIntegerMax) { @@ -1414,6 +1378,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper return; } + if ([tableValues dataDownloaded]) { + [tableLoadingCondition lock]; + [tableLoadingCondition signal]; + [self clearTableLoadTimer]; + [tableLoadingCondition unlock]; + } + // Check whether a table update is required, based on whether new rows are // available to display. if (tableRowsCount == tableLoadLastRowCount) { @@ -1422,7 +1393,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Update the table display [tableContentView noteNumberOfRowsChanged]; - if (!tableLoadLastRowCount) [tableContentView setNeedsDisplay:YES]; // Update column widths in two cases: on very first rows displayed, and once // more than 200 rows are present. @@ -2452,7 +2422,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper for (NSTableColumn *tableColumn in tableColumns) { - [tempRow addObject:[self tableView:tableContentView objectValueForTableColumn:tableColumn row:i]]; + [tempRow addObject:[self _contentValueForTableColumn:[[tableColumn identifier] integerValue] row:i asPreview:NO]]; } [currentResult addObject:[NSArray arrayWithArray:tempRow]]; @@ -4274,6 +4244,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper if(fieldEditor) [fieldEditor release], fieldEditor = nil; [self clearTableLoadTimer]; + [tableLoadingCondition release]; [tableValues release]; pthread_mutex_destroy(&tableValuesLock); [dataColumns release]; diff --git a/Source/SPTableContentDataSource.m b/Source/SPTableContentDataSource.m index c49169a0..8ffa2c69 100644 --- a/Source/SPTableContentDataSource.m +++ b/Source/SPTableContentDataSource.m @@ -39,6 +39,12 @@ #import <pthread.h> #import <SPMySQL/SPMySQL.h> +@interface SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview; + +@end + @implementation SPTableContent (SPTableContentDataSource) #pragma mark - @@ -91,7 +97,7 @@ pthread_mutex_lock(&tableValuesLock); if (rowIndex < (NSInteger)tableRowsCount && columnIndex < [tableValues columnCount]) { - value = [[SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex) copy] autorelease]; + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:YES]; } pthread_mutex_unlock(&tableValuesLock); @@ -99,7 +105,11 @@ if (!value) return @"..."; } else { - value = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + if ([tableView editedColumn] == (NSInteger)columnIndex && [tableView editedRow] == rowIndex) { + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:NO]; + } else { + value = [self _contentValueForTableColumn:columnIndex row:rowIndex asPreview:YES]; + } } if ([value isKindOfClass:[SPMySQLGeometryData class]]) @@ -185,3 +195,15 @@ } @end + +@implementation SPTableContent (SPTableContentDataSource_Private_API) + +- (id)_contentValueForTableColumn:(NSUInteger)columnIndex row:(NSUInteger)rowIndex asPreview:(BOOL)asPreview +{ + if (asPreview) { + return SPDataStoragePreviewAtRowAndColumn(tableValues, rowIndex, columnIndex, 150); + } + return SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); +} + +@end diff --git a/Source/SPTableContentDelegate.m b/Source/SPTableContentDelegate.m index d2e7f2d5..8f228aa0 100644 --- a/Source/SPTableContentDelegate.m +++ b/Source/SPTableContentDelegate.m @@ -464,9 +464,18 @@ if (![cell respondsToSelector:@selector(setTextColor:)]) return; - id theValue = nil; + BOOL showCellAsGray = NO; NSUInteger columnIndex = [[tableColumn identifier] integerValue]; + // If user wants to edit 'cell' set text color to black and return to avoid + // writing in gray if value was NULL + if ([tableView editedColumn] != -1 + && [tableView editedRow] == rowIndex + && (NSUInteger)[[NSArrayObjectAtIndex([tableView tableColumns], [tableView editedColumn]) identifier] integerValue] == columnIndex) { + [cell setTextColor:blackColor]; + return; + } + // While the table is being loaded, additional validation is required - data // locks must be used to avoid crashes, and indexes higher than the available // rows or columns may be requested. Use gray to indicate loading in these cases. @@ -474,34 +483,18 @@ pthread_mutex_lock(&tableValuesLock); if (rowIndex < (NSInteger)tableRowsCount && columnIndex < [tableValues columnCount]) { - theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + showCellAsGray = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } pthread_mutex_unlock(&tableValuesLock); - - if (!theValue) { - [cell setTextColor:[NSColor lightGrayColor]]; - return; - } } else { - theValue = SPDataStorageObjectAtRowAndColumn(tableValues, rowIndex, columnIndex); + showCellAsGray = [tableValues cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex]; } - // If user wants to edit 'cell' set text color to black and return to avoid - // writing in gray if value was NULL - if ([tableView editedColumn] != -1 - && [tableView editedRow] == rowIndex - && (NSUInteger)[[NSArrayObjectAtIndex([tableView tableColumns], [tableView editedColumn]) identifier] integerValue] == columnIndex) { - [cell setTextColor:blackColor]; - return; - } - // For null cells and not loaded cells, display the contents in gray. - if ([theValue isNSNull] || [theValue isSPNotLoaded]) { + if (showCellAsGray) { [cell setTextColor:lightGrayColor]; - - // Otherwise, set the color to black - required as NSTableView reuses NSCells. } else { [cell setTextColor:blackColor]; |