diff options
author | rowanbeentje <rowan@beent.je> | 2009-02-18 21:07:43 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2009-02-18 21:07:43 +0000 |
commit | 2525366dbfed3aef78beaed89630ea543389cec1 (patch) | |
tree | 68dc953f1bc0c127c05e618072dc8936d8a8cf0e /Source/SPTableData.m | |
parent | 1dae450e32b269cb95c47a4274de9641ea0e779a (diff) | |
download | sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.tar.gz sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.tar.bz2 sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.zip |
Visible improvements in this build:
- Significantly reduce the queries that have to be performed, improving lag - especially over slow connections (Issue #118; see new controller info under headline code changes).
- Fix Issue #117 properly (export numeric quoting - we now have access to column types and so can quote appropriately).
- Fix Issue #145 (loss of unsigned/null/default attributes when reordering columns).
- Fixes Issue #90 (support for filtering DECIMAL column types)
- Improve table scrolling speed when the table contains long items. (Added a NSFormatter to automatically truncate strings > 150 chars for display purposes only)
- Improved SQL compatibility - for example /* C style comments */ are now correctly ignored in imports and custom queries.
- Add text and symbols emphasising that the table info pane / status view row count is an approximation (partially addresses Issue #141)
- Fixes a major memory leak whenever opening or scrolling tables containing text/blob data.
- SQL import is now faster (SQL parsing part is 3x faster).
- Speed up SQL export (1.5x faster for numeric data; 1.1x faster for string data) and slightly speed up CSV export (~1.1x faster).
- Display sizes on the status view using the byte size formatter, as per table info pane.
Headline code changes:
- Add a new NSMutableString subclass, SPSQLParser. See the header file for documentation and overview, but in short it's a centralised place for SQL parsing. Centralises and improves parsing, improves comment support, improves quoting support. Despite the improved featureset this is also faster than the previous distributed implementations - for example, when used to replace the old splitQueries:, > 3x speedup.
- Implement a new controller which handles a structure and status cache for the current table, and provides structure parsing for specified tables. This cache is now used throughout the code, reducing the queries that have to be performed and providing additional information about the table structure for use; I think it also improves column type format slightly.
- The table info pane and the status view now draw all their data from the cache.
Tweaks:
- Table encoding is now detected directly instead of being derived from the collation - increased accuracy and cope with the DEFAULT encoding.
- Comments and formatting cleaned up in bits I was working on, obviously.
- A couple of methods - particularly [tablesListInstance table] and [tableDocument encoding] - have been renamed to avoid conflicts and fix code warnings.
Future improvements now possible:
- As we now have access to column types and other information, we can provide per-type behaviour where desired.
- The table parsing doesn't currently pull out comments or table indices, together with one or two other attributes. Some of this would be useful for display; some, such as indices, could be used to draw the table structure view as long as we're happy discarding a couple of columns (ie cardinality!)
Diffstat (limited to 'Source/SPTableData.m')
-rw-r--r-- | Source/SPTableData.m | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/Source/SPTableData.m b/Source/SPTableData.m new file mode 100644 index 00000000..144c3280 --- /dev/null +++ b/Source/SPTableData.m @@ -0,0 +1,509 @@ +// +// SPTableData.m +// sequel-pro +// +// Created by Rowan Beentje on 24/01/2009. +// Copyright 2009 Arboreal. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "CMMCPConnection.h" +#import "CMMCPResult.h" +#import <MCPKit_bundled/MCPKit_bundled.h> +#import "SPTableData.h" +#import "SPSQLParser.h" +#import "TableDocument.h" +#import "TablesList.h" + + +@implementation SPTableData + + +- (id) init +{ + if ((self = [super init])) { + columns = [[NSMutableArray alloc] init]; + columnNames = [[NSMutableArray alloc] init]; + status = [[NSMutableDictionary alloc] init]; + tableEncoding = nil; + mySQLConnection = nil; + } + + return self; +} + +/* + * Set the connection for use. + * Called by the connect sheet methods. + */ +- (void) setConnection:(CMMCPConnection *)theConnection +{ + mySQLConnection = theConnection; + [mySQLConnection retain]; +} + + +/* + * Retrieve the encoding for the current table, using or refreshing the cache as appropriate. + */ +- (NSString *) tableEncoding +{ + if (tableEncoding == nil) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return [NSString stringWithString:tableEncoding]; +} + + +/* + * Retrieve all columns for the current table as an array, using or refreshing the cache as appropriate. + */ +- (NSArray *) columns +{ + if ([columns count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return columns; +} + + +/* + * Retrieve a column with a specified name, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) columnWithName:(NSString *)colName +{ + int columnIndex = [columnNames indexOfObject:colName]; + return [columns objectAtIndex:columnIndex]; +} + + +/* + * Retrieve column names for the current table as an array, using or refreshing the cache as appropriate. + */ +- (NSArray *) columnNames +{ + if ([columnNames count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return columnNames; +} + + +/* + * Retrieve a specified column for the current table as a dictionary, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) columnAtIndex:(int)index +{ + if ([columns count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return [columns objectAtIndex:index]; +} + + +/* + * Retrieve the table status value for a supplied key, using or refreshing the cache as appropriate. + */ +- (NSString *) statusValueForKey:(NSString *)aKey +{ + if ([status count] == 0) { + [self updateStatusInformationForCurrentTable]; + } + return [status objectForKey:aKey]; +} + + +/* + * Retrieve all known status values as a dictionary, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) statusValues +{ + if ([status count] == 0) { + [self updateStatusInformationForCurrentTable]; + } + return status; +} + + +/* + * Flushes all caches - should be used on major changes, for example table changes. + */ +- (void) resetAllData +{ + [columns removeAllObjects]; + [columnNames removeAllObjects]; + [status removeAllObjects]; + if (tableEncoding != nil) { + [tableEncoding release]; + tableEncoding = nil; + } +} + + +/* + * Flushes any status-related caches. + */ +- (void) resetStatusData +{ + [status removeAllObjects]; +} + + +/* + * Flushes any field/column-related caches. + */ +- (void) resetColumnData +{ + [columns removeAllObjects]; + [columnNames removeAllObjects]; +} + + +/* + * Retrieves the information for the current table and stores it in cache. + * Returns a boolean indicating success. + */ +- (BOOL) updateInformationFromCreateTableForCurrentTable +{ + NSDictionary *tableData = [self informationFromCreateTableSyntaxForTable:[tableListInstance tableName]]; + NSDictionary *columnData; + NSEnumerator *enumerator; + + if (tableData == nil) { + [columns removeAllObjects]; + [columnNames removeAllObjects]; + return FALSE; + } + + [columns addObjectsFromArray:[tableData objectForKey:@"columns"]]; + + enumerator = [columns objectEnumerator]; + while (columnData = [enumerator nextObject]) { + [columnNames addObject:[NSString stringWithString:[columnData objectForKey:@"name"]]]; + } + + if (tableEncoding != nil) { + [tableEncoding release]; + } + tableEncoding = [[NSString alloc] initWithString:[tableData objectForKey:@"encoding"]]; + + return TRUE; +} + + +/* + * Retrieve the CREATE TABLE string for a table and analyse it to extract the field + * details and table encoding. + * In future this could also be used to retrieve the majority of index information + * assuming information like cardinality isn't needed. + * This function is rather long due to the painful parsing required, but is fast. + * Returns a boolean indicating success. + */ +- (NSDictionary *) informationFromCreateTableSyntaxForTable:(NSString *)tableName +{ + SPSQLParser *createTableParser, *fieldsParser, *fieldParser, *detailParser; + NSMutableArray *tableColumns, *fieldStrings, *definitionParts, *detailParts; + NSMutableDictionary *tableColumn, *tableData; + NSString *detailString, *encodingString; + unsigned i, j, stringStart, partsArrayLength; + + // Catch unselected tables and return nil + if ([tableName isEqualToString:@""] || !tableName) return nil; + + // Retrieve the CREATE TABLE syntax for the table + CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", tableName]]; + + // Check for any errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table information:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return nil; + } + + // Retrieve the table syntax string + NSArray *syntaxResult = [theResult fetchRowAsArray]; + if ([[syntaxResult objectAtIndex:1] isKindOfClass:[NSData class]]) { + createTableParser = [[SPSQLParser alloc] initWithData:[syntaxResult objectAtIndex:1] encoding:[mySQLConnection encoding]]; + } else { + createTableParser = [[SPSQLParser alloc] initWithString:[syntaxResult objectAtIndex:1]]; + } + + // Extract the fields definition string from the CREATE TABLE syntax + fieldsParser = [[SPSQLParser alloc] initWithString:[createTableParser trimAndReturnStringFromCharacter:'(' toCharacter:')' trimmingInclusively:YES returningInclusively:NO skippingBrackets:YES]]; + + // Split the fields and keys string into an array of individual elements + fieldStrings = [[NSMutableArray alloc] initWithArray:[fieldsParser splitStringByCharacter:',' skippingBrackets:YES]]; + + // fieldStrings should now hold unparsed field and key strings, while tableProperty string holds unparsed + // table information. Proceed further by parsing the field strings. + tableColumns = [[NSMutableArray alloc] init]; + tableColumn = [[NSMutableDictionary alloc] init]; + definitionParts = [[NSMutableArray alloc] init]; + fieldParser = [[SPSQLParser alloc] init]; + for (i = 0; i < [fieldStrings count]; i++) { + + // Take this field/key string, trim whitespace from both ends and remove comments + [fieldsParser setString:[[fieldStrings objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + [fieldsParser deleteComments]; + if (![fieldsParser length]) { + continue; + } + [tableColumn removeAllObjects]; + [definitionParts removeAllObjects]; + + // If the first character is a backtick, this is a field definition. + if ([fieldsParser characterAtIndex:0] =='`') { + + // Capture the area between the two backticks as the name + [tableColumn setObject:[fieldsParser trimAndReturnStringFromCharacter:'`' toCharacter:'`' trimmingInclusively:YES returningInclusively:NO ignoringQuotedStrings:NO] forKey:@"name"]; + + // Split the remaining field definition string by spaces ready for processing + [definitionParts addObjectsFromArray:[fieldsParser splitStringByCharacter:' ' skippingBrackets:YES]]; + + // The first item is always the data type. + [fieldParser setString:[definitionParts objectAtIndex:0]]; + + // If no field length definition is present, store only the type + if ([fieldParser firstOccurrenceOfCharacter:'(' ignoringQuotedStrings:YES] == NSNotFound) { + [tableColumn setObject:[fieldParser uppercaseString] forKey:@"type"]; + + // Otherwise separate out the length definition for processing + } else { + detailParser = [[SPSQLParser alloc] initWithString:[[fieldParser stringToCharacter:'(' inclusively:NO] uppercaseString]]; + [tableColumn setObject:[NSString stringWithString:detailParser] forKey:@"type"]; + + // For ENUMs and SETs, capture the field value options into an array for storage + if ([detailParser isEqualToString:@"ENUM"] || [detailParser isEqualToString:@"SET"]) { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + detailParts = [[NSMutableArray alloc] initWithArray:[detailParser splitStringByCharacter:',']]; + for (j = 0; j < [detailParts count]; j++) { + [detailParser setString:[detailParts objectAtIndex:j]]; + [detailParts replaceObjectAtIndex:j withObject:[detailParser unquotedString]]; + } + [tableColumn setObject:[NSArray arrayWithArray:detailParts] forKey:@"values"]; + [detailParts release]; + + // For types with required or optional decimals, store as appropriate + } else if ([detailParser isEqualToString:@"REAL"] || [detailParser isEqualToString:@"DOUBLE"] || [detailParser isEqualToString:@"FLOAT"] || [detailParser isEqualToString:@"DECIMAL"] || [detailParser isEqualToString:@"NUMERIC"]) { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + detailParts = [[NSMutableArray alloc] initWithArray:[detailParser splitStringByCharacter:',']]; + [detailParser setString:[detailParts objectAtIndex:0]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"length"]; + if ([detailParts count] > 1) { + [detailParser setString:[detailParts objectAtIndex:1]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"decimals"]; + } + [detailParts release]; + + // Otherwise capture the length only. + } else { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"length"]; + } + [detailParser release]; + } + + // Also capture a general column type "group" to allow behavioural switches + detailString = [[NSString alloc] initWithString:[tableColumn objectForKey:@"type"]]; + if ([detailString isEqualToString:@"BIT"]) { + [tableColumn setObject:@"bit" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"TINYINT"] || [detailString isEqualToString:@"SMALLINT"] || [detailString isEqualToString:@"MEDIUMINT"] + || [detailString isEqualToString:@"INT"] || [detailString isEqualToString:@"INTEGER"] || [detailString isEqualToString:@"BIGINT"]) { + [tableColumn setObject:@"integer" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"REAL"] || [detailString isEqualToString:@"DOUBLE"] || [detailString isEqualToString:@"FLOAT"] + || [detailString isEqualToString:@"DECIMAL"] || [detailString isEqualToString:@"NUMERIC"]) { + [tableColumn setObject:@"float" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"DATE"] || [detailString isEqualToString:@"TIME"] || [detailString isEqualToString:@"TIMESTAMP"] + || [detailString isEqualToString:@"DATETIME"] || [detailString isEqualToString:@"YEAR"]) { + [tableColumn setObject:@"date" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"CHAR"] || [detailString isEqualToString:@"VARCHAR"]) { + [tableColumn setObject:@"string" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"BINARY"] || [detailString isEqualToString:@"VARBINARY"]) { + [tableColumn setObject:@"binary" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"ENUM"] || [detailString isEqualToString:@"SET"]) { + [tableColumn setObject:@"enum" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"TINYTEXT"] || [detailString isEqualToString:@"TEXT"] + || [detailString isEqualToString:@"MEDIUMTEXT"] || [detailString isEqualToString:@"LONGTEXT"]) { + [tableColumn setObject:@"textdata" forKey:@"typegrouping"]; + + // Default to "blobdata". This means that future and currently unsupported types - including spatial extensions - + // will be preserved unmangled. + } else { + [tableColumn setObject:@"blobdata" forKey:@"typegrouping"]; + } + + // Set up some column defaults for all columns + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"null"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"unsigned"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"binary"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"zerofill"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"autoincrement"]; + + // Walk through the remaining column definition parts storing recognised details + partsArrayLength = [definitionParts count]; + for (j = 1; j < partsArrayLength; j++) { + detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:j] uppercaseString]]; + + // Whether numeric fields are unsigned + if ([detailString isEqualToString:@"UNSIGNED"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"unsigned"]; + + // Whether numeric fields are zerofill + } else if ([detailString isEqualToString:@"ZEROFILL"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"zerofill"]; + + // Whether text types are binary + } else if ([detailString isEqualToString:@"BINARY"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"binary"]; + + // Whether text types have a different encoding to the table + } else if ([detailString isEqualToString:@"CHARSET"] && (j + 1 < partsArrayLength)) { + if (![[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"DEFAULT"]) { + [tableColumn setValue:[definitionParts objectAtIndex:j+1] forKey:@"encoding"]; + } + j++; + } else if ([detailString isEqualToString:@"CHARACTER"] && (j + 2 < partsArrayLength) + && [[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"SET"]) { + if (![[[definitionParts objectAtIndex:j+2] uppercaseString] isEqualToString:@"DEFAULT"]) {; + [tableColumn setValue:[definitionParts objectAtIndex:j+2] forKey:@"encoding"]; + } + j = j + 2; + + // Whether text types have a different collation to the table + } else if ([detailString isEqualToString:@"COLLATE"] && (j + 1 < partsArrayLength)) { + if (![[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"DEFAULT"]) { + [tableColumn setValue:[definitionParts objectAtIndex:j+1] forKey:@"collation"]; + } + j++; + + // Whether fields are NOT NULL + } else if ([detailString isEqualToString:@"NOT"] && (j + 1 < partsArrayLength) + && [[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"NULL"]) { + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"null"]; + j++; + + // Whether fields are NULL + } else if ([detailString isEqualToString:@"NULL"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"null"]; + + // Whether fields should auto-increment + } else if ([detailString isEqualToString:@"AUTO_INCREMENT"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"autoincrement"]; + + // Field defaults + } else if ([detailString isEqualToString:@"DEFAULT"] && (j + 1 < partsArrayLength)) { + detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:j+1]]; + [tableColumn setValue:[detailParser unquotedString] forKey:@"default"]; + [detailParser release]; + j++; + } + + // TODO: Currently unhandled: [UNIQUE | PRIMARY] KEY | COMMENT 'foo' | COLUMN_FORMAT bar | STORAGE q | REFERENCES... + + [detailString release]; + } + + // Store the column. + [tableColumns addObject:[NSDictionary dictionaryWithDictionary:tableColumn]]; + + // TODO: Otherwise it's a key definition, constraint, check, or other 'metadata'. Would be useful to parse/display these! + } else { + + } + } + [fieldStrings release]; + [fieldsParser release]; + [definitionParts release]; + [tableColumn release]; + + // Extract the encoding from the table properties string - other details come from TABLE STATUS. + NSRange charsetDefinitionRange = [createTableParser rangeOfString:@"CHARSET=" options:NSCaseInsensitiveSearch]; + if (charsetDefinitionRange.location == NSNotFound) { + charsetDefinitionRange = [createTableParser rangeOfString:@"CHARACTER SET=" options:NSCaseInsensitiveSearch]; + } + if (charsetDefinitionRange.location != NSNotFound) { + stringStart = charsetDefinitionRange.location + charsetDefinitionRange.length; + for (i = stringStart; i < [createTableParser length]; i++) { + if ([createTableParser characterAtIndex:i] == ' ') break; + } + + // Catch the "default" character encoding: + if ([[[createTableParser substringWithRange:NSMakeRange(stringStart, i-stringStart)] lowercaseString] isEqualToString:@"default"]) { + encodingString = [[NSString alloc] initWithString:[tableDocumentInstance databaseEncoding]]; + } else { + encodingString = [[NSString alloc] initWithString:[createTableParser substringWithRange:NSMakeRange(stringStart, i-stringStart)]]; + } + + // If no DEFAULT CHARSET is present, it's likely MySQL < 4; fall back to latin1. + } else { + encodingString = [[NSString alloc] initWithString:@"latin1"]; + } + + [createTableParser release]; + [fieldParser release]; + + tableData = [NSMutableDictionary dictionary]; + [tableData setObject:[NSString stringWithString:encodingString] forKey:@"encoding"]; + [tableData setObject:[NSArray arrayWithArray:tableColumns] forKey:@"columns"]; + + [encodingString release]; + [tableColumns release]; + + return tableData; +} + + +/* + * Retrieve the status of a table as a dictionary and add it to the local cache for reuse. + */ +- (BOOL) updateStatusInformationForCurrentTable +{ + + // Catch unselected tables and return nil + if ([[tableListInstance tableName] isEqualToString:@""] || ![tableListInstance tableName]) return nil; + + // Run the status query and retrieve as a dictionary. + CMMCPResult *tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", [tableListInstance tableName]]]; + + // Check for any errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table status:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return FALSE; + } + + // Retrieve the status as a dictionary and set as the cache + [status setDictionary:[tableStatusResult fetchRowAsDictionary]]; + + // Reassign any "Type" key - for MySQL < 4.1 - to "Engine" for consistency. + if ([status objectForKey:@"Type"]) { + [status setObject:[status objectForKey:@"Type"] forKey:@"Engine"]; + } + + return TRUE; +} + + +- (void) dealloc +{ + [columns release]; + [columnNames release]; + [status release]; + if (tableEncoding != nil) [tableEncoding release]; + + [super dealloc]; +} + +@end |