diff options
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]; |