aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPTableData.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPTableData.m')
-rw-r--r--Source/SPTableData.m509
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