diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/CustomQuery.h | 2 | ||||
-rw-r--r-- | Source/CustomQuery.m | 18 | ||||
-rw-r--r-- | Source/SPDataCellFormatter.h | 31 | ||||
-rw-r--r-- | Source/SPDataCellFormatter.m | 57 | ||||
-rw-r--r-- | Source/SPSQLParser.h | 233 | ||||
-rw-r--r-- | Source/SPSQLParser.m | 655 | ||||
-rw-r--r-- | Source/SPTableData.h | 54 | ||||
-rw-r--r-- | Source/SPTableData.m | 509 | ||||
-rw-r--r-- | Source/SPTableInfo.h | 1 | ||||
-rw-r--r-- | Source/SPTableInfo.m | 60 | ||||
-rw-r--r-- | Source/TableContent.h | 7 | ||||
-rw-r--r-- | Source/TableContent.m | 599 | ||||
-rw-r--r-- | Source/TableDocument.h | 7 | ||||
-rw-r--r-- | Source/TableDocument.m | 69 | ||||
-rw-r--r-- | Source/TableDump.h | 4 | ||||
-rw-r--r-- | Source/TableDump.m | 181 | ||||
-rw-r--r-- | Source/TableSource.h | 1 | ||||
-rw-r--r-- | Source/TableSource.m | 106 | ||||
-rw-r--r-- | Source/TableStatus.h | 2 | ||||
-rw-r--r-- | Source/TableStatus.m | 83 | ||||
-rw-r--r-- | Source/TablesList.h | 3 | ||||
-rw-r--r-- | Source/TablesList.m | 38 |
22 files changed, 2091 insertions, 629 deletions
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h index e873d833..61dbc7b8 100644 --- a/Source/CustomQuery.h +++ b/Source/CustomQuery.h @@ -31,8 +31,6 @@ @interface CustomQuery : NSObject { - IBOutlet id tableDumpInstance; - IBOutlet id tableWindow; IBOutlet id queryFavoritesButton; IBOutlet id queryHistoryButton; diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index e5f48e7b..7936e962 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -23,7 +23,7 @@ // Or mail to <lorenz@textor.ch> #import "CustomQuery.h" -#import "TableDump.h" +#import "SPSQLParser.h" #import "SPGrowlController.h" @implementation CustomQuery @@ -45,23 +45,26 @@ sets the tableView columns corresponding to the mysql-result NSTableColumn *theCol; CMMCPResult *theResult = nil; NSArray *queries; -// NSArray *theTypes; NSMutableArray *menuItems = [NSMutableArray array]; NSMutableArray *tempResult = [NSMutableArray array]; NSMutableString *errors = [NSMutableString string]; + SPSQLParser *queryParser; int i; - //query started + // Notify listeners that a query has started [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; - //split queries by ;'s - queries = [tableDumpInstance splitQueries:[textView string]]; + // Retrieve the custom query string and split it into separate SQL queries + queryParser = [[SPSQLParser alloc] initWithString:[textView string]]; + queries = [queryParser splitStringByCharacter:';']; + [queryParser release]; -//perform queries + // Perform the queries in series for ( i = 0 ; i < [queries count] ; i++ ) { theResult = [mySQLConnection queryString:[queries objectAtIndex:i]]; if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { - //query gave error + + // If the query errored, append error to the error log for display at the end if ( [queries count] > 1 ) { [errors appendString:[NSString stringWithFormat:NSLocalizedString(@"[ERROR in query %d] %@\n", @"error text when multiple custom query failed"), i+1, @@ -70,7 +73,6 @@ sets the tableView columns corresponding to the mysql-result [errors setString:[mySQLConnection getLastErrorMessage]]; } } -// theTypes = [queryResult fetchTypesAsArray]; } //perform empty query if no query is given diff --git a/Source/SPDataCellFormatter.h b/Source/SPDataCellFormatter.h new file mode 100644 index 00000000..aafb1b1c --- /dev/null +++ b/Source/SPDataCellFormatter.h @@ -0,0 +1,31 @@ +// +// SPDataCell.h +// sequel-pro +// +// Created by Rowan Beentje on 11/02/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 <Cocoa/Cocoa.h> + + +@interface SPDataCellFormatter : NSFormatter { + +} + +@end diff --git a/Source/SPDataCellFormatter.m b/Source/SPDataCellFormatter.m new file mode 100644 index 00000000..c23fccf5 --- /dev/null +++ b/Source/SPDataCellFormatter.m @@ -0,0 +1,57 @@ +// +// SPDataCell.m +// sequel-pro +// +// Created by Rowan Beentje on 11/02/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 "SPDataCellFormatter.h" + + +@implementation SPDataCellFormatter + +- (NSString *)stringForObjectValue:(id)anObject +{ + + // Truncate the string for speed purposes if it's very long - improves table scrolling speed. + if ([anObject length] > 150) { + return ([NSString stringWithFormat:@"%@...", [anObject substringToIndex:147]]); + } + + return anObject; +} + +// Always provide the full string when editing +- (NSString *)editingStringForObjectValue:(id)anObject +{ + return anObject; +} + +- (BOOL) getObjectValue:(id*) object forString:(NSString*) string errorDescription:(NSString**) error +{ + *object = string; + return YES; +} + +- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes +{ + return [[[NSAttributedString alloc] initWithString:[self stringForObjectValue:anObject] attributes:attributes] autorelease]; +} + +@end diff --git a/Source/SPSQLParser.h b/Source/SPSQLParser.h new file mode 100644 index 00000000..14ac6f9d --- /dev/null +++ b/Source/SPSQLParser.h @@ -0,0 +1,233 @@ +// +// SPSQLParsing.h +// sequel-pro +// +// Created by Rowan Beentje on 18/01/2009. +// Copyright 2009 Rowan Beentje. 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 <Cocoa/Cocoa.h> + + +/* + * This class provides a string class intended for SQL parsing. It extends NSMutableString, + * with the intention that as a string is parsed the parsed content is removed. This also + * allows parsing to occur in "streaming" mode, with parseable content being pulled off the + * start of the string as additional content is appended onto the end of the string, eg from + * a file. + * + * While some methods may look similar to NSScanner methods, and others look like they could be + * achieved with Regex libraries or other string parsing libraries, this class was written with + * the following goals in mind: + * - SQL comments, in "/* ... * /", "#" and "--[\s]" form, are ignored automatically while parsing - + *but* are left in the strings in question, to allow (for example) MySQL-version specific query + support, eg /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT * / + * - Support for quoted strings in most commands, allowing strings quoted with ", ', and ` characters - + including support for \-escaping of the quote characters within "- and '-terminated strings. + * - Optional support for bracket capturing in most commands. This can allow simpler parsing of strings + which also contain subqueries, enums, definitions or groups. + * - Speed should remain high even on large strings due to specific context awareness (ie no reliance + * on complex lookaheads or lookbehinds to achieve the above). + * + * It is anticipated that characterAtIndex: is currently the parsing weak point, and that in future + * this class could be further optimised by working with the underlying object/characters directly. + * This class could also be improved by maintaining an internal parsedTo number to allow streaming + * processing to occur without repetition. + */ + +@interface SPSQLParser : NSMutableString +{ + id string; +} + + +typedef enum _SPCommentTypes { + SPHashComment = 0, + SPDoubleDashComment = 1, + SPCStyleComment = 2 +} SPCommentType; + + +/* + * Removes comments within the current string, trimming "#", "--[/s]", and "/* * /" style strings. + */ +- (void) deleteComments; + +/* + * Removes quotes surrounding the string if present, and un-escapes internal occurrences of the quote character, + * before returning the resulting string. + * If no quotes surround the current string, return the entire string; if the current string contains several + * quoted strings, the first will be returned. + */ +- (NSString *) unquotedString; + +/* + * Removes characters from the string up to the first occurrence of the supplied character. + * "inclusively" controls whether the supplied character is also removed. + * Quoted strings are automatically ignored when looking for the character. + * SQL comments are automatically ignored when looking for the character. + * Returns YES if this caused the string to be shortened, or NO if the character was not encountered. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive; + +/* + * As trimToCharacter: ..., but allows control over whether characters within quoted strings + * are ignored. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied character. + * "inclusively" controls whether the supplied character is also returned. + * Quoted strings are automatically ignored when looking for the character. + * SQL comments are automatically ignored when looking for the character. + * If the character is not found, nil is returned. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive; + +/* + * As stringToCharacter: ..., but allows control over whether characters within quoted strings + * are ignored. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied + * character, also removing them from the string. "trimmingInclusively" controls whether or not the + * supplied character is removed from the string on a successful match, while "returningInclusively" + * controls whether it is included in the returned string. + * Quoted strings are automatically ignored when looking for the characters. + * SQL comments are automatically ignored when looking for the characters. + * If the character is not found, nil is returned. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn; + +/* + * As trimAndReturnStringToCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * Returns characters from the string up to and from the first occurrence of the supplied opening character + * to the appropriate occurrence of the supplied closing character. "inclusively" controls whether the supplied + * characters should also be returned. + * Quoted strings are automatically ignored when looking for the characters. + * SQL comments are automatically ignored when looking for the characters. + * Returns nil if no valid matching string can be found. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive; + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether to skip + * over bracket-enclosed characters, as in subqueries, enums, definitions or groups + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets; + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * As stringFromCharacter: toCharacter: ..., but also trims the string up to the "to" character and + * up to or including the "from" character, depending on whether "trimmingInclusively" is set. + * "returningInclusively" controls whether the supplied characters should also be returned. + * Returns nil if no valid matching string can be found. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn; + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether to + * skip over bracket-enclosed characters, as in subqueries, enums, definitions or groups. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets; + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over both bracketing + * and quoting. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * Split a string on the boundaries formed by the supplied character, returning an array of strings. + * Quoted strings are automatically ignored when looking for the characters. + * SQL comments are automatically ignored when looking for the characters. + * Returns an array with one element containing the entire string if the supplied character is not found. + */ +- (NSArray *) splitStringByCharacter:(unichar)character; + +/* + * As splitStringByCharacter: ..., but allows control over whether to skip over bracket-enclosed + * characters, as in subqueries, enums, definitions or groups. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets; + +/* + * As splitStringByCharacter:, but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSArray *) splitStringByCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * As splitStringByCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; + +/* + * Methods used internally by this class to power the methods above: + */ +- (long) firstOccurrenceOfCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex ignoringQuotedStrings:(BOOL)ignoreQuotedStrings; +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings ; +- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index; +- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index; + + +/* Required and primitive methods to allow subclassing class cluster */ +#pragma mark - +- (id) init; +- (id) initWithBytes:(const void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding; +- (id) initWithBytesNoCopy:(void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)flag; +- (id) initWithCapacity:(unsigned int)capacity; +- (id) initWithCharactersNoCopy:(unichar *)chars length:(unsigned int)length freeWhenDone:(BOOL)flag; +- (id) initWithContentsOfFile:(id)path; +- (id) initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error; +- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding; +- (id) initWithFormat:(NSString *)format, ...; +- (id) initWithFormat:(NSString *)format arguments:(va_list)argList; +- (unsigned int) length; +- (unichar) characterAtIndex:(unsigned int)index; +- (id) description; +- (unsigned int) replaceOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(unsigned)opts range:(NSRange)searchRange; +- (void) setString:(NSString *)string; +- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)string; +- (void) dealloc; +@end
\ No newline at end of file diff --git a/Source/SPSQLParser.m b/Source/SPSQLParser.m new file mode 100644 index 00000000..9828f529 --- /dev/null +++ b/Source/SPSQLParser.m @@ -0,0 +1,655 @@ +// +// SPSQLParsing.m +// sequel-pro +// +// Created by Rowan Beentje on 18/01/2009. +// Copyright 2009 Rowan Beentje. 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 "SPSQLParser.h" + +/* + * Please see the header files for a general description of the purpose of this class, + * and increased overview detail for the functions below. + */ +@implementation SPSQLParser : NSMutableString + + + +/* + * Removes comments within the current string, trimming "#", "--[/s]", and "/* * /" style strings. + */ +- (void) deleteComments +{ + long currentStringIndex, commentEndIndex, quotedStringEndIndex; + unichar currentCharacter; + long stringLength = [string length]; + + // Walk along the string, processing characters. + for (currentStringIndex = 0; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + switch (currentCharacter) { + + // When quote characters are encountered walk to the end of the quoted string. + case '\'': + case '"': + case '`': + quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1]; + if (quotedStringEndIndex == NSNotFound) { + return; + } + currentStringIndex = quotedStringEndIndex; + break; + + // For comments starting "--[\s]", ensure the start syntax is valid before proceeding. + case '-': + if (stringLength < currentStringIndex + 2) break; + if ([string characterAtIndex:currentStringIndex+1] != '-') break; + if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break; + commentEndIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + + case '#': + commentEndIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + + // For comments starting "/*", ensure the start syntax is valid before proceeding. + case '/': + if (stringLength < currentStringIndex + 1) break; + if ([string characterAtIndex:currentStringIndex+1] != '*') break; + commentEndIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + } + } +} + + +/* + * Removes quotes surrounding the string if present, and un-escapes internal occurrences of the quote character before returning. + */ +- (NSString *) unquotedString +{ + NSMutableString *returnString; + long stringEndIndex; + unichar quoteCharacter; + + if (![string length]) return nil; + + // If the first character is not a quote character, return the entire string. + quoteCharacter = [string characterAtIndex:0]; + if (quoteCharacter != '`' && quoteCharacter != '"' && quoteCharacter != '\'') { + return [NSString stringWithString:string]; + } + + // Get the end of the string + stringEndIndex = [self endIndexOfStringQuotedByCharacter:quoteCharacter startingAtIndex:1]; + if (stringEndIndex == NSNotFound) { + return [NSString stringWithString:string]; + } + + // Trim the string appropriately + returnString = [NSMutableString stringWithString:[string substringWithRange:NSMakeRange(1, stringEndIndex-1)]]; + + // Remove escaped characters and escaped strings as appropriate + if (quoteCharacter == '`' || quoteCharacter == '"' || quoteCharacter == '\'') { + [returnString replaceOccurrencesOfString:[NSString stringWithFormat:@"%C%C", quoteCharacter, quoteCharacter] withString:[NSString stringWithFormat:@"%C", quoteCharacter] options:0 range:NSMakeRange(0, [returnString length])]; + } + if (quoteCharacter == '"') { + [returnString replaceOccurrencesOfString:@"\\\"" withString:@"\"" options:0 range:NSMakeRange(0, [returnString length])]; + [returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])]; + } else if (quoteCharacter == '\'') { + [returnString replaceOccurrencesOfString:@"\\'" withString:@"'" options:0 range:NSMakeRange(0, [returnString length])]; + [returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])]; + } + + return returnString; +} + + +/* + * Removes characters from the string up to the first occurrence of the supplied character. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive +{ + return [self trimToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES]; +} + + +/* + * As trimToCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long stringIndex; + + // Get the first occurrence of the specified character, returning NO if not found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return NO; + + // If it has been found, trim the string appropriately and return YES + [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))]; + return YES; +} + + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied character. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive +{ + return [self stringToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES]; +} + + +/* + * As stringToCharacter: ..., but allows control over whether characters within quoted strings + * are ignored. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings { + long stringIndex; + + // Get the first occurrence of the specified character, returning nil if not found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return nil; + + // If it has been found, return the appropriate string range + return [string substringWithRange:NSMakeRange(0, stringIndex + (inclusive?1:0))]; +} + + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied + * character, also removing them from the string. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn +{ + return [self trimAndReturnStringToCharacter:character trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringToCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long stringIndex; + NSString *resultString; + + // Get the first occurrence of the specified character, returning nil if it could not be found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return nil; + + // Select the appropriate string range, truncate the current string, and return the selected string + resultString = [NSString stringWithString:[string substringWithRange:NSMakeRange(0, stringIndex + (inclusiveReturn?1:0))]]; + [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))]; + return resultString; +} + + +/* + * Returns characters from the string up to and from the first occurrence of the supplied opening character + * to the appropriate occurrence of the supplied closing character. "inclusively" controls whether the supplied + * characters should also be returned. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:YES]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether to skip + * over bracket-enclosed characters, as in subqueries, enums, definitions or groups + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long fromCharacterIndex, toCharacterIndex; + + // Look for the first occurrence of the from: character + fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (fromCharacterIndex == NSNotFound) return nil; + + // Look for the first/balancing occurrence of the to: character + toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (toCharacterIndex == NSNotFound) return nil; + + // Return the correct part of the string. + return [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusive?0:1), toCharacterIndex + (inclusive?1:-1) - fromCharacterIndex)]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but also trims the string up to the "to" character and + * up to or including the "from" character, depending on whether "trimmingInclusively" is set. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether to + * skip over bracket-enclosed characters, as in subqueries, enums, definitions or groups. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over both bracketing + * and quoting. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long fromCharacterIndex, toCharacterIndex; + NSString *resultString; + + // Look for the first occurrence of the from: character + fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (fromCharacterIndex == NSNotFound) return nil; + + // Look for the first/balancing occurrence of the to: character + toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (toCharacterIndex == NSNotFound) return nil; + + // Select the correct part of the string, truncate the current string, and return the selected string. + resultString = [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusiveReturn?0:1), toCharacterIndex + (inclusiveReturn?1:-1) - fromCharacterIndex)]; + [string deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)]; + return resultString; +} + +/* + * Split a string on the boundaries formed by the supplied character, returning an array of strings. + */ +- (NSArray *) splitStringByCharacter:(unichar)character +{ + return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:YES]; +} + +/* + * As splitStringByCharacter: ..., but allows control over whether to skip over bracket-enclosed + * characters, as in subqueries, enums, definitions or groups. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets +{ + return [self splitStringByCharacter:character skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As splitStringByCharacter:, but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSArray *) splitStringByCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As splitStringByCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + NSMutableArray *resultsArray = [NSMutableArray array]; + long stringIndex = -1, nextIndex = 0; + + // Walk through the string finding the character to split by, and add non-zero length strings. + while (1) { + nextIndex = [self firstOccurrenceOfCharacter:character afterIndex:stringIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (nextIndex == NSNotFound) { + break; + } + + if (nextIndex - stringIndex - 1 > 0) { + [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]]; + } + stringIndex = nextIndex; + } + + // Add the end of the string after the previously matched character where appropriate. + if (stringIndex + 1 < [string length]) { + [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, [string length] - stringIndex - 1)]]; + } + + return resultsArray; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self firstOccurrenceOfCharacter:character afterIndex:-1 skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self firstOccurrenceOfCharacter:character afterIndex:startIndex skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long currentStringIndex, quotedStringEndIndex; + unichar currentCharacter; + long stringLength = [string length]; + int bracketingLevel = 0; + + // Sanity check inputs + if (startIndex < -1) startIndex = -1; + + // Walk along the string, processing characters + for (currentStringIndex = startIndex + 1; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + + // Check for the ending character, and if it has been found and quoting/brackets is valid, return. + if (currentCharacter == character) { + if (!skipBrackets || bracketingLevel <= 0) { + return currentStringIndex; + } + } + + // Process strings and comments as appropriate + switch (currentCharacter) { + + // When quote characters are encountered and strings are not being ignored, walk to the end of the quoted string. + case '\'': + case '"': + case '`': + if (!ignoreQuotedStrings) break; + quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1]; + if (quotedStringEndIndex == NSNotFound) { + return NSNotFound; + } + currentStringIndex = quotedStringEndIndex; + break; + + // For opening brackets increment the bracket count + case '(': + bracketingLevel++; + break; + + // For closing brackets decrement the bracket count + case ')': + bracketingLevel--; + + // For comments starting "--[\s]", ensure the start syntax is valid before proceeding. + case '-': + if (stringLength < currentStringIndex + 2) break; + if ([string characterAtIndex:currentStringIndex+1] != '-') break; + if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break; + currentStringIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex]; + break; + + case '#': + currentStringIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex]; + break; + + // For comments starting "/*", ensure the start syntax is valid before proceeding. + case '/': + if (stringLength < currentStringIndex + 1) break; + if ([string characterAtIndex:currentStringIndex+1] != '*') break; + currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex]; + break; + } + } + + // If no matches have been made in this string, return NSNotFound. + return NSNotFound; +} + +/* + * A method intended for use by the functions above. + */ +- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index +{ + long currentStringIndex, stringLength, i, quotedStringLength; + BOOL characterIsEscaped; + unichar currentCharacter; + + stringLength = [string length]; + + // Walk the string looking for the string end + for ( currentStringIndex = index; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + + // If the string end is a backtick and one has been encountered, treat it as end of string + if (quoteCharacter == '`' && currentCharacter == '`') { + + // ...as long as the next character isn't also a backtick, in which case it's being quoted. Skip both. + if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == '`') { + currentStringIndex++; + continue; + } + + return currentStringIndex; + + // Otherwise, prepare to treat the string as ended when meeting the correct boundary character.... + } else if (currentCharacter == quoteCharacter) { + + // ...but only if the string end isn't escaped with an *odd* number of escaping characters... + characterIsEscaped = NO; + i = 1; + quotedStringLength = currentStringIndex - 1; + while ((quotedStringLength - i) > 0 && [string characterAtIndex:currentStringIndex - i] == '\\') { + characterIsEscaped = !characterIsEscaped; + i++; + } + + // If an even number have been found, it may be the end of the string - as long as the subsequent character + // isn't also the same character, in which case it's another form of escaping. + if (!characterIsEscaped) { + if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == quoteCharacter) { + currentStringIndex++; + continue; + } + + // Really is the end of the string. + return currentStringIndex; + } + } + } + + return NSNotFound; +} + +/* + * A method intended for use by the functions above. + */ +- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index +{ + long stringLength = [string length]; + unichar currentCharacter; + + switch (commentType) { + + // For comments of type "--[\s]", start the comment processing two characters in to match the start syntax, + // then flow into the Hash comment handling (looking for first newline). + case SPDoubleDashComment: + index = index+2; + + // For comments starting "--[\s]" and "#", continue until the first newline. + case SPHashComment: + index++; + for ( ; index < stringLength; index++ ) { + currentCharacter = [string characterAtIndex:index]; + if (currentCharacter == '\r' || currentCharacter == '\n') { + return index-1; + } + } + break; + + // For comments starting "/*", start the comment processing one character in to match the start syntax, then + // continue until the first matching "*/". + case SPCStyleComment: + index = index+2; + for ( ; index < stringLength; index++ ) { + if ([string characterAtIndex:index] == '*') { + if ((stringLength > index + 1) && [string characterAtIndex:index+1] == '/') { + return (index+1); + } + } + } + } + + // If no match has been found, the comment must continue until the very end of the string. + return (stringLength-1); +} + + +/* Required and primitive methods to allow subclassing class cluster */ +#pragma mark - +- (id) init { + if (self = [super init]) { + string = [[NSMutableString string] retain]; + } + return self; +} +- (id) initWithBytes:(const void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithBytes:bytes length:length encoding:encoding]; + } + return self; +} +- (id) initWithBytesNoCopy:(void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)flag { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithBytesNoCopy:bytes length:length encoding:encoding freeWhenDone:flag]; + } + return self; +} +- (id) initWithCapacity:(unsigned int)capacity { + if (self = [super init]) { + string = [[NSMutableString stringWithCapacity:capacity] retain]; + } + return self; +} +- (id) initWithCharactersNoCopy:(unichar *)characters length:(unsigned int)length freeWhenDone:(BOOL)flag { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithCharactersNoCopy:characters length:length freeWhenDone:flag]; + } + return self; +} +- (id) initWithContentsOfFile:(id)path { + return [self initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; +} +- (id) initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding error:(NSError **)error { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithContentsOfFile:path encoding:encoding error:error]; + } + return self; +} +- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithCString:nullTerminatedCString encoding:encoding]; + } + return self; +} +- (id) initWithFormat:(NSString *)format, ... { + va_list argList; + va_start(argList, format); + id str = [self initWithFormat:format arguments:argList]; + va_end(argList); + return str; +} +- (id) initWithFormat:(NSString *)format arguments:(va_list)argList { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithFormat:format arguments:argList]; + } + return self; +} +- (unsigned int) length { + return [string length]; +} +- (unichar) characterAtIndex:(unsigned int)index { + return [string characterAtIndex:index]; +} +- (id) description { + return [string description]; +} +- (unsigned int) replaceOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(unsigned)options range:(NSRange)searchRange { + return [string replaceOccurrencesOfString:target withString:replacement options:options range:searchRange]; +} +- (void) setString:(NSString *)aString { + [string setString:aString]; +} +- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)aString { + [string replaceCharactersInRange:range withString:aString]; +} +- (void) dealloc { + [string release]; + [super dealloc]; +} +@end
\ No newline at end of file diff --git a/Source/SPTableData.h b/Source/SPTableData.h new file mode 100644 index 00000000..fd069a6b --- /dev/null +++ b/Source/SPTableData.h @@ -0,0 +1,54 @@ +// +// SPTableData.h +// 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 <Cocoa/Cocoa.h> + + +@interface SPTableData : NSObject { + IBOutlet id tableDocumentInstance; + IBOutlet id tableListInstance; + + NSMutableArray *columns; + NSMutableArray *columnNames; + NSMutableDictionary *status; + NSString *tableEncoding; + + CMMCPConnection *mySQLConnection; +} + +- (void) setConnection:(CMMCPConnection *)theConnection; +- (NSString *) tableEncoding; +- (NSArray *) columns; +- (NSDictionary *) columnWithName:(NSString *)colName; +- (NSArray *) columnNames; +- (NSDictionary *) columnAtIndex:(int)index; +- (NSString *) statusValueForKey:(NSString *)aKey; +- (NSDictionary *) statusValues; +- (void) resetAllData; +- (void) resetStatusData; +- (void) resetColumnData; +- (BOOL) updateInformationFromCreateTableForCurrentTable; +- (NSDictionary *) informationFromCreateTableSyntaxForTable:(NSString *)tableName; +- (BOOL) updateStatusInformationForCurrentTable; + +@end 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 diff --git a/Source/SPTableInfo.h b/Source/SPTableInfo.h index 4088feaf..901b2573 100644 --- a/Source/SPTableInfo.h +++ b/Source/SPTableInfo.h @@ -27,6 +27,7 @@ IBOutlet id infoTable; IBOutlet id tableList; IBOutlet id tableListInstance; + IBOutlet id tableDataInstance; IBOutlet id tableDocumentInstance; NSMutableArray *info; diff --git a/Source/SPTableInfo.m b/Source/SPTableInfo.m index 7bab9322..80760278 100644 --- a/Source/SPTableInfo.m +++ b/Source/SPTableInfo.m @@ -25,6 +25,8 @@ #import "CMMCPConnection.h" #import "CMMCPResult.h" #import "TableDocument.h" +#import "TablesList.h" +#import "SPTableData.h" #import "SPStringAdditions.h" #import <MCPKit_bundled/MCPKit_bundled.h> @@ -101,72 +103,64 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn - (void)tableChanged:(NSNotification *)notification { - NSString *query; - CMMCPResult *theResult; - NSDictionary *theRow; + NSDictionary *tableStatus; [info removeAllObjects]; [info addObject:@"TABLE INFORMATION"]; - if ([tableListInstance table]) + if ([tableListInstance tableName]) { - if ([(NSString *)[tableListInstance table] isEqualToString:@""]) { + if ([[tableListInstance tableName] isEqualToString:@""]) { [info addObject:@"multiple tables"]; - + } else { - // Notify that we are about to perform a query - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; - // Create the query and get results - query = [NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", [tableListInstance table]]; - - // This line triggers a bug when opening a new window. but only after having closed a window - theResult = [[tableDocumentInstance sharedConnection] queryString:query]; + // Retrieve the table status information via the data cache + tableStatus = [tableDataInstance statusValues]; // Check for errors - if (![[[tableDocumentInstance sharedConnection] getLastErrorMessage] isEqualToString:@""]) { + if (![tableStatus count]) { [info addObject:@"error occurred"]; return; } - - // Process result - theRow = [[theResult fetch2DResultAsType:MCPTypeDictionary] lastObject]; - + // Check for "Create_time" == NULL - if (![[theRow objectForKey:@"Create_time"] isNSNull]) { - // Setup our data formatter + if (![[tableStatus objectForKey:@"Create_time"] isNSNull]) { + + // Set up our data formatter NSDateFormatter *createDateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [createDateFormatter setDateStyle:NSDateFormatterShortStyle]; [createDateFormatter setTimeStyle:NSDateFormatterNoStyle]; - + // Convert our string date from the result to an NSDate. - NSDate *create_date = [NSDate dateWithNaturalLanguageString:[theRow objectForKey:@"Create_time"]]; - + NSDate *create_date = [NSDate dateWithNaturalLanguageString:[tableStatus objectForKey:@"Create_time"]]; + // Add the creation date to the infoTable [info addObject:[NSString stringWithFormat:@"created: %@", [createDateFormatter stringFromDate:create_date]]]; } // Check for "Update_time" == NULL - InnoDB tables don't have an update time - if (![[theRow objectForKey:@"Update_time"] isNSNull]) { + if (![[tableStatus objectForKey:@"Update_time"] isNSNull]) { + // Setup our data formatter NSDateFormatter *updateDateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [updateDateFormatter setDateStyle:NSDateFormatterShortStyle]; [updateDateFormatter setTimeStyle:NSDateFormatterNoStyle]; - + // Convert our string date from the result to an NSDate. - NSDate *update_date = [NSDate dateWithNaturalLanguageString:[theRow objectForKey:@"Update_time"]]; - + NSDate *update_date = [NSDate dateWithNaturalLanguageString:[tableStatus objectForKey:@"Update_time"]]; + // Add the update date to the infoTable [info addObject:[NSString stringWithFormat:@"updated: %@", [updateDateFormatter stringFromDate:update_date]]]; } - [info addObject:[NSString stringWithFormat:@"rows: %@", [theRow objectForKey:@"Rows"]]]; - [info addObject:[NSString stringWithFormat:@"size: %@", [NSString stringForByteSize:[[theRow objectForKey:@"Data_length"] intValue]]]]; - [info addObject:[NSString stringWithFormat:@"encoding: %@", [[[theRow objectForKey:@"Collation"] componentsSeparatedByString:@"_"] objectAtIndex:0]]]; - [info addObject:[NSString stringWithFormat:@"auto_increment: %@", [theRow objectForKey:@"Auto_increment"]]]; + [info addObject:[NSString stringWithFormat:@"rows: ~%@", [tableStatus objectForKey:@"Rows"]]]; + [info addObject:[NSString stringWithFormat:@"size: %@", [NSString stringForByteSize:[[tableStatus objectForKey:@"Data_length"] intValue]]]]; + [info addObject:[NSString stringWithFormat:@"encoding: %@", [tableDataInstance tableEncoding]]]; - // Notify that we've finished performing the query - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + if (![[tableStatus objectForKey:@"Auto_increment"] isNSNull]) { + [info addObject:[NSString stringWithFormat:@"auto_increment: %@", [tableStatus objectForKey:@"Auto_increment"]]]; + } } } diff --git a/Source/TableContent.h b/Source/TableContent.h index 54ad2481..ac7a8249 100644 --- a/Source/TableContent.h +++ b/Source/TableContent.h @@ -35,7 +35,7 @@ IBOutlet id tableDocumentInstance; IBOutlet id tablesListInstance; - IBOutlet id tableSourceInstance; + IBOutlet id tableDataInstance; IBOutlet id tableWindow; IBOutlet CMCopyTable *tableContentView; @@ -63,7 +63,6 @@ NSString *selectedTable; NSMutableArray *fullResult, *filteredResult, *keys; NSMutableDictionary *oldRow; - NSArray *fieldNames, *fieldTypes; NSString *compareType, *sortField; BOOL isEditingRow, isEditingNewRow, isDesc, setLimit; NSUserDefaults *prefs; @@ -103,7 +102,7 @@ - (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult; - (BOOL)addRowToDB; - (NSString *)argumentForRow:(int)row; -- (BOOL)isBlobOrText:(NSString *)fieldType; +- (BOOL)tableContainsBlobOrTextColumns; - (NSString *)fieldListForQuery; - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; - (int)getNumberOfRows; @@ -111,7 +110,7 @@ //tableView datasource methods - (int)numberOfRowsInTableView:(NSTableView *)aTableView; -- (id)tableView:(NSTableView *)aTableView +- (id)tableView:(CMCopyTable *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex; - (void)tableView:(NSTableView *)aTableView diff --git a/Source/TableContent.m b/Source/TableContent.m index 211ee350..04df0953 100644 --- a/Source/TableContent.m +++ b/Source/TableContent.m @@ -26,9 +26,9 @@ #import "TableContent.h" #import "TableDocument.h" #import "TablesList.h" -#import "TableSource.h" #import "CMImageView.h" - +#import "SPDataCellFormatter.h" +#import "SPTableData.h" @implementation TableContent @@ -59,7 +59,8 @@ { int i; NSNumber *colWidth; - NSArray *theColumns; + NSArray *theColumns, *columnNames; + NSDictionary *columnDefinition; NSTableColumn *theCol; NSString *query; CMMCPResult *queryResult; @@ -82,15 +83,17 @@ [tableContentView scrollRowToVisible:0]; [tableContentView scrollColumnToVisible:0]; + // Remove existing columns from the table + theColumns = [tableContentView tableColumns]; + while ([theColumns count]) { + [tableContentView removeTableColumn:[theColumns objectAtIndex:0]]; + } + // If no table has been supplied, reset the view to a blank table and disabled elements if ( [aTable isEqualToString:@""] || !aTable ) { - // Empty the table and stored data arrays - theColumns = [tableContentView tableColumns]; - while ([theColumns count]) { - [tableContentView removeTableColumn:[theColumns objectAtIndex:0]]; - } + // Empty the stored data arrays [fullResult removeAllObjects]; [filteredResult removeAllObjects]; [tableContentView reloadData]; @@ -126,40 +129,27 @@ // Post a notification that a query will be performed [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; - - // Make a fast query to get fieldNames and fieldTypes for this table. This is used to decide whether to preserve the - // current filter/sort settings, and also used when grabbing all the data as part of the fieldListForQuery method. - queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@` LIMIT 0", selectedTable]]; - if ( queryResult == nil ) { - NSLog(@"Loading table columns for %@ failed", aTable); - return; - } - fieldNames = [[queryResult fetchFieldNames] retain]; - fieldTypes = [[queryResult fetchTypesAsArray] retain]; + // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part + // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. + theColumns = [tableDataInstance columns]; + columnNames = [tableDataInstance columnNames]; // Retrieve the number of rows in the table and initially mark all as being visible. numRows = [self getNumberOfRows]; areShowingAllRows = YES; - - // Remove existing columns from the table - theColumns = [tableContentView tableColumns]; - while ([theColumns count]) { - [tableContentView removeTableColumn:[theColumns objectAtIndex:0]]; - } // Add the new columns to the table - for ( i = 0 ; i < [fieldNames count] ; i++ ) { - + for ( i = 0 ; i < [theColumns count] ; i++ ) { + columnDefinition = [theColumns objectAtIndex:i]; + // Set up the column - theCol = [[NSTableColumn alloc] initWithIdentifier:[fieldNames objectAtIndex:i]]; + theCol = [[NSTableColumn alloc] initWithIdentifier:[columnDefinition objectForKey:@"name"]]; + [[theCol headerCell] setStringValue:[columnDefinition objectForKey:@"name"]]; [theCol setEditable:YES]; - [[theCol headerCell] setStringValue:[fieldNames objectAtIndex:i]]; - // Set up the data cell depending on the column type NSComboBoxCell *dataCell; - if ( [[tableSourceInstance enumFields] objectForKey:[fieldNames objectAtIndex:i]] ) - { + if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"enum"]) { dataCell = [[[NSComboBoxCell alloc] initTextCell:@""] autorelease]; [dataCell setButtonBordered:NO]; [dataCell setBezeled:NO]; @@ -167,33 +157,29 @@ [dataCell setCompletes:YES]; [dataCell setControlSize:NSSmallControlSize]; [dataCell addItemWithObjectValue:@"NULL"]; - [dataCell addItemsWithObjectValues:[[tableSourceInstance enumFields] objectForKey:[fieldNames objectAtIndex:i]]]; - } - else - { + [dataCell addItemsWithObjectValues:[columnDefinition objectForKey:@"values"]]; + } else { dataCell = [[[NSTextFieldCell alloc] initTextCell:@""] autorelease]; } [dataCell setEditable:YES]; - + + // Set the line break mode and an NSFormatter subclass which truncates long strings for display [dataCell setLineBreakMode:NSLineBreakByTruncatingTail]; - + [dataCell setFormatter:[[SPDataCellFormatter new] autorelease]]; + // Set the data cell font according to the preferences - if ( [prefs boolForKey:@"useMonospacedFonts"] ) - { + if ( [prefs boolForKey:@"useMonospacedFonts"] ) { [dataCell setFont:[NSFont fontWithName:@"Monaco" size:10]]; - } - else - { + } else { [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } - + // Assign the data cell [theCol setDataCell:dataCell]; // Set the width of this column to saved value if exists - colWidth = [[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:[NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]] objectForKey:[tablesListInstance table]] objectForKey:[fieldNames objectAtIndex:i]]; - if ( colWidth ) - { + colWidth = [[[[prefs objectForKey:@"tableColumnWidths"] objectForKey:[NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]] objectForKey:[tablesListInstance tableName]] objectForKey:[columnDefinition objectForKey:@"name"]]; + if ( colWidth ) { [theCol setWidth:[colWidth floatValue]]; } @@ -203,8 +189,7 @@ } // If the table has been reloaded and the previously selected sort column is still present, reselect it. - if (preserveCurrentView && [fieldNames containsObject:sortField]) - { + if (preserveCurrentView && [columnNames containsObject:sortField]) { theCol = [tableContentView tableColumnWithIdentifier:sortField]; [tableContentView setHighlightedTableColumn:theCol]; if ( isDesc ) { @@ -212,18 +197,15 @@ } else { [tableContentView setIndicatorImage:[NSImage imageNamed:@"NSAscendingSortIndicator"] inTableColumn:theCol]; } - } // Otherwise, clear sorting - else - { + } else { sortField = nil; isDesc = NO; } // Preserve the stored filter settings if appropriate - if (preserveCurrentView && [fieldField isEnabled]) - { + if (preserveCurrentView && [fieldField isEnabled]) { preservedFilterField = [NSString stringWithString:[[fieldField selectedItem] title]]; preservedFilterComparison = [NSString stringWithString:[[compareField selectedItem] title]]; preservedFilterValue = [NSString stringWithString:[argumentField stringValue]]; @@ -232,7 +214,7 @@ // Enable and initialize filter fields (with tags for position of menu item and field position) [fieldField setEnabled:YES]; [fieldField removeAllItems]; - [fieldField addItemsWithTitles:fieldNames]; + [fieldField addItemsWithTitles:columnNames]; for ( i = 0 ; i < [fieldField numberOfItems] ; i++ ) { [[fieldField itemAtIndex:i] setTag:i]; } @@ -243,23 +225,20 @@ [filterButton setEnabled:YES]; // Restore preserved filter settings if appropriate and valid - if (preserveCurrentView && preservedFilterField != nil && [fieldField itemWithTitle:preservedFilterField]) - { + if (preserveCurrentView && preservedFilterField != nil && [fieldField itemWithTitle:preservedFilterField]) { [fieldField selectItemWithTitle:preservedFilterField]; [self setCompareTypes:self]; } if (preserveCurrentView && preservedFilterField != nil && [fieldField itemWithTitle:preservedFilterField] - && [compareField itemWithTitle:preservedFilterComparison]) - { + && [compareField itemWithTitle:preservedFilterComparison]) { [compareField selectItemWithTitle:preservedFilterComparison]; [argumentField setStringValue:preservedFilterValue]; areShowingAllRows = NO; } // Enable or disable the limit fields according to preference setting - if ( [prefs boolForKey:@"limitRows"] ) - { + if ( [prefs boolForKey:@"limitRows"] ) { // Attempt to preserve the limit value if it's still valid if (!preserveCurrentView || [limitRowsField intValue] < 1 || [limitRowsField intValue] >= numRows) { @@ -272,9 +251,7 @@ [prefs integerForKey:@"limitRowsValue"]]]; if ([prefs integerForKey:@"limitRowsValue"] < numRows) areShowingAllRows = NO; - } - else - { + } else { [limitRowsField setEnabled:NO]; [limitRowsButton setEnabled:NO]; [limitRowsStepper setEnabled:NO]; @@ -287,19 +264,15 @@ [copyButton setEnabled:YES]; [removeButton setEnabled:YES]; - // Perform the data query and store the result as an array containing a dictionary per result row query = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; - if ( sortField ) - { + if ( sortField ) { query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField]; if ( isDesc ) query = [query stringByAppendingString:@" DESC"]; } - if ( [prefs boolForKey:@"limitRows"] ) - { - if ( [limitRowsField intValue] <= 0 ) - { + if ( [prefs boolForKey:@"limitRows"] ) { + if ( [limitRowsField intValue] <= 0 ) { [limitRowsField setStringValue:@"1"]; } query = [query stringByAppendingString: @@ -329,24 +302,29 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; } + /* - Reloads the current table data, performing a new SQL query. Now attempts to preserve sort order, filters, and viewport. + * Reloads the current table data, performing a new SQL query. Now attempts to preserve sort order, filters, and viewport. */ - (IBAction)reloadTable:(id)sender { // Store the current viewport location NSRect viewRect = [tableContentView visibleRect]; + // Clear the table data column cache + [tableDataInstance resetColumnData]; + [self loadTable:selectedTable]; // Restore the viewport [tableContentView scrollRectToVisible:viewRect]; } -- (IBAction)reloadTableValues:(id)sender + /* - reload the table values without reconfiguring the tableView (with filter and limit if set) + * Reload the table values without reconfiguring the tableView (with filter and limit if set) */ +- (IBAction)reloadTableValues:(id)sender { NSString *queryString; CMMCPResult *queryResult; @@ -403,8 +381,9 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; } -/** - * filter the table with arguments given by the user + +/* + * Filter the table with arguments given by the user */ - (IBAction)filterTable:(id)sender { @@ -617,25 +596,32 @@ } -//edit methods -- (IBAction)addRow:(id)sender +#pragma mark Edit methods + /* - adds an empty row to the table-array and goes into edit mode + * Adds an empty row to the table-array and goes into edit mode */ +- (IBAction)addRow:(id)sender { - NSMutableDictionary *newRow = [NSMutableDictionary dictionary]; + NSArray *columns; + NSMutableDictionary *column, *newRow = [NSMutableDictionary dictionary]; int i; - + if ( ![self selectionShouldChangeInTableView:nil] ) return; - - for ( i = 0 ; i < [fieldNames count] ; i++ ) { - // [newRow setObject:[prefs stringForKey:@"nullValue"] forKey:[fieldNames objectAtIndex:i]]; - [newRow setObject:[tableSourceInstance defaultValueForField:[fieldNames objectAtIndex:i]] - forKey:[fieldNames objectAtIndex:i]]; + + columns = [[NSArray alloc] initWithArray:[tableDataInstance columns]]; + for ( i = 0 ; i < [columns count] ; i++ ) { + column = [columns objectAtIndex:i]; + if ([column objectForKey:@"default"] == nil) { + [newRow setObject:[prefs stringForKey:@"nullValue"] forKey:[column objectForKey:@"name"]]; + } else { + [newRow setObject:[column objectForKey:@"default"] forKey:[column objectForKey:@"name"]]; + } } [filteredResult addObject:newRow]; - + [columns release]; + isEditingRow = YES; isEditingNewRow = YES; [tableContentView reloadData]; @@ -905,7 +891,7 @@ //load table if not already done if ( ![tablesListInstance contentLoaded] ) { - [self loadTable:(NSString *)[tablesListInstance table]]; + [self loadTable:[tablesListInstance tableName]]; } tableColumns = [tableContentView tableColumns]; @@ -966,28 +952,16 @@ */ - (IBAction)setCompareTypes:(id)sender { - NSArray *stringFields = [NSArray arrayWithObjects:@"varstring", @"string", @"tinyblob", @"blob", @"mediumblob", @"longblob", @"set", @"enum", nil]; NSArray *stringTypes = [NSArray arrayWithObjects:NSLocalizedString(@"is", @"popup menuitem for field IS value"), NSLocalizedString(@"is not", @"popup menuitem for field IS NOT value"), NSLocalizedString(@"contains", @"popup menuitem for field CONTAINS value"), NSLocalizedString(@"contains not", @"popup menuitem for field CONTAINS NOT value"), @"IN", nil]; - NSArray *numberFields = [NSArray arrayWithObjects:@"tiny", @"short", @"long", @"int24", @"longlong", @"decimal", @"float", @"double", nil]; NSArray *numberTypes = [NSArray arrayWithObjects:@"=", @"≠", @">", @"<", @"≥", @"≤", @"IN", nil]; - NSArray *dateFields = [NSArray arrayWithObjects:@"timestamp", @"date", @"time", @"datetime", @"year", nil]; NSArray *dateTypes = [NSArray arrayWithObjects:NSLocalizedString(@"is", @"popup menuitem for field IS value"), NSLocalizedString(@"is not", @"popup menuitem for field IS NOT value"), NSLocalizedString(@"older than", @"popup menuitem for field OLDER THAN value"), NSLocalizedString(@"younger than", @"popup menuitem for field YOUNGER THAN value"), NSLocalizedString(@"older than or equal to", @"popup menuitem for field OLDER THAN OR EQUAL TO value"), NSLocalizedString(@"younger than or equal to", @"popup menuitem for field YOUNGER THAN OR EQUAL TO value"), nil]; - NSString *fieldType = [NSString stringWithString:[fieldTypes objectAtIndex:[[fieldField selectedItem] tag]]]; + NSString *fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"typegrouping"]]; int i; [compareField removeAllItems]; - // Why do we get "string" for enum fields? (error in framework?) - if ( [stringFields containsObject:fieldType] ) { - [compareField addItemsWithTitles:stringTypes]; - compareType = @"string"; - // [argumentField setFormatter:nil]; - } else if ( [numberFields containsObject:fieldType] ) { - [compareField addItemsWithTitles:numberTypes]; - compareType = @"number"; - // [argumentField setFormatter:numberFormatter]; - } else if ( [dateFields containsObject:fieldType] ) { + if ( [fieldTypeGrouping isEqualToString:@"date"] ) { [compareField addItemsWithTitles:dateTypes]; compareType = @"date"; /* @@ -1008,8 +982,22 @@ [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y" allowNaturalLanguage:YES]]; } */ - } else { - NSLog(@"ERROR: unknown type for comparision: %@", fieldType); + + // TODO: A bug in the framework previously meant enum fields had to be treated as string fields for the purposes + // of comparison - this can now be split out to support additional comparison fucntionality if desired. + } else if ([fieldTypeGrouping isEqualToString:@"string"] || [fieldTypeGrouping isEqualToString:@"binary"] + || [fieldTypeGrouping isEqualToString:@"textdata"] || [fieldTypeGrouping isEqualToString:@"blobdata"] + || [fieldTypeGrouping isEqualToString:@"enum"]) { + [compareField addItemsWithTitles:stringTypes]; + compareType = @"string"; + // [argumentField setFormatter:nil]; + } else if ([fieldTypeGrouping isEqualToString:@"bit"] || [fieldTypeGrouping isEqualToString:@"integer"] + || [fieldTypeGrouping isEqualToString:@"float"]) { + [compareField addItemsWithTitles:numberTypes]; + compareType = @"number"; + // [argumentField setFormatter:numberFormatter]; + } else { + NSLog(@"ERROR: unknown type for comparision: %@, in %@", [[tableDataInstance columnWithName:[[fieldField selectedItem] title]] objectForKey:@"type"], fieldTypeGrouping); } // Add IS NULL and IS NOT NULL as they should always be available @@ -1044,57 +1032,69 @@ [limitRowsStepper setIntValue:0]; } -- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult /* - fetches the result as an array with a dictionary for each row in it + * Fetches the result as an array with a dictionary for each row in it */ +- (NSArray *)fetchResultAsArray:(CMMCPResult *)theResult { - NSMutableArray *tempResult = [NSMutableArray array]; + NSArray *columns; + NSString *columnTypeGrouping; + NSMutableArray *columnsBlobStatus, *tempResult = [NSMutableArray array]; NSDictionary *tempRow; NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary]; NSEnumerator *enumerator; id key; - int i,j; - + int i, j; + BOOL columnIsBlobOrText; + + columns = [tableDataInstance columns]; + columnsBlobStatus = [[NSMutableArray alloc] init]; + for ( i = 0 ; i < [columns count]; i++ ) { + columnTypeGrouping = [[columns objectAtIndex:i] objectForKey:@"typegrouping"]; + columnIsBlobOrText = ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]); + [columnsBlobStatus addObject:[NSNumber numberWithBool:columnIsBlobOrText]]; + } + if ([theResult numOfRows]) [theResult dataSeek:0]; for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { tempRow = [theResult fetchRowAsDictionary]; enumerator = [tempRow keyEnumerator]; + while ( key = [enumerator nextObject] ) { if ( [[tempRow objectForKey:key] isMemberOfClass:[NSNull class]] ) { [modifiedRow setObject:[prefs stringForKey:@"nullValue"] forKey:key]; - /* - //NSData objects now decoded in tableView:objectValueForTableColumn:row - //object in result remains a NSData object - } else if ( [[tempRow objectForKey:key] isKindOfClass:[NSData class]] ) { - [modifiedRow setObject:[[NSString alloc] initWithData:[tempRow objectForKey:key] encoding:[mySQLConnection encoding]] - forKey:key]; - */ } else { [modifiedRow setObject:[tempRow objectForKey:key] forKey:key]; } - //add values for hidden blob and text fields - if ( [prefs boolForKey:@"dontShowBlob"] ) { - for ( j = 0 ; j < [fieldTypes count] ; j++ ) { - if ( [self isBlobOrText:[fieldTypes objectAtIndex:j]] ) { - [modifiedRow setObject:NSLocalizedString(@"- blob or text -", @"value shown for hidden blob and text fields") forKey:[fieldNames objectAtIndex:j]]; - } + } + + // Add values for hidden blob and text fields if appropriate + if ( [prefs boolForKey:@"dontShowBlob"] ) { + for ( j = 0 ; j < [columns count] ; j++ ) { + if ( [[columnsBlobStatus objectAtIndex:j] boolValue] ) { + [modifiedRow setObject:NSLocalizedString(@"- blob or text -", @"value shown for hidden blob and text fields") forKey:[[columns objectAtIndex:j] objectForKey:@"name"]]; } } } + [tempResult addObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]]; } + + [columnsBlobStatus release]; + return tempResult; } -- (BOOL)addRowToDB + /* - tries to write row to mysql-db - returns YES if row written to db, otherwies NO - returns YES if no row is beeing edited and nothing has to be written to db + * Tries to write a new row to the database. + * Returns YES if row is written to database, otherwise NO; also returns YES if no row + * is being edited and nothing has to be written to the database. */ +- (BOOL)addRowToDB { int rowIndex = [tableContentView selectedRow]; + NSArray *theColumns, *columnNames; NSMutableArray *fieldValues = [[NSMutableArray alloc] init]; NSMutableString *queryString; NSString *query; @@ -1108,27 +1108,34 @@ [fieldValues release]; return YES; } + + // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part + // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. + theColumns = [tableDataInstance columns]; + columnNames = [tableDataInstance columnNames]; - //get field values - for ( i=0 ; i < [fieldNames count] ; i++) { - rowObject = [[filteredResult objectAtIndex:rowIndex] objectForKey:[fieldNames objectAtIndex:i]]; - //convert the object to a string (here we can add special treatment for date-, number- and data-fields) - if ( [[rowObject description] isEqualToString:[prefs stringForKey:@"nullValue"]] || - ([rowObject isMemberOfClass:[NSString class]] && [[rowObject description] isEqualToString:@""]) ) { + // Get the field values + for ( i = 0 ; i < [columnNames count] ; i++ ) { + rowObject = [[filteredResult objectAtIndex:rowIndex] objectForKey:[columnNames objectAtIndex:i]]; + + // Convert the object to a string (here we can add special treatment for date-, number- and data-fields) + if ( [[rowObject description] isEqualToString:[prefs stringForKey:@"nullValue"]] + || ([rowObject isMemberOfClass:[NSString class]] && [[rowObject description] isEqualToString:@""]) ) { + //NULL when user entered the nullValue string defined in the prefs or when a number field isn't set // problem: when a number isn't set, sequel-pro enters 0 // -> second if argument isn't necessary! [rowValue setString:@"NULL"]; } else { + + // I don't believe any of these class matches are ever met at present. if ( [rowObject isKindOfClass:[NSCalendarDate class]] ) { - // [rowValue setString:[NSString stringWithFormat:@"\"%@\"", [mySQLConnection prepareString:[rowObject description]]]]; [rowValue setString:[NSString stringWithFormat:@"'%@'", [mySQLConnection prepareString:[rowObject description]]]]; } else if ( [rowObject isKindOfClass:[NSNumber class]] ) { [rowValue setString:[rowObject stringValue]]; } else if ( [rowObject isKindOfClass:[NSData class]] ) { [rowValue setString:[NSString stringWithFormat:@"X'%@'", [mySQLConnection prepareBinaryData:rowObject]]]; } else { - // [rowValue setString:[NSString stringWithFormat:@"\"%@\"", [mySQLConnection prepareString:[rowObject description]]]]; if ( [[rowObject description] isEqualToString:@"CURRENT_TIMESTAMP"] ) { [rowValue setString:@"CURRENT_TIMESTAMP"]; } else { @@ -1136,44 +1143,31 @@ } } } - //escape special characters -> now escaped by framework - /* - for ( j = 0 ; j < [rowValue length] ; j++ ) { - if ( [rowValue characterAtIndex:j] == '\\' ) { - [rowValue insertString:@"\\" atIndex:j]; - j++; - } else if ( [rowValue characterAtIndex:j] == '"' ) { - [rowValue insertString:@"\\" atIndex:j]; - j++; - } - } - */ [fieldValues addObject:[NSString stringWithString:rowValue]]; } + // Use INSERT syntax when creating new rows if ( isEditingNewRow ) { - //INSERT syntax queryString = [NSString stringWithFormat:@"INSERT INTO `%@` (`%@`) VALUES (%@)", - selectedTable, [fieldNames componentsJoinedByString:@"`,`"], [fieldValues componentsJoinedByString:@","]]; + selectedTable, [columnNames componentsJoinedByString:@"`,`"], [fieldValues componentsJoinedByString:@","]]; + + // Use UPDATE syntax otherwise } else { - //UPDATE syntax queryString = [NSMutableString stringWithFormat:@"UPDATE `%@` SET ", selectedTable]; - for ( i = 0 ; i < [fieldNames count] ; i++ ) { + for ( i = 0 ; i < [columnNames count] ; i++ ) { if ( i > 0 ) { [queryString appendString:@", "]; } [queryString appendString:[NSString stringWithFormat:@"`%@`=%@", - [fieldNames objectAtIndex:i], [fieldValues objectAtIndex:i]]]; + [columnNames objectAtIndex:i], [fieldValues objectAtIndex:i]]]; } - [fieldValues release]; [queryString appendString:[NSString stringWithFormat:@" WHERE %@", [self argumentForRow:-2]]]; } [mySQLConnection queryString:queryString]; + [fieldValues release]; - //NSLog( @"%@", queryString ); - + // If no rows have been changed, show error if appropriate. if ( ![mySQLConnection affectedRows] ) { - //no rows changed if ( [prefs boolForKey:@"showError"] ) { NSBeginAlertSheet(NSLocalizedString(@"Warning", @"warning"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"The row was not written to the MySQL database. You probably haven't changed anything.\nReload the table to be sure that the row exists and use a primary key for your table.\n(This error can be turned off in the preferences.)", @"message of panel when no rows have been affected after writing to the db")); @@ -1185,39 +1179,39 @@ isEditingNewRow = NO; [tableDocumentInstance showErrorInConsole:[NSString stringWithFormat:NSLocalizedString(@"/* WARNING %@ No rows have been affected */\n", @"warning shown in the console when no rows have been affected after writing to the db"), currentTime]]; return YES; + + // On success... } else if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { - //added new row with success isEditingRow = NO; + + // New row created successfully if ( isEditingNewRow ) { if ( [prefs boolForKey:@"reloadAfterAdding"] ) { [self reloadTableValues:self]; - // if ( sortField ) [tableContentView deselectAll:self]; } else { - //set insertId for fields with auto_increment - queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]]; - if ([queryResult numOfRows]) [queryResult dataSeek:0]; - for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { - rowObject = [queryResult fetchRowAsDictionary]; - if ( [[rowObject objectForKey:@"Extra"] isEqualToString:@"auto_increment"] ) { + + // Set the insertId for fields with auto_increment + for ( i = 0; i < [theColumns count] ; i++ ) { + if ([[theColumns objectAtIndex:i] objectForKey:@"autoincrement"]) { [[filteredResult objectAtIndex:rowIndex] setObject:[NSNumber numberWithLong:[mySQLConnection insertId]] - forKey:[rowObject objectForKey:@"Field"]]; + forKey:[columnNames objectAtIndex:i]]; } } [fullResult addObject:[filteredResult objectAtIndex:rowIndex]]; } isEditingNewRow = NO; + + // Existing row edited successfully } else { - //updated row with success if ( [prefs boolForKey:@"reloadAfterEditing"] ) { [self reloadTableValues:self]; - // if ( sortField ) [tableContentView deselectAll:self]; + + // TODO: this probably needs looking at... it's reloading it all itself? } else { - // query = [@"SELECT * FROM " stringByAppendingString:selectedTable]; query = [NSString stringWithFormat:@"SELECT %@ FROM `%@`", [self fieldListForQuery], selectedTable]; if ( sortField ) { - // query = [query stringByAppendingString:[NSString stringWithFormat:@" ORDER BY `%@`", sortField]]; query = [NSString stringWithFormat:@"%@ ORDER BY `%@`", query, sortField]; if ( isDesc ) query = [query stringByAppendingString:@" DESC"]; @@ -1231,51 +1225,43 @@ [limitRowsField intValue]-1, [prefs integerForKey:@"limitRowsValue"]]]; } queryResult = [mySQLConnection queryString:query]; - // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; [fullResult setArray:[self fetchResultAsArray:queryResult]]; } } return YES; + + // Report errors which have occurred } else { - //error in mysql-query NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), NSLocalizedString(@"Cancel", @"cancel button"), nil, tableWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), nil, @"addrow", [NSString stringWithFormat:NSLocalizedString(@"Couldn't write row.\nMySQL said: %@", @"message of panel when error while adding row to db"), [mySQLConnection getLastErrorMessage]]); return NO; } } -- (NSString *)argumentForRow:(int)row /* - returns the WHERE argument to identify a row - if row is -2, it uses the oldRow - if there is one, it uses the primary key, otherwise uses all fields as argument and sets LIMIT to 1 + * Returns the WHERE argument to identify a row. + * If "row" is -2, it uses the oldRow. + * Uses the primary key if available, otherwise uses all fields as argument and sets LIMIT to 1 */ +- (NSString *)argumentForRow:(int)row { CMMCPResult *theResult; NSDictionary *theRow; id tempValue; NSMutableString *value = [NSMutableString string]; NSMutableString *argument = [NSMutableString string]; + NSString *columnType; + NSArray *columnNames; int i,j; - NSEnumerator *enumerator; - id type; - BOOL blob = NO; - NSArray *numberFields = [NSArray arrayWithObjects:@"tiny", @"short", @"long", @"int24", @"longlong", @"decimal", @"float", @"double", nil]; if ( row == -1 ) return @""; - //get primary key if there is one - /* - theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM `%@`", selectedTable]]; - if ([theResult numOfRows]) [theResult dataSeek:0]; - for ( i = 0 ; i < [theResult numOfRows] ; i++ ) { - theRow = [theResult fetchRowAsDictionary]; - if ( [[theRow objectForKey:@"Key_name"] isEqualToString:@"PRIMARY"] ) { - [keys addObject:[theRow objectForKey:@"Column_name"]]; - } - } - */ + // Retrieve the field names for this table from the data cache. This is used when requesting all data as part + // of the fieldListForQuery method, and also to decide whether or not to preserve the current filter/sort settings. + columnNames = [tableDataInstance columnNames]; + + // Get the primary key if there is one if ( !keys ) { setLimit = NO; keys = [[NSMutableArray alloc] init]; @@ -1289,18 +1275,14 @@ } } + // If there is no primary key, all the fields are used in the argument. if ( ![keys count] ) { - //if there is no primary key, take all fields as argument - //here we have a problem when dontShowBlob == YES (we don't have the right values to use in the WHERE statement) - [keys setArray:fieldNames]; + [keys setArray:columnNames]; setLimit = YES; - enumerator = [fieldTypes objectEnumerator]; - while ( (type = [enumerator nextObject]) ) { - if ( [self isBlobOrText:type] ) { - blob = YES; - } - } - if ( [prefs boolForKey:@"dontShowBlob"] && blob ) { + + // When the option to not show blob or text options is set, we have a problem - we don't have + // the right values to use in the WHERE statement. Throw an error if this is the case. + if ( [prefs boolForKey:@"dontShowBlob"] && [self tableContainsBlobOrTextColumns] ) { NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, NSLocalizedString(@"You can't hide blob and text fields when working with tables without index.", @"message of panel when trying to edit tables without index and with hidden blob/text fields")); [keys removeAllObjects]; @@ -1308,26 +1290,34 @@ return @""; } } + + // Walk through the keys list constructing the argument list for ( i = 0 ; i < [keys count] ; i++ ) { if ( i ) [argument appendString:@" AND "]; + + // Use the selected row if appropriate if ( row >= 0 ) { - //use selected row tempValue = [[filteredResult objectAtIndex:row] objectForKey:[keys objectAtIndex:i]]; + + // Otherwise use the oldRow } else { - //use oldRow tempValue = [oldRow objectForKey:[keys objectAtIndex:i]]; } + if ( [tempValue isKindOfClass:[NSData class]] ) { - [value setString:[[NSString alloc] initWithData:tempValue encoding:[mySQLConnection encoding]]]; + NSString *tmpString = [[NSString alloc] initWithData:tempValue encoding:[mySQLConnection encoding]]; + [value setString:[NSString stringWithString:tmpString]]; + [tmpString release]; } else { [value setString:[tempValue description]]; } - + if ( [value isEqualToString:[prefs stringForKey:@"nullValue"]] ) { - [value setString:@"NULL"]; + [argument appendString:[NSString stringWithFormat:@"`%@` IS NULL", [keys objectAtIndex:i]]]; } else { - //escape special characters (in WHERE statement!) + + // Escape special characters (in WHERE statement!) for ( j = 0 ; j < [value length] ; j++ ) { if ( [value characterAtIndex:j] == '\\' ) { [value insertString:@"\\" atIndex:j]; @@ -1342,13 +1332,10 @@ j++; } } - // [value setString:[NSString stringWithFormat:@"\"%@\"", value]]; [value setString:[NSString stringWithFormat:@"'%@'", value]]; - } - if ( [value isEqualToString:@"NULL"] ) { - [argument appendString:[NSString stringWithFormat:@"`%@` IS NULL", [keys objectAtIndex:i]]]; - } else { - if ( [numberFields containsObject:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[keys objectAtIndex:i]]]] ) { + + columnType = [[tableDataInstance columnWithName:[keys objectAtIndex:i]] objectForKey:@"typegrouping"]; + if ( [columnType isEqualToString:@"integer"] || [columnType isEqualToString:@"float"] ) { [argument appendString:[NSString stringWithFormat:@"`%@` = %@", [keys objectAtIndex:i], value]]; } else { [argument appendString:[NSString stringWithFormat:@"`%@` LIKE %@", [keys objectAtIndex:i], value]]; @@ -1360,37 +1347,50 @@ return argument; } -- (BOOL)isBlobOrText:(NSString *)fieldType + /* - returns YES if fieldType is some kind of blob or text. afaik the type of this fields is always blob, but better we test it... - it would be nice to know if it is blob or text, but mysql doesn't want to tell it... + * Returns YES if the table contains any columns which are of any of the blob or text types, + * NO otherwise. */ +- (BOOL)tableContainsBlobOrTextColumns { - if ( [fieldType isEqualToString:@"tinyblob"] || [fieldType isEqualToString:@"blob"] || - [fieldType isEqualToString:@"mediumblob"] || [fieldType isEqualToString:@"longblob"] ) { - return YES; - } else { - return NO; + int i; + NSArray *tableColumns = [tableDataInstance tableColumns]; + NSString *columnTypeGrouping; + + for ( i = 0 ; i < [tableColumns count]; i++ ) { + columnTypeGrouping = [[tableColumns objectAtIndex:i] objectForKey:@"typegrouping"]; + if ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]) { + return YES; + } } + + return NO; } -- (NSString *)fieldListForQuery /* - returns * if dontShowBlob == NO - returns a comma-separated list of all fields which aren't of type blob or text if dontShowBlob == YES + * Returns a string controlling which fields to retrieve for a query. Returns * (all fields) if the preferences + * option dontShowBlob isn't set; otherwise, returns a comma-separated list of all non-blob/text fields. */ +- (NSString *)fieldListForQuery { int i; NSMutableArray *fields = [NSMutableArray array]; + NSArray *columns = [tableDataInstance columns]; + NSArray *columnNames = [tableDataInstance columnNames]; + NSString *columnTypeGrouping; if ( [prefs boolForKey:@"dontShowBlob"] ) { - for ( i = 0 ; i < [fieldTypes count] ; i++ ) { - if ( ![self isBlobOrText:[fieldTypes objectAtIndex:i]] ) { - [fields addObject:[fieldNames objectAtIndex:i]]; + for ( i = 0 ; i < [columnNames count] ; i++ ) { + columnTypeGrouping = [[columns objectAtIndex:i] objectForKey:@"typegrouping"]; + if (![columnTypeGrouping isEqualToString:@"textdata"] && ![columnTypeGrouping isEqualToString:@"blobdata"]) { + [fields addObject:[columnNames objectAtIndex:i]]; } } + + // Always select at least one field - the first if there are no non-blob fields. if ( [fields count] == 0 ) { - return [NSString stringWithFormat:@"`%@`", [fieldNames objectAtIndex:0]]; + return [NSString stringWithFormat:@"`%@`", [columnNames objectAtIndex:0]]; } else { return [NSString stringWithFormat:@"`%@`", [fields componentsJoinedByString:@"`,`"]]; } @@ -1543,25 +1543,22 @@ return [filteredResult count]; } -- (id)tableView:(NSTableView *)aTableView +- (id)tableView:(CMCopyTable *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { id theRow, theValue; - + theRow = [filteredResult objectAtIndex:rowIndex]; theValue = [theRow objectForKey:[aTableColumn identifier]]; - + + // Convert data objects to their string representation in the current encoding. if ( [theValue isKindOfClass:[NSData class]] ) { - theValue = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; - //show only first 50 characters to speed up interface (but return everything when this method is used to return the current result) - // if ( ([theValue length] > 100) && aTableView ) { + NSString *dataRepresentation = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; + theValue = [NSString stringWithString:dataRepresentation]; + [dataRepresentation release]; } -// if ( ([(NSString *)theValue length] > 100) && aTableView ) { -// theValue = [NSString stringWithFormat:@"%@(...)", [theValue substringToIndex:100]]; -// } - return theValue; } @@ -1716,7 +1713,7 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn NSMutableDictionary *tableColumnWidths; NSString *database = [NSString stringWithFormat:@"%@@%@", [tableDocumentInstance database], [tableDocumentInstance host]]; - NSString *table = (NSString *)[tablesListInstance table]; + NSString *table = [tablesListInstance tableName]; // get tableColumnWidths object if ( [prefs objectForKey:@"tableColumnWidths"] != nil ) { @@ -1741,35 +1738,26 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn [prefs setObject:tableColumnWidths forKey:@"tableColumnWidths"]; } -- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex /* - opens sheet if multipleLineEditingButton is clicked or field is a hidden blob or text field + * Confirm whether to allow editing of a row. Returns YES by default, unless the multipleLineEditingButton is in + * the ON state, or for blob or text fields - in those cases opens a sheet for editing instead and returns NO. */ +- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { + int code; + NSString *columnTypeGrouping, *query; NSEnumerator *enumerator; - id type; - BOOL blob = NO; NSDictionary *tempRow; NSMutableDictionary *modifiedRow = [NSMutableDictionary dictionary]; - id key; - int code; - NSString *query; + id key, theValue; CMMCPResult *tempResult; - id theValue; - BOOL columnIsBlob = NO; - // int i; - // NSArray *columns = [aTableView tableColumns]; + BOOL columnIsBlobOrText = NO; + // If not isEditingRow and the preference value for not showing blobs is set, check whether the row contains any blobs. if ( [prefs boolForKey:@"dontShowBlob"] && !isEditingRow ) { - //get all row values if dontShowBlob == YES and table contains blob or text field and isEditingRow = NO - enumerator = [fieldTypes objectEnumerator]; - while ( (type = [enumerator nextObject]) ) { - if ( [self isBlobOrText:type] ) { - blob = YES; - } - } - - if ( blob ) { + + // If the table does contain blob or text fields, load the values ready for editing. + if ( [self tableContainsBlobOrTextColumns] ) { query = [NSString stringWithFormat:@"SELECT * FROM `%@` WHERE %@", selectedTable, [self argumentForRow:[tableContentView selectedRow]]]; tempResult = [mySQLConnection queryString:query]; @@ -1787,28 +1775,19 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn [modifiedRow setObject:[tempRow objectForKey:key] forKey:key]; } } - [filteredResult replaceObjectAtIndex:rowIndex - withObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]]; + [filteredResult replaceObjectAtIndex:rowIndex withObject:[NSMutableDictionary dictionaryWithDictionary:modifiedRow]]; [tableContentView reloadData]; } } - /* - // find the column we're trying to edit - for ( i = 0; i < [columns count]; i++ ) { - if ( [columns objectAtIndex:i] == aTableColumn ) { - // this flag will let us determine if we should "force" multi-line edit. - columnIsBlob = [self isBlobOrText:[fieldTypes objectAtIndex:i]]; - break; - } - } - */ - //is the column a blob field -> if YES force sheet editing - if ( [self isBlobOrText:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[aTableColumn identifier]]]] ) { - columnIsBlob = YES; + // If the selected column is a blob/text type, force sheet editing. + columnTypeGrouping = [[tableDataInstance columnWithName:[aTableColumn identifier]] objectForKey:@"typegrouping"]; + if ([columnTypeGrouping isEqualToString:@"textdata"] || [columnTypeGrouping isEqualToString:@"blobdata"]) { + columnIsBlobOrText = YES; } - - if ( [multipleLineEditingButton state] == NSOnState || columnIsBlob ) { + + // Open the sheet if the multipleLineEditingButton is enabled or the column was a blob or a text. + if ( [multipleLineEditingButton state] == NSOnState || columnIsBlobOrText ) { theValue = [[filteredResult objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]; NSImage *image = nil; editData = [theValue retain]; @@ -1816,40 +1795,31 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn if ( [theValue isKindOfClass:[NSData class]] ) { image = [[NSImage alloc] initWithData:theValue]; [hexTextView setString:[self dataToHex:theValue]]; - /* - // update displayed font to monospace - NSFont *font = [NSFont fontWithName:@"Courier" size:12.0f]; - NSRange hexRange = { 0, [[hexTextView string] length] - 1 }; - [hexTextView setFont:font range:hexRange]; - */ theValue = [[NSString alloc] initWithData:theValue encoding:[mySQLConnection encoding]]; } else { [hexTextView setString:@""]; theValue = [theValue description]; } - + [editImage setImage:image]; [editTextView setString:theValue]; [editTextView setSelectedRange:NSMakeRange(0,[[editTextView string] length])]; - //different sheets for date (with up/down arrows), number and text - [NSApp beginSheet:editSheet - modalForWindow:tableWindow modalDelegate:self - didEndSelector:nil contextInfo:nil]; + + [NSApp beginSheet:editSheet modalForWindow:tableWindow modalDelegate:self didEndSelector:nil contextInfo:nil]; code = [NSApp runModalForWindow:editSheet]; - + [NSApp endSheet:editSheet]; [editSheet orderOut:nil]; - + if ( code ) { if ( !isEditingRow ) { [oldRow setDictionary:[filteredResult objectAtIndex:rowIndex]]; isEditingRow = YES; } - [[filteredResult objectAtIndex:rowIndex] setObject:[editData copy] - forKey:[aTableColumn identifier]]; + [[filteredResult objectAtIndex:rowIndex] setObject:[editData copy] forKey:[aTableColumn identifier]]; - // clean up + // Clean up [editImage setImage:nil]; [editTextView setString:@""]; [hexTextView setString:@""]; @@ -1889,57 +1859,56 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn #pragma mark - -- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command /* - traps enter and esc an make/cancel editing without entering next row + * Trap the enter and escape keys, overriding default behaviour and continuing/ending editing, + * only within the current row. */ +- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command { + NSString *fieldType; int row, column, i; row = [tableContentView editedRow]; column = [tableContentView editedColumn]; - + + // Trap enter and tab keys if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertNewline:)] || - [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) //trap enter and tab + [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) { - //save current line [[control window] makeFirstResponder:control]; + + // Save the current line if it's the last field in the table if ( column == ( [tableContentView numberOfColumns] - 1 ) ) { [self addRowToDB]; - /* - if ( [self addRowToDB] && - ( [textView methodForSelector:command] == [textView methodForSelector:@selector(insertTab:)] ) && - !(sortField && ([prefs boolForKey:@"reloadAfterAdding"] || [prefs boolForKey:@"reloadAfterEditing"])) ) { - //get in edit-mode of next row if user hit tab (and result isn't sorted and reloaded) - if ( row < ([tableContentView numberOfRows] - 1) ) { - [tableContentView selectRow:row+1 byExtendingSelection:NO]; - [tableContentView editColumn:0 row:row+1 withEvent:nil select:YES]; - } else { - [tableContentView selectRow:0 byExtendingSelection:NO]; - [tableContentView editColumn:0 row:0 withEvent:nil select:YES]; - } - } - */ } else { - //check if next column is a blob column + + // Check if next column is a blob column, and skip to the next non-blob column i = 1; - while ( [self isBlobOrText:[fieldTypes objectAtIndex:[fieldNames indexOfObject:[[[tableContentView tableColumns] objectAtIndex:column+i] identifier]]]] ) { + while ( + (fieldType = [[tableDataInstance columnWithName:[[[tableContentView tableColumns] objectAtIndex:column+i] identifier]] objectForKey:@"typegrouping"]) + && ([fieldType isEqualToString:@"textdata"] || [fieldType isEqualToString:@"blobdata"]) + ) { i++; + + // If there are no columns after the latest blob or text column, save the current line. if ( (column+i) >= [tableContentView numberOfColumns] ) { - //there is no other column after the blob column [self addRowToDB]; return TRUE; } } - //edit the column after the blob column + + // Edit the column after the blob column [tableContentView editColumn:column+i row:row withEvent:nil select:YES]; } return TRUE; } + + // Trap the escape key else if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(_cancelKey:)] || - [textView methodForSelector:command] == [textView methodForSelector:@selector(complete:)] ) //trap esc + [textView methodForSelector:command] == [textView methodForSelector:@selector(complete:)] ) { - //abort editing + + // Abort editing [control abortEditing]; if ( isEditingRow && !isEditingNewRow ) { isEditingRow = NO; @@ -1990,8 +1959,6 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn [filteredResult release]; [keys release]; [oldRow release]; - [fieldNames release]; - [fieldTypes release]; [compareType release]; [sortField release]; [prefs release]; diff --git a/Source/TableDocument.h b/Source/TableDocument.h index 62798b2d..52429c7b 100644 --- a/Source/TableDocument.h +++ b/Source/TableDocument.h @@ -41,6 +41,7 @@ IBOutlet id tableContentInstance; IBOutlet id customQueryInstance; IBOutlet id tableDumpInstance; + IBOutlet id tableDataInstance; IBOutlet id tableStatusInstance; IBOutlet id tableWindow; @@ -133,9 +134,9 @@ - (void)showErrorInConsole:(NSString *)error; //encoding methods -- (void)setEncoding:(NSString *)encoding; -- (void)detectDatabaseEncoding; -- (void)detectTableEncodingForTable:(NSString *)table; +- (void)setConnectionEncoding:(NSString *)mysqlEncoding reloadingViews:(BOOL)reloadViews; +- (NSString *)databaseEncoding; +- (NSString *)connectionEncoding; - (IBAction)chooseEncoding:(id)sender; - (BOOL)supportsEncoding; - (void)updateEncodingMenuWithSelectedEncoding:(NSString *)encoding; diff --git a/Source/TableDocument.m b/Source/TableDocument.m index 6969e561..b9cb66a6 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -33,6 +33,8 @@ #import "TableStatus.h" #import "ImageAndTextCell.h" #import "SPGrowlController.h" +#import "SPSQLParser.h" +#import "SPTableData.h" NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange"; NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFavoritesControllerFavoritesDidChange"; @@ -190,9 +192,9 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa // set encoding NSString *encodingName = [prefs objectForKey:@"encoding"]; if ( [encodingName isEqualToString:@"Autodetect"] ) { - [self detectDatabaseEncoding]; + [self setConnectionEncoding:[self databaseEncoding] reloadingViews:NO]; } else { - [self setEncoding:[self mysqlEncodingFromDisplayEncoding:encodingName]]; + [self setConnectionEncoding:[self mysqlEncodingFromDisplayEncoding:encodingName] reloadingViews:NO]; } //get mysql version theResult = [mySQLConnection queryString:@"SHOW VARIABLES LIKE 'version'"]; @@ -210,6 +212,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa [customQueryInstance setConnection:mySQLConnection]; [tableDumpInstance setConnection:mySQLConnection]; [tableStatusInstance setConnection:mySQLConnection]; + [tableDataInstance setConnection:mySQLConnection]; [self setFileName:[NSString stringWithFormat:@"(MySQL %@) %@@%@ %@", mySQLVersion, [userField stringValue], [hostField stringValue], [databaseField stringValue]]]; [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@", mySQLVersion, [userField stringValue], @@ -642,7 +645,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa /** * Set the encoding for the database connection */ -- (void)setEncoding:(NSString *)mysqlEncoding +- (void)setConnectionEncoding:(NSString *)mysqlEncoding reloadingViews:(BOOL)reloadViews { // set encoding of connection and client [mySQLConnection queryString:[NSString stringWithFormat:@"SET NAMES '%@'", mysqlEncoding]]; @@ -652,22 +655,29 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa [_encoding autorelease]; _encoding = [mysqlEncoding retain]; } else { - [self detectDatabaseEncoding]; + [mySQLConnection queryString:[NSString stringWithFormat:@"SET NAMES '%@'", [self databaseEncoding]]]; + if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { + NSLog(@"Error: could not set encoding to %@ nor fall back to database encoding on MySQL %@", mysqlEncoding, [self mySQLVersion]); + return; + } } // update the selected menu item [self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:mysqlEncoding]]; - // reload stuff - [tableSourceInstance reloadTable:self]; - [tableContentInstance reloadTable:self]; - [tableStatusInstance reloadTable:self]; + // Reload stuff as appropriate + [tableDataInstance resetAllData]; + if (reloadViews) { + if ([tablesListInstance structureLoaded]) [tableSourceInstance reloadTable:self]; + if ([tablesListInstance contentLoaded]) [tableContentInstance reloadTable:self]; + if ([tablesListInstance statusLoaded]) [tableStatusInstance reloadTable:self]; + } } /** * returns the current mysql encoding for this object */ -- (NSString *)encoding +- (NSString *)connectionEncoding { return _encoding; } @@ -755,9 +765,10 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa } /** - * Autodetect the connection encoding and select the relevant encoding menu item in Database -> Database Encoding + * Detect and return the database connection encoding. + * TODO: See http://code.google.com/p/sequel-pro/issues/detail?id=134 - some question over why this [historically] uses _connection not _database... */ -- (void)detectDatabaseEncoding +- (NSString *)databaseEncoding { // MySQL > 4.0 id mysqlEncoding = [[[mySQLConnection queryString:@"SHOW VARIABLES LIKE 'character_set_connection'"] fetchRowAsDictionary] objectForKey:@"Value"]; @@ -774,36 +785,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa mysqlEncoding = @"latin1"; } - [mySQLConnection setEncoding:[CMMCPConnection encodingForMySQLEncoding:[mysqlEncoding cString]]]; - - // save the encoding - [_encoding autorelease]; - _encoding = [mysqlEncoding retain]; - - // update the selected menu item - [self updateEncodingMenuWithSelectedEncoding:[self encodingNameFromMySQLEncoding:mysqlEncoding]]; -} - -/** - * Detects and sets the character set encoding of the supplied table name. - */ -- (void)detectTableEncodingForTable:(NSString *)table; -{ - NSString *tableCollation = [[[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", table]] fetchRowAsDictionary] objectForKey:@"Collation"]; - - if (tableCollation != nil) { - // Split up the collation string so we can get the encoding - NSArray *encodingComponents = [tableCollation componentsSeparatedByString:@"_"]; - - if ([encodingComponents count] > 0) { - NSString *tableEncoding = [encodingComponents objectAtIndex:0]; - - [self setEncoding:tableEncoding]; - } - } - else { - NSLog(@"Error: unable to determine table collation and thus character encoding for table '%@'", table); - } + return mysqlEncoding; } /** @@ -811,7 +793,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa */ - (IBAction)chooseEncoding:(id)sender { - [self setEncoding:[self mysqlEncodingFromDisplayEncoding:[(NSMenuItem *)sender title]]]; + [self setConnectionEncoding:[self mysqlEncodingFromDisplayEncoding:[(NSMenuItem *)sender title]] reloadingViews:YES]; } /** @@ -995,6 +977,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa NSRunInformationalAlertPanel([NSString stringWithFormat:@"Checksum '%@' table", [self table]], [NSString stringWithFormat:@"Checksum: %@", [theRow objectForKey:@"Checksum"]], @"OK", nil, nil); } + #pragma mark Other Methods /** * returns the host @@ -1086,7 +1069,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFa returns the currently selected table (passing the request to TablesList) */ { - return (NSString *)[tablesListInstance table]; + return [tablesListInstance tableName]; } - (NSString *)mySQLVersion diff --git a/Source/TableDump.h b/Source/TableDump.h index b759d8fa..c5f83cd0 100644 --- a/Source/TableDump.h +++ b/Source/TableDump.h @@ -34,6 +34,7 @@ IBOutlet id tablesListInstance; IBOutlet id tableSourceInstance; IBOutlet id tableContentInstance; + IBOutlet id tableDataInstance; IBOutlet id customQueryInstance; IBOutlet id tableWindow; @@ -119,7 +120,6 @@ enclosedBy:(NSString *)enclosed escapedBy:(NSString *)escaped lineEnds:(NSString *)lineEnds; - (NSArray *)arrayForString:(NSString *)string enclosed:(NSString *)enclosed escaped:(NSString *)escaped terminated:(NSString *)terminated; -- (NSArray *)splitQueries:(NSString *)query; // Export methods - (BOOL)dumpSelectedTablesAsSqlToFileHandle:(NSFileHandle *)fileHandle; @@ -127,7 +127,7 @@ toFileHandle:(NSFileHandle *)fileHandle outputFieldNames:(BOOL)firstLine terminatedBy:(NSString *)terminated enclosedBy:(NSString *)enclosed escapedBy:(NSString *)escaped - lineEnds:(NSString *)lineEnds silently:(BOOL)silently; + lineEnds:(NSString *)lineEnds withNumericColumns:(NSArray *)tableColumnNumericStatus silently:(BOOL)silently; - (BOOL)writeXmlForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle tableName:(NSString *)table withHeader:(BOOL)header silently:(BOOL)silently; diff --git a/Source/TableDump.m b/Source/TableDump.m index 3c34634b..9c27e325 100644 --- a/Source/TableDump.m +++ b/Source/TableDump.m @@ -29,6 +29,8 @@ #import "TableContent.h" #import "CustomQuery.h" #import "SPGrowlController.h" +#import "SPSQLParser.h" +#import "SPTableData.h" @implementation TableDump @@ -244,13 +246,14 @@ enclosedBy:[exportFieldsEnclosedField stringValue] escapedBy:[exportFieldsEscapedField stringValue] lineEnds:[exportLinesTerminatedField stringValue] + withNumericColumns:nil silently:NO]; // Export the current "browse" view to a file in XML format } else if ( [contextInfo isEqualToString:@"exportBrowseViewAsXML"] ) { success = [self writeXmlForArray:[tableContentInstance currentResult] orQueryResult:nil toFileHandle:fileHandle - tableName:[tableDocumentInstance table] + tableName:(NSString *)[tableDocumentInstance table] withHeader:YES silently:NO]; @@ -263,6 +266,7 @@ enclosedBy:[exportFieldsEnclosedField stringValue] escapedBy:[exportFieldsEscapedField stringValue] lineEnds:[exportLinesTerminatedField stringValue] + withNumericColumns:nil silently:NO]; // Export the current custom query result set to a file in XML format @@ -353,13 +357,13 @@ - (void)importBackgroundProcess:(NSString*)filename { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *dumpFile; + SPSQLParser *dumpFile; NSError **errorStr; NSMutableString *errors = [NSMutableString string]; NSString *fileType = [[importFormatPopup selectedItem] title]; //load file into string - dumpFile = [NSString stringWithContentsOfFile:filename - encoding:[CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]] + dumpFile = [SPSQLParser stringWithContentsOfFile:filename + encoding:[CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] cString]] error:errorStr]; if ( !dumpFile ) { @@ -398,7 +402,7 @@ [singleProgressBar startAnimation:self]; //get array with an object for each mysql-query - queries = [self splitQueries:dumpFile]; + queries = [dumpFile splitStringByCharacter:';']; [singleProgressBar stopAnimation:self]; [singleProgressBar setUsesThreadedAnimation:NO]; @@ -717,9 +721,9 @@ */ - (BOOL)dumpSelectedTablesAsSqlToFileHandle:(NSFileHandle *)fileHandle { - int i,j,t,rowCount, progressBarWidth, lastProgressValue, queryLength; + int i,j,t,rowCount, colCount, progressBarWidth, lastProgressValue, queryLength; CMMCPResult *queryResult; - NSString *tableName; + NSString *tableName, *tableColumnTypeGrouping; NSArray *fieldNames; NSArray *theRow; NSMutableArray *selectedTables = [NSMutableArray array]; @@ -727,8 +731,9 @@ NSMutableString *cellValue = [NSMutableString string]; NSMutableString *sqlString = [NSMutableString string]; NSMutableString *errors = [NSMutableString string]; + NSDictionary *tableDetails; + NSMutableArray *tableColumnNumericStatus; NSStringEncoding connectionEncoding = [mySQLConnection encoding]; - NSScanner *sqlNumericTester; id createTableSyntax; // Reset the interface @@ -811,6 +816,20 @@ fieldNames = [queryResult fetchFieldNames]; rowCount = [queryResult numOfRows]; + // Retrieve the table details via the data class, and use it to build an array containing column numeric status + tableDetails = [NSDictionary dictionaryWithDictionary:[tableDataInstance informationFromCreateTableSyntaxForTable:tableName]]; + colCount = [[tableDetails objectForKey:@"columns"] count]; + tableColumnNumericStatus = [NSMutableArray arrayWithCapacity:colCount]; + for ( j = 0; j < colCount ; j++ ) { + tableColumnTypeGrouping = [[[tableDetails objectForKey:@"columns"] objectAtIndex:j] objectForKey:@"typegrouping"]; + if ([tableColumnTypeGrouping isEqualToString:@"bit"] || [tableColumnTypeGrouping isEqualToString:@"integer"] + || [tableColumnTypeGrouping isEqualToString:@"float"]) { + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:YES]]; + } else { + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:NO]]; + } + } + // Update the progress text and set the progress bar back to determinate [singleProgressText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Table %i of %i (%@): Dumping...", @"text showing that app is writing data for table dump"), (i+1), [selectedTables count], tableName]]; [singleProgressText displayIfNeeded]; @@ -840,7 +859,7 @@ [singleProgressBar displayIfNeeded]; } - for ( t = 0 ; t < [theRow count] ; t++ ) { + for ( t = 0 ; t < colCount ; t++ ) { // Add NULL values directly to the output row if ( [[theRow objectAtIndex:t] isMemberOfClass:[NSNull class]] ) { @@ -861,14 +880,8 @@ } else { - // Until we have access to field types, test whether this cell contains a - // number via use of an NSScanner and a check of the first couple of - // characters (0[^.] is not a number). If it is a number, add the number directly. - sqlNumericTester = [NSScanner scannerWithString:cellValue]; - if ([sqlNumericTester scanFloat:nil] && [sqlNumericTester isAtEnd] && - ([cellValue characterAtIndex:0] != '0' - || [cellValue length] == 1 - || ([cellValue length] > 1 && [cellValue characterAtIndex:1] == '.'))) { + // If this is a numeric column type, add the number directly. + if ( [[tableColumnNumericStatus objectAtIndex:t] boolValue] ) { [sqlString appendString:cellValue]; // Otherwise add a quoted string with special characters escaped @@ -944,10 +957,16 @@ /* Takes an array and writes it in CSV format to the supplied NSFileHandle */ -- (BOOL)writeCsvForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle outputFieldNames:(BOOL)outputFieldNames terminatedBy:(NSString *)fieldSeparatorString - enclosedBy:(NSString *)enclosingString escapedBy:(NSString *)escapeString lineEnds:(NSString *)lineEndString silently:(BOOL)silently; +- (BOOL)writeCsvForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle + outputFieldNames:(BOOL)outputFieldNames + terminatedBy:(NSString *)fieldSeparatorString + enclosedBy:(NSString *)enclosingString + escapedBy:(NSString *)escapeString + lineEnds:(NSString *)lineEndString + withNumericColumns:(NSArray *)tableColumnNumericStatus + silently:(BOOL)silently; { - NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]]; + NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] cString]]; NSMutableString *csvCell = [NSMutableString string]; NSMutableArray *csvRow = [NSMutableArray array]; NSMutableString *csvString = [NSMutableString string]; @@ -1072,14 +1091,21 @@ } else { - // Until we have access to field types, test whether this cell contains a number via use of an - // NSScanner and a check of the first couple of characters. + // Test whether this cell contains a number if ([[csvRow objectAtIndex:j] isKindOfClass:[NSData class]]) { csvCellIsNumeric = FALSE; + + // If an array of bools supplying information as to whether the column is numeric has been supplied, use it. + } else if (tableColumnNumericStatus != nil) { + csvCellIsNumeric = [[tableColumnNumericStatus objectAtIndex:j] boolValue]; + + // Or fall back to testing numeric content via an NSScanner. } else { csvNumericTester = [NSScanner scannerWithString:csvCell]; - csvCellIsNumeric = [csvNumericTester scanFloat:nil] && [csvNumericTester isAtEnd] && - ([csvCell characterAtIndex:0] != '0' || [csvCell characterAtIndex:1] == '.'); + csvCellIsNumeric = [csvNumericTester scanFloat:nil] && [csvNumericTester isAtEnd] + && ([csvCell characterAtIndex:0] != '0' + || [csvCell length] == 1 + || ([csvCell length] > 1 && [csvCell characterAtIndex:1] == '.')); } // Escape any occurrences of the escaping character @@ -1292,7 +1318,7 @@ */ - (BOOL)writeXmlForArray:(NSArray *)array orQueryResult:(CMMCPResult *)queryResult toFileHandle:(NSFileHandle *)fileHandle tableName:(NSString *)table withHeader:(BOOL)header silently:(BOOL)silently { - NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance encoding] cString]]; + NSStringEncoding tableEncoding = [CMMCPConnection encodingForMySQLEncoding:[[tableDocumentInstance connectionEncoding] cString]]; NSMutableArray *xmlTags = [NSMutableArray array]; NSMutableArray *xmlRow = [NSMutableArray array]; NSMutableString *xmlString = [NSMutableString string]; @@ -1444,13 +1470,15 @@ */ - (BOOL)exportTables:(NSArray *)selectedTables toFileHandle:(NSFileHandle *)fileHandle usingFormat:(NSString *)type { - int i; + int i, j; CMMCPResult *queryResult; - NSString *tableName; + NSString *tableName, *tableColumnTypeGrouping; NSMutableString *infoString = [NSMutableString string]; NSMutableString *errors = [NSMutableString string]; NSStringEncoding connectionEncoding = [mySQLConnection encoding]; NSMutableString *csvLineEnd; + NSDictionary *tableDetails; + NSMutableArray *tableColumnNumericStatus; // Reset the interface [errorsView setString:@""]; @@ -1514,7 +1542,20 @@ if ( [type isEqualToString:@"csv"] && [selectedTables count] > 1) { [fileHandle writeData:[[NSString stringWithFormat:@"Table %@%@%@", tableName, csvLineEnd, csvLineEnd] dataUsingEncoding:connectionEncoding]]; } - +NSDate *startDate = [NSDate date]; + // Retrieve the table details via the data class, and use it to build an array containing column numeric status + tableDetails = [NSDictionary dictionaryWithDictionary:[tableDataInstance informationFromCreateTableSyntaxForTable:tableName]]; + tableColumnNumericStatus = [NSMutableArray array]; + for ( j = 0; j < [[tableDetails objectForKey:@"columns"] count] ; j++ ) { + tableColumnTypeGrouping = [[[tableDetails objectForKey:@"columns"] objectAtIndex:j] objectForKey:@"typegrouping"]; + if ([tableColumnTypeGrouping isEqualToString:@"bit"] || [tableColumnTypeGrouping isEqualToString:@"integer"] + || [tableColumnTypeGrouping isEqualToString:@"float"]) { + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:YES]]; + } else { + [tableColumnNumericStatus addObject:[NSNumber numberWithBool:NO]]; + } + } + // Retrieve all the content within this table queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SELECT * FROM `%@`", tableName]]; @@ -1541,8 +1582,9 @@ enclosedBy:[exportMultipleFieldsEnclosedField stringValue] escapedBy:[exportMultipleFieldsEscapedField stringValue] lineEnds:[exportMultipleLinesTerminatedField stringValue] + withNumericColumns:tableColumnNumericStatus silently:YES]; - + NSLog(@"CSV export took %f s", [[NSDate date] timeIntervalSinceDate:startDate]); // Add a spacer to the file [fileHandle writeData:[[NSString stringWithFormat:@"%@%@%@", csvLineEnd, csvLineEnd, csvLineEnd] dataUsingEncoding:connectionEncoding]]; } else if ( [type isEqualToString:@"xml"] ) { @@ -1693,87 +1735,6 @@ return [NSArray arrayWithArray:tempArray]; } -- (NSArray *)splitQueries:(NSString *)query -/* - splits the queries by ;'s which aren't inside any ", ' or ` characters - */ -{ - NSMutableString *queries = [NSMutableString stringWithString:query]; - NSMutableArray *queryArray = [NSMutableArray array]; - char stringType = nil; - BOOL inString = NO; - BOOL escaped; - unsigned lineStart = 0; - unsigned i, j, x, currentLineLength; - - //parse string - for ( i = 0 ; i < [queries length] ; i++ ) { - if ( inString ) { - //we are in a string - //look for end of string - for ( ; i < [queries length] ; i++ ) { - - // For the backtick character treat the string as ended - if ( ([queries characterAtIndex:i] == '`') && (stringType == '`') ) { - - inString = NO; - break; - - // Otherwise, prepare to treat the string as ended after a stringType.... - } else if ( [queries characterAtIndex:i] == stringType ) { - - // ...but only if the stringType isn't escaped with an *odd* number of escaping characters. - escaped = NO; - j = 1; - currentLineLength = i - lineStart; - while ( ((currentLineLength-j) > 0) && ([queries characterAtIndex:i-j] == '\\') ) { - escaped = !escaped; - j++; - } - - // If an odd number have been found, it really is the end of the string. - if ( !escaped ) { - inString = NO; - break; - } - } - } - } else if ( ([queries characterAtIndex:i] == '#') || - ((i+2<[queries length]) && - ([queries characterAtIndex:i] == '-') && - ([queries characterAtIndex:i+1] == '-') && - ([queries characterAtIndex:i+2] == ' ')) ) { - //it's a comment -> delete it - x = i; - while ( (x<[queries length]) && ([queries characterAtIndex:x] != '\r') && ([queries characterAtIndex:x] != '\n') ) { - x++; - } - [queries deleteCharactersInRange:NSMakeRange(i,x-i)]; - } else if ( [queries characterAtIndex:i] == ';' ) { - //we are at the end of a query - [queryArray addObject:[queries substringWithRange:NSMakeRange(lineStart, (i-lineStart))]]; - while ( ((i+1)<[queries length]) && (([queries characterAtIndex:i+1]=='\n') || ([queries characterAtIndex:i+1]=='\r') || ([queries characterAtIndex:i+1]==' ')) ) { - i++; - } - lineStart = i + 1; - } else if ( ([queries characterAtIndex:i] == '\'') || - ([queries characterAtIndex:i] == '"') || - ([queries characterAtIndex:i] == '`') ) { - //we are entering a string - inString = YES; - stringType = [queries characterAtIndex:i]; - } - } - - //add rest of string to array (if last line has not ended with a ";") - if ( lineStart < [queries length] ) { - [queryArray addObject:[queries substringWithRange:NSMakeRange(lineStart, ([queries length]-lineStart))]]; - } - - //return array - return [NSArray arrayWithArray:queryArray]; -} - //additional methods - (void)setConnection:(CMMCPConnection *)theConnection diff --git a/Source/TableSource.h b/Source/TableSource.h index 2a62232c..29cbfbaa 100644 --- a/Source/TableSource.h +++ b/Source/TableSource.h @@ -31,6 +31,7 @@ @interface TableSource : NSObject { IBOutlet id tablesListInstance; + IBOutlet id tableDataInstance; IBOutlet id tableWindow; IBOutlet id indexSheet; diff --git a/Source/TableSource.m b/Source/TableSource.m index b9b1a3c1..d9c7a595 100644 --- a/Source/TableSource.m +++ b/Source/TableSource.m @@ -24,6 +24,7 @@ #import "TableSource.h" #import "TablesList.h" +#import "SPTableData.h" @implementation TableSource @@ -41,19 +42,16 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab NSEnumerator *extrasEnumerator; id extra; int i; - + selectedTable = aTable; [tableSourceView deselectAll:self]; - + if ( isEditingRow ) return; - + // empty variables [enumFields removeAllObjects]; - - //query started - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; - + if ( [aTable isEqualToString:@""] || !aTable ) { [tableFields removeAllObjects]; [indexes removeAllObjects]; @@ -70,14 +68,14 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab [tableTypeButton setEnabled:NO]; tableType = nil; - //query finished - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; - [scanner release]; return; } + //query started + [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; + //perform queries and load results in array (each row as a dictionary) tableSourceResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM `%@`", selectedTable]] retain]; @@ -91,17 +89,10 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab // [indexes setArray:[[self fetchResultAsArray:indexResult] retain]]; [indexes setArray:[self fetchResultAsArray:indexResult]]; [indexResult release]; - - CMMCPResult *tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", selectedTable]]; + + // Retrieve the table type via the table data's status cache (which automatically maps Type to Engine) [tableType release]; - - NSDictionary *tempRow = [tableStatusResult fetchRowAsDictionary]; - if ( [tempRow objectForKey:@"Type"]) { - tableType = [tempRow objectForKey:@"Type"]; - } else { - tableType = [tempRow objectForKey:@"Engine"]; - } - [tableType retain]; + tableType = [[NSString stringWithString:[tableDataInstance statusValueForKey:@"Engine"]] retain]; //get table default values if ( defaultValues ) { @@ -384,8 +375,9 @@ opens alertsheet and asks for confirmation // [[NSNotificationCenter defaultCenter] postNotificationName:@"SelectedTableStatusHasChanged" object:self]; - // Mark the content table for refresh + // Mark the content table for refresh and update column caches [tablesListInstance setContentRequiresReload:YES]; + [tableDataInstance resetColumnData]; } else { [sender selectItemWithTitle:tableType]; NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, @@ -655,8 +647,9 @@ returns YES if no row is beeing edited and nothing has to be written to db isEditingNewRow = NO; [self loadTable:selectedTable]; - // Mark the content table for refresh + // Mark the content table and column caches for refresh [tablesListInstance setContentRequiresReload:YES]; + [tableDataInstance resetColumnData]; return YES; } else { @@ -711,8 +704,9 @@ returns YES if no row is beeing edited and nothing has to be written to db if ( [[mySQLConnection getLastErrorMessage] isEqualToString:@""] ) { [self loadTable:selectedTable]; - // Mark the content table for refresh + // Mark the content table and column cache for refresh [tablesListInstance setContentRequiresReload:YES]; + [tableDataInstance resetColumnData]; } else { NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, tableWindow, self, nil, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove field %@.\nMySQL said: %@", @"message of panel when field cannot be removed"), @@ -768,7 +762,7 @@ returns an array containing the field names of the selected table //load table if not already done if ( ![tablesListInstance structureLoaded] ) { - [self loadTable:(NSString *)[tablesListInstance table]]; + [self loadTable:[tablesListInstance tableName]]; } //get field names @@ -883,37 +877,61 @@ would result in a position change. } /* -Having validated a drop, perform the field/column reordering to match. -*/ -- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id <NSDraggingInfo>)info row:(int)destinationRow dropOperation:(NSTableViewDropOperation)operation + * Having validated a drop, perform the field/column reordering to match. + */ +- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id <NSDraggingInfo>)info row:(int)destinationRowIndex dropOperation:(NSTableViewDropOperation)operation { - int originalRow; + int originalRowIndex; NSMutableString *queryString; + NSDictionary *originalRow; - // Extract the original row position from the pasteboard. - originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + // Extract the original row position from the pasteboard and retrieve the details + originalRowIndex = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue]; + originalRow = [[NSDictionary alloc] initWithDictionary:[tableFields objectAtIndex:originalRowIndex]]; [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; // Begin construction of the reordering query queryString = [NSMutableString stringWithFormat:@"ALTER TABLE `%@` MODIFY COLUMN `%@` %@", selectedTable, - [[tableFields objectAtIndex:originalRow] objectForKey:@"Field"], - [[tableFields objectAtIndex:originalRow] objectForKey:@"Type"]]; + [originalRow objectForKey:@"Field"], + [originalRow objectForKey:@"Type"]]; // Add the length parameter if necessary - if ( [[tableFields objectAtIndex:originalRow] objectForKey:@"Length"] && - ![[[tableFields objectAtIndex:originalRow] objectForKey:@"Length"] isEqualToString:@""]) - { - [queryString appendString:[NSString stringWithFormat:@"(%@)", - [[tableFields objectAtIndex:originalRow] objectForKey:@"Length"]]]; + if ( [originalRow objectForKey:@"Length"] && ![[originalRow objectForKey:@"Length"] isEqualToString:@""]) { + [queryString appendString:[NSString stringWithFormat:@"(%@)", [originalRow objectForKey:@"Length"]]]; + } + + // Add unsigned, zerofill, binary, not null if necessary + if ([[originalRow objectForKey:@"unsigned"] isEqualToString:@"1"]) { + [queryString appendString:@" UNSIGNED"]; + } + if ([[originalRow objectForKey:@"zerofill"] isEqualToString:@"1"]) { + [queryString appendString:@" ZEROFILL"]; + } + if ([[originalRow objectForKey:@"binary"] isEqualToString:@"1"]) { + [queryString appendString:@" BINARY"]; + } + if ([[originalRow objectForKey:@"Null"] isEqualToString:@"NO"] ) { + [queryString appendString:@" NOT NULL"]; + } + + // Add the default value + if ([[originalRow objectForKey:@"Default"] isEqualToString:[prefs objectForKey:@"nullValue"]]) { + if ([[originalRow objectForKey:@"Null"] isEqualToString:@"YES"]) { + [queryString appendString:@" DEFAULT NULL"]; + } + } else if ( [[originalRow objectForKey:@"Type"] isEqualToString:@"timestamp"] && ([[[originalRow objectForKey:@"Default"] uppercaseString] isEqualToString:@"CURRENT_TIMESTAMP"]) ) { + [queryString appendString:@" DEFAULT CURRENT_TIMESTAMP"]; + } else { + [queryString appendString:[NSString stringWithFormat:@" DEFAULT '%@'", [mySQLConnection prepareString:[originalRow objectForKey:@"Default"]]]]; } // Add the new location - if ( destinationRow == 0 ){ + if ( destinationRowIndex == 0 ){ [queryString appendString:@" FIRST"]; } else { [queryString appendString:[NSString stringWithFormat:@" AFTER `%@`", - [[tableFields objectAtIndex:destinationRow-1] objectForKey:@"Field"]]]; + [[tableFields objectAtIndex:destinationRowIndex-1] objectForKey:@"Field"]]]; } // Run the query; report any errors, or reload the table on success @@ -923,18 +941,20 @@ Having validated a drop, perform the field/column reordering to match. [NSString stringWithFormat:NSLocalizedString(@"Couldn't move field. MySQL said: %@", @"message of panel when field cannot be added in drag&drop operation"), [mySQLConnection getLastErrorMessage]]); } else { [self loadTable:selectedTable]; - if ( originalRow < destinationRow ) { - [tableSourceView selectRow:destinationRow-1 byExtendingSelection:NO]; + if ( originalRowIndex < destinationRowIndex ) { + [tableSourceView selectRow:destinationRowIndex-1 byExtendingSelection:NO]; } else { - [tableSourceView selectRow:destinationRow byExtendingSelection:NO]; + [tableSourceView selectRow:destinationRowIndex byExtendingSelection:NO]; } } [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; - // Mark the content table for refresh + // Mark the content table and column caches for refresh [tablesListInstance setContentRequiresReload:YES]; + [tableDataInstance resetColumnData]; + [originalRow release]; return YES; } diff --git a/Source/TableStatus.h b/Source/TableStatus.h index cb631435..73b68fd7 100644 --- a/Source/TableStatus.h +++ b/Source/TableStatus.h @@ -29,6 +29,8 @@ @interface TableStatus : NSObject { + IBOutlet id tableDataInstance; + IBOutlet id commentsBox; IBOutlet id rowsNumber; IBOutlet id rowsFormat; diff --git a/Source/TableStatus.m b/Source/TableStatus.m index 5bfd5513..d1a4d45b 100644 --- a/Source/TableStatus.m +++ b/Source/TableStatus.m @@ -1,4 +1,6 @@ #import "TableStatus.h" +#import "SPTableData.h" +#import "SPStringAdditions.h" @implementation TableStatus @@ -13,15 +15,21 @@ [mySQLConnection retain]; } -- (NSString*)getSQLColumnValue:(NSString *)withName usingFields:(NSDictionary*)fields withLabel:(NSString*)label +- (NSString*)formatValueWithKey:(NSString *)aKey inDictionary:(NSDictionary*)statusDict withLabel:(NSString*)label { - NSString* value = [fields objectForKey:withName]; - if([value isKindOfClass:[NSNull class]]) - { - value = @"--"; + NSString* value = [statusDict objectForKey:aKey]; + if([value isKindOfClass:[NSNull class]]) { + value = @"--"; + } else { + + // Format size strings + if ([aKey isEqualToString:@"Data_length"] || [aKey isEqualToString:@"Max_data_length"] + || [aKey isEqualToString:@"Index_length"] || [aKey isEqualToString:@"Data_free"]) { + value = [NSString stringForByteSize:[value intValue]]; + } } - NSString* labelVal = [NSString stringWithFormat:@"%@: %@",label,value]; + NSString* labelVal = [NSString stringWithFormat:@"%@: %@", label, value]; return labelVal; } @@ -31,76 +39,61 @@ // Store the table name away for future use... selectedTable = aTable; - // Notify any listeners that a query is about to begin... - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryWillBePerformed" object:self]; - - // no table selected + // No table selected if([aTable isEqualToString:@""] || !aTable) { [tableName setStringValue:@"Name: --"]; [tableType setStringValue:@"Type: --"]; [tableCreatedAt setStringValue:@"Created At: --"]; [tableUpdatedAt setStringValue:@"Updated At: --"]; - + // Assign the row values... [rowsNumber setStringValue:@"Number Of: --"]; [rowsFormat setStringValue:@"Format: --"]; [rowsAvgLength setStringValue:@"Avg. Length: --"]; [rowsAutoIncrement setStringValue:@"Auto Increment: --"]; - + // Assign the size values... [sizeData setStringValue:@"Data: --"]; [sizeMaxData setStringValue:@"Max Data: --"]; [sizeIndex setStringValue:@"Index: --"]; [sizeFree setStringValue:@"Free: --"]; - + // Finally, set the value of the comments box [commentsBox setStringValue:@"--"]; - - // Tell everyone we've finished with our query... - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; + return; } - - // Run the query to retrieve the status of the selected table. We'll then use this information to populate - // the associated view's controls. - tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", selectedTable]]; - - statusFields = [tableStatusResult fetchRowAsDictionary]; - + + // Retrieve the table status information via the table data cache + statusFields = [tableDataInstance statusValues]; + // Assign the table values... [tableName setStringValue:[NSString stringWithFormat:@"Name: %@",selectedTable]]; - if ( [statusFields objectForKey:@"Type"] ) { - [tableType setStringValue:[self getSQLColumnValue:@"Type" usingFields:statusFields withLabel:@"Type"]]; - } else { - // mysql > 4.1 - [tableType setStringValue:[self getSQLColumnValue:@"Engine" usingFields:statusFields withLabel:@"Type"]]; - } - [tableCreatedAt setStringValue:[self getSQLColumnValue:@"Create_time" usingFields:statusFields withLabel:@"Created At"]]; - [tableUpdatedAt setStringValue:[self getSQLColumnValue:@"Update_time" usingFields:statusFields withLabel:@"Updated At"]]; - + [tableType setStringValue:[self formatValueWithKey:@"Engine" inDictionary:statusFields withLabel:@"Type"]]; + [tableCreatedAt setStringValue:[self formatValueWithKey:@"Create_time" inDictionary:statusFields withLabel:@"Created At"]]; + [tableUpdatedAt setStringValue:[self formatValueWithKey:@"Update_time" inDictionary:statusFields withLabel:@"Updated At"]]; + // Assign the row values... - [rowsNumber setStringValue:[self getSQLColumnValue:@"Rows" usingFields:statusFields withLabel:@"Number Of"]]; - [rowsFormat setStringValue:[self getSQLColumnValue:@"Row_format" usingFields:statusFields withLabel:@"Format"]]; - [rowsAvgLength setStringValue:[self getSQLColumnValue:@"Avg_row_length" usingFields:statusFields withLabel:@"Avg. Length"]]; - [rowsAutoIncrement setStringValue:[self getSQLColumnValue:@"Auto_increment" usingFields:statusFields withLabel:@"Auto Increment"]]; + [rowsNumber setStringValue:[self formatValueWithKey:@"Rows" inDictionary:statusFields withLabel:@"Approx. Number"]]; + [rowsFormat setStringValue:[self formatValueWithKey:@"Row_format" inDictionary:statusFields withLabel:@"Format"]]; + [rowsAvgLength setStringValue:[self formatValueWithKey:@"Avg_row_length" inDictionary:statusFields withLabel:@"Avg. Length"]]; + [rowsAutoIncrement setStringValue:[self formatValueWithKey:@"Auto_increment" inDictionary:statusFields withLabel:@"Auto Increment"]]; // Assign the size values... - [sizeData setStringValue:[self getSQLColumnValue:@"Data_length" usingFields:statusFields withLabel:@"Data"]]; - [sizeMaxData setStringValue:[self getSQLColumnValue:@"Max_data_length" usingFields:statusFields withLabel:@"Max Data"]]; - [sizeIndex setStringValue:[self getSQLColumnValue:@"Index_length" usingFields:statusFields withLabel:@"Index"]]; - [sizeFree setStringValue:[self getSQLColumnValue:@"Data_free" usingFields:statusFields withLabel:@"Free"]]; - + [sizeData setStringValue:[self formatValueWithKey:@"Data_length" inDictionary:statusFields withLabel:@"Data"]]; + [sizeMaxData setStringValue:[self formatValueWithKey:@"Max_data_length" inDictionary:statusFields withLabel:@"Max Data"]]; + [sizeIndex setStringValue:[self formatValueWithKey:@"Index_length" inDictionary:statusFields withLabel:@"Index"]]; + [sizeFree setStringValue:[self formatValueWithKey:@"Data_free" inDictionary:statusFields withLabel:@"Free"]]; + // Finally, assign the comments... [commentsBox setStringValue:[statusFields objectForKey:@"Comment"]]; - - // Tell everyone we've finished with our query... - [[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self]; - + return; } - (IBAction)reloadTable:(id)sender { + [tableDataInstance resetStatusData]; [self loadTable:selectedTable]; } diff --git a/Source/TablesList.h b/Source/TablesList.h index 7270219b..d2cb20da 100644 --- a/Source/TablesList.h +++ b/Source/TablesList.h @@ -35,6 +35,7 @@ IBOutlet id tableContentInstance; IBOutlet id customQueryInstance; IBOutlet id tableDumpInstance; + IBOutlet id tableDataInstance; IBOutlet id tableStatusInstance; IBOutlet id tableWindow; @@ -68,7 +69,7 @@ - (void)doPerformQueryService:(NSString *)query; //getter methods -- (NSString *)table; +- (NSString *)tableName; - (NSArray *)tables; - (BOOL)structureLoaded; - (BOOL)contentLoaded; diff --git a/Source/TablesList.m b/Source/TablesList.m index 40ea87c5..e92c7b13 100644 --- a/Source/TablesList.m +++ b/Source/TablesList.m @@ -26,6 +26,7 @@ #import "TableDocument.h" #import "TableSource.h" #import "TableContent.h" +#import "SPTableData.h" #import "TableDump.h" #import "ImageAndTextCell.h" #import "CMMCPConnection.h" @@ -295,7 +296,7 @@ selects customQuery tab and passes query to customQueryInstance /* returns the currently selected table or nil if no table or mulitple tables are selected */ -- (NSString *)table +- (NSString *)tableName { if ( [tablesListView numberOfSelectedRows] == 1 ) { return [tables objectAtIndex:[tablesListView selectedRow]]; @@ -395,9 +396,8 @@ Mark the content table for refresh when it's next switched to contentLoaded = NO; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { - [tableSourceInstance loadTable:anObject]; [tableContentInstance loadTable:anObject]; - structureLoaded = YES; + structureLoaded = NO; contentLoaded = YES; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { @@ -451,9 +451,8 @@ Mark the content table for refresh when it's next switched to contentLoaded = NO; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { - [tableSourceInstance loadTable:anObject]; [tableContentInstance loadTable:anObject]; - structureLoaded = YES; + structureLoaded = NO; contentLoaded = YES; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { @@ -549,15 +548,27 @@ traps enter and esc and edit/cancel without entering next row - (void)tableViewSelectionDidChange:(NSNotification *)aNotification { if ( [tablesListView numberOfSelectedRows] == 1 ) { + + // Reset the table information caches + [tableDataInstance resetAllData]; + + // If encoding is set to Autodetect, update the connection character set encoding + // based on the newly selected table's encoding - but only if it differs from the current encoding. + if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"encoding"] isEqualToString:@"Autodetect"]) { + if (![[tableDataInstance tableEncoding] isEqualToString:[tableDocumentInstance connectionEncoding]]) { + [tableDocumentInstance setConnectionEncoding:[tableDataInstance tableEncoding] reloadingViews:NO]; + [tableDataInstance resetAllData]; + } + } + if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0 ) { [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; structureLoaded = YES; contentLoaded = NO; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1 ) { - [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; [tableContentInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; - structureLoaded = YES; + structureLoaded = NO; contentLoaded = YES; statusLoaded = NO; } else if ( [tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 3 ) { @@ -574,11 +585,6 @@ traps enter and esc and edit/cancel without entering next row // set window title [tableWindow setTitle:[NSString stringWithFormat:@"(MySQL %@) %@@%@/%@/%@", [tableDocumentInstance mySQLVersion], [tableDocumentInstance user], [tableDocumentInstance host], [tableDocumentInstance database], [tables objectAtIndex:[tablesListView selectedRow]]]]; - - // Update connection characater set encoding based on the table's encoding if required - if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"encoding"] isEqualToString:@"Autodetect"]) { - [tableDocumentInstance detectTableEncodingForTable:[tables objectAtIndex:[tablesListView selectedRow]]]; - } } else { [tableSourceInstance loadTable:nil]; [tableContentInstance loadTable:nil]; @@ -646,18 +652,11 @@ loads structure or source if tab selected the first time if ( [tablesListView numberOfSelectedRows] == 1 ) { if ( ([tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 0) && !structureLoaded ) { - [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; structureLoaded = YES; } if ( ([tabView indexOfTabViewItem:[tabView selectedTabViewItem]] == 1) && !contentLoaded ) { - - if ( !structureLoaded ) { - [tableSourceInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; - structureLoaded = YES; - } - [tableContentInstance loadTable:[tables objectAtIndex:[tablesListView selectedRow]]]; contentLoaded = YES; } @@ -683,6 +682,7 @@ loads structure or source if tab selected the first time tables = [[NSMutableArray alloc] init]; structureLoaded = NO; contentLoaded = NO; + statusLoaded = NO; [tables addObject:NSLocalizedString(@"TABLES",@"header for table list")]; return self; } |