// // 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 #import "CMMCPConnection.h" #import "CMMCPResult.h" #import #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