From 2525366dbfed3aef78beaed89630ea543389cec1 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Wed, 18 Feb 2009 21:07:43 +0000 Subject: Visible improvements in this build: - Significantly reduce the queries that have to be performed, improving lag - especially over slow connections (Issue #118; see new controller info under headline code changes). - Fix Issue #117 properly (export numeric quoting - we now have access to column types and so can quote appropriately). - Fix Issue #145 (loss of unsigned/null/default attributes when reordering columns). - Fixes Issue #90 (support for filtering DECIMAL column types) - Improve table scrolling speed when the table contains long items. (Added a NSFormatter to automatically truncate strings > 150 chars for display purposes only) - Improved SQL compatibility - for example /* C style comments */ are now correctly ignored in imports and custom queries. - Add text and symbols emphasising that the table info pane / status view row count is an approximation (partially addresses Issue #141) - Fixes a major memory leak whenever opening or scrolling tables containing text/blob data. - SQL import is now faster (SQL parsing part is 3x faster). - Speed up SQL export (1.5x faster for numeric data; 1.1x faster for string data) and slightly speed up CSV export (~1.1x faster). - Display sizes on the status view using the byte size formatter, as per table info pane. Headline code changes: - Add a new NSMutableString subclass, SPSQLParser. See the header file for documentation and overview, but in short it's a centralised place for SQL parsing. Centralises and improves parsing, improves comment support, improves quoting support. Despite the improved featureset this is also faster than the previous distributed implementations - for example, when used to replace the old splitQueries:, > 3x speedup. - Implement a new controller which handles a structure and status cache for the current table, and provides structure parsing for specified tables. This cache is now used throughout the code, reducing the queries that have to be performed and providing additional information about the table structure for use; I think it also improves column type format slightly. - The table info pane and the status view now draw all their data from the cache. Tweaks: - Table encoding is now detected directly instead of being derived from the collation - increased accuracy and cope with the DEFAULT encoding. - Comments and formatting cleaned up in bits I was working on, obviously. - A couple of methods - particularly [tablesListInstance table] and [tableDocument encoding] - have been renamed to avoid conflicts and fix code warnings. Future improvements now possible: - As we now have access to column types and other information, we can provide per-type behaviour where desired. - The table parsing doesn't currently pull out comments or table indices, together with one or two other attributes. Some of this would be useful for display; some, such as indices, could be used to draw the table structure view as long as we're happy discarding a couple of columns (ie cardinality!) --- Interfaces/English.lproj/DBView.xib | 138 ++++++-- Source/CustomQuery.h | 2 - Source/CustomQuery.m | 18 +- Source/SPDataCellFormatter.h | 31 ++ Source/SPDataCellFormatter.m | 57 +++ Source/SPSQLParser.h | 233 +++++++++++++ Source/SPSQLParser.m | 655 +++++++++++++++++++++++++++++++++++ Source/SPTableData.h | 54 +++ Source/SPTableData.m | 509 +++++++++++++++++++++++++++ Source/SPTableInfo.h | 1 + Source/SPTableInfo.m | 60 ++-- Source/TableContent.h | 7 +- Source/TableContent.m | 599 +++++++++++++++----------------- Source/TableDocument.h | 7 +- Source/TableDocument.m | 69 ++-- Source/TableDump.h | 4 +- Source/TableDump.m | 181 ++++------ Source/TableSource.h | 1 + Source/TableSource.m | 106 +++--- Source/TableStatus.h | 2 + Source/TableStatus.m | 83 ++--- Source/TablesList.h | 3 +- Source/TablesList.m | 38 +- sequel-pro.xcodeproj/project.pbxproj | 26 ++ 24 files changed, 2235 insertions(+), 649 deletions(-) create mode 100644 Source/SPDataCellFormatter.h create mode 100644 Source/SPDataCellFormatter.m create mode 100644 Source/SPSQLParser.h create mode 100644 Source/SPSQLParser.m create mode 100644 Source/SPTableData.h create mode 100644 Source/SPTableData.m diff --git a/Interfaces/English.lproj/DBView.xib b/Interfaces/English.lproj/DBView.xib index 392632b3..30e81caa 100644 --- a/Interfaces/English.lproj/DBView.xib +++ b/Interfaces/English.lproj/DBView.xib @@ -9612,6 +9612,9 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 SPTableInfo + + SPTableData + @@ -10584,14 +10587,6 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 854 - - - tableSourceInstance - - - - 855 - queryProgressBar @@ -11064,14 +11059,6 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 1238 - - - tableDumpInstance - - - - 1239 - tableTabView @@ -11828,6 +11815,78 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 4701 + + + tableDocumentInstance + + + + 4705 + + + + tableListInstance + + + + 4706 + + + + tableDataInstance + + + + 4707 + + + + tableDataInstance + + + + 4708 + + + + tableDataInstance + + + + 4709 + + + + tableDataInstance + + + + 4710 + + + + tableDataInstance + + + + 4711 + + + + tableDataInstance + + + + 4712 + + + + tableDataInstance + + + + 4713 + @@ -16884,6 +16943,12 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 + + 4702 + + + SPTableData + @@ -17874,6 +17939,7 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 47.IBPluginDependency 47.ImportedFromIB2 4700.IBPluginDependency + 4702.IBPluginDependency 495.IBAttributePlaceholdersKey 495.IBPluginDependency 495.ImportedFromIB2 @@ -19381,6 +19447,7 @@ IGRvIHlvdSB3YW50IHRvIGFkZCBmb3IgdGhpcyBmaWVsZD8 com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin ToolTip @@ -19805,7 +19872,7 @@ Y2hhbmdlIHRoZSBvcmRlcg - 4701 + 4713 @@ -19901,7 +19968,6 @@ Y2hhbmdlIHRoZSBvcmRlcg queryFavoritesSheet queryFavoritesView queryHistoryButton - tableDumpInstance tableWindow textView valueSheet @@ -19920,7 +19986,6 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id - id @@ -19987,6 +20052,27 @@ Y2hhbmdlIHRoZSBvcmRlcg Source/CMMCPConnection.h + + SPTableData + NSObject + + YES + + YES + tableDocumentInstance + tableListInstance + + + YES + id + id + + + + IBProjectSource + Source/SPTableData.h + + SPTableInfo NSObject @@ -19995,6 +20081,7 @@ Y2hhbmdlIHRoZSBvcmRlcg YES infoTable + tableDataInstance tableDocumentInstance tableList tableListInstance @@ -20005,6 +20092,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id @@ -20075,8 +20163,8 @@ Y2hhbmdlIHRoZSBvcmRlcg multipleLineEditingButton removeButton tableContentView + tableDataInstance tableDocumentInstance - tableSourceInstance tableWindow tablesListInstance @@ -20227,6 +20315,7 @@ Y2hhbmdlIHRoZSBvcmRlcg syntaxView syntaxViewContent tableContentInstance + tableDataInstance tableDumpInstance tableSourceInstance tableStatusInstance @@ -20267,6 +20356,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id NSTabView id id @@ -20391,6 +20481,7 @@ Y2hhbmdlIHRoZSBvcmRlcg singleProgressSheet singleProgressText tableContentInstance + tableDataInstance tableDocumentInstance tableListView tableSourceInstance @@ -20446,6 +20537,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id @@ -20512,6 +20604,7 @@ Y2hhbmdlIHRoZSBvcmRlcg removeFieldButton removeIndexButton structureGrabber + tableDataInstance tableSourceView tableTypeButton tableWindow @@ -20536,6 +20629,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id @@ -20572,6 +20666,7 @@ Y2hhbmdlIHRoZSBvcmRlcg sizeIndex sizeMaxData tableCreatedAt + tableDataInstance tableName tableType tableUpdatedAt @@ -20591,6 +20686,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id @@ -20638,6 +20734,7 @@ Y2hhbmdlIHRoZSBvcmRlcg customQueryInstance tabView tableContentInstance + tableDataInstance tableDocumentInstance tableDumpInstance tableSourceInstance @@ -20659,6 +20756,7 @@ Y2hhbmdlIHRoZSBvcmRlcg id id id + id 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 #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 + +#import + + +@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 + +#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 + +#import + + +/* + * 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 + +#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 + +#import + + +@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 + +#import "CMMCPConnection.h" +#import "CMMCPResult.h" +#import +#import "SPTableData.h" +#import "SPSQLParser.h" +#import "TableDocument.h" +#import "TablesList.h" + + +@implementation SPTableData + + +- (id) init +{ + if ((self = [super init])) { + columns = [[NSMutableArray alloc] init]; + columnNames = [[NSMutableArray alloc] init]; + status = [[NSMutableDictionary alloc] init]; + tableEncoding = nil; + mySQLConnection = nil; + } + + return self; +} + +/* + * Set the connection for use. + * Called by the connect sheet methods. + */ +- (void) setConnection:(CMMCPConnection *)theConnection +{ + mySQLConnection = theConnection; + [mySQLConnection retain]; +} + + +/* + * Retrieve the encoding for the current table, using or refreshing the cache as appropriate. + */ +- (NSString *) tableEncoding +{ + if (tableEncoding == nil) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return [NSString stringWithString:tableEncoding]; +} + + +/* + * Retrieve all columns for the current table as an array, using or refreshing the cache as appropriate. + */ +- (NSArray *) columns +{ + if ([columns count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return columns; +} + + +/* + * Retrieve a column with a specified name, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) columnWithName:(NSString *)colName +{ + int columnIndex = [columnNames indexOfObject:colName]; + return [columns objectAtIndex:columnIndex]; +} + + +/* + * Retrieve column names for the current table as an array, using or refreshing the cache as appropriate. + */ +- (NSArray *) columnNames +{ + if ([columnNames count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return columnNames; +} + + +/* + * Retrieve a specified column for the current table as a dictionary, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) columnAtIndex:(int)index +{ + if ([columns count] == 0) { + [self updateInformationFromCreateTableForCurrentTable]; + } + return [columns objectAtIndex:index]; +} + + +/* + * Retrieve the table status value for a supplied key, using or refreshing the cache as appropriate. + */ +- (NSString *) statusValueForKey:(NSString *)aKey +{ + if ([status count] == 0) { + [self updateStatusInformationForCurrentTable]; + } + return [status objectForKey:aKey]; +} + + +/* + * Retrieve all known status values as a dictionary, using or refreshing the cache as appropriate. + */ +- (NSDictionary *) statusValues +{ + if ([status count] == 0) { + [self updateStatusInformationForCurrentTable]; + } + return status; +} + + +/* + * Flushes all caches - should be used on major changes, for example table changes. + */ +- (void) resetAllData +{ + [columns removeAllObjects]; + [columnNames removeAllObjects]; + [status removeAllObjects]; + if (tableEncoding != nil) { + [tableEncoding release]; + tableEncoding = nil; + } +} + + +/* + * Flushes any status-related caches. + */ +- (void) resetStatusData +{ + [status removeAllObjects]; +} + + +/* + * Flushes any field/column-related caches. + */ +- (void) resetColumnData +{ + [columns removeAllObjects]; + [columnNames removeAllObjects]; +} + + +/* + * Retrieves the information for the current table and stores it in cache. + * Returns a boolean indicating success. + */ +- (BOOL) updateInformationFromCreateTableForCurrentTable +{ + NSDictionary *tableData = [self informationFromCreateTableSyntaxForTable:[tableListInstance tableName]]; + NSDictionary *columnData; + NSEnumerator *enumerator; + + if (tableData == nil) { + [columns removeAllObjects]; + [columnNames removeAllObjects]; + return FALSE; + } + + [columns addObjectsFromArray:[tableData objectForKey:@"columns"]]; + + enumerator = [columns objectEnumerator]; + while (columnData = [enumerator nextObject]) { + [columnNames addObject:[NSString stringWithString:[columnData objectForKey:@"name"]]]; + } + + if (tableEncoding != nil) { + [tableEncoding release]; + } + tableEncoding = [[NSString alloc] initWithString:[tableData objectForKey:@"encoding"]]; + + return TRUE; +} + + +/* + * Retrieve the CREATE TABLE string for a table and analyse it to extract the field + * details and table encoding. + * In future this could also be used to retrieve the majority of index information + * assuming information like cardinality isn't needed. + * This function is rather long due to the painful parsing required, but is fast. + * Returns a boolean indicating success. + */ +- (NSDictionary *) informationFromCreateTableSyntaxForTable:(NSString *)tableName +{ + SPSQLParser *createTableParser, *fieldsParser, *fieldParser, *detailParser; + NSMutableArray *tableColumns, *fieldStrings, *definitionParts, *detailParts; + NSMutableDictionary *tableColumn, *tableData; + NSString *detailString, *encodingString; + unsigned i, j, stringStart, partsArrayLength; + + // Catch unselected tables and return nil + if ([tableName isEqualToString:@""] || !tableName) return nil; + + // Retrieve the CREATE TABLE syntax for the table + CMMCPResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE `%@`", tableName]]; + + // Check for any errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table information:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return nil; + } + + // Retrieve the table syntax string + NSArray *syntaxResult = [theResult fetchRowAsArray]; + if ([[syntaxResult objectAtIndex:1] isKindOfClass:[NSData class]]) { + createTableParser = [[SPSQLParser alloc] initWithData:[syntaxResult objectAtIndex:1] encoding:[mySQLConnection encoding]]; + } else { + createTableParser = [[SPSQLParser alloc] initWithString:[syntaxResult objectAtIndex:1]]; + } + + // Extract the fields definition string from the CREATE TABLE syntax + fieldsParser = [[SPSQLParser alloc] initWithString:[createTableParser trimAndReturnStringFromCharacter:'(' toCharacter:')' trimmingInclusively:YES returningInclusively:NO skippingBrackets:YES]]; + + // Split the fields and keys string into an array of individual elements + fieldStrings = [[NSMutableArray alloc] initWithArray:[fieldsParser splitStringByCharacter:',' skippingBrackets:YES]]; + + // fieldStrings should now hold unparsed field and key strings, while tableProperty string holds unparsed + // table information. Proceed further by parsing the field strings. + tableColumns = [[NSMutableArray alloc] init]; + tableColumn = [[NSMutableDictionary alloc] init]; + definitionParts = [[NSMutableArray alloc] init]; + fieldParser = [[SPSQLParser alloc] init]; + for (i = 0; i < [fieldStrings count]; i++) { + + // Take this field/key string, trim whitespace from both ends and remove comments + [fieldsParser setString:[[fieldStrings objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + [fieldsParser deleteComments]; + if (![fieldsParser length]) { + continue; + } + [tableColumn removeAllObjects]; + [definitionParts removeAllObjects]; + + // If the first character is a backtick, this is a field definition. + if ([fieldsParser characterAtIndex:0] =='`') { + + // Capture the area between the two backticks as the name + [tableColumn setObject:[fieldsParser trimAndReturnStringFromCharacter:'`' toCharacter:'`' trimmingInclusively:YES returningInclusively:NO ignoringQuotedStrings:NO] forKey:@"name"]; + + // Split the remaining field definition string by spaces ready for processing + [definitionParts addObjectsFromArray:[fieldsParser splitStringByCharacter:' ' skippingBrackets:YES]]; + + // The first item is always the data type. + [fieldParser setString:[definitionParts objectAtIndex:0]]; + + // If no field length definition is present, store only the type + if ([fieldParser firstOccurrenceOfCharacter:'(' ignoringQuotedStrings:YES] == NSNotFound) { + [tableColumn setObject:[fieldParser uppercaseString] forKey:@"type"]; + + // Otherwise separate out the length definition for processing + } else { + detailParser = [[SPSQLParser alloc] initWithString:[[fieldParser stringToCharacter:'(' inclusively:NO] uppercaseString]]; + [tableColumn setObject:[NSString stringWithString:detailParser] forKey:@"type"]; + + // For ENUMs and SETs, capture the field value options into an array for storage + if ([detailParser isEqualToString:@"ENUM"] || [detailParser isEqualToString:@"SET"]) { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + detailParts = [[NSMutableArray alloc] initWithArray:[detailParser splitStringByCharacter:',']]; + for (j = 0; j < [detailParts count]; j++) { + [detailParser setString:[detailParts objectAtIndex:j]]; + [detailParts replaceObjectAtIndex:j withObject:[detailParser unquotedString]]; + } + [tableColumn setObject:[NSArray arrayWithArray:detailParts] forKey:@"values"]; + [detailParts release]; + + // For types with required or optional decimals, store as appropriate + } else if ([detailParser isEqualToString:@"REAL"] || [detailParser isEqualToString:@"DOUBLE"] || [detailParser isEqualToString:@"FLOAT"] || [detailParser isEqualToString:@"DECIMAL"] || [detailParser isEqualToString:@"NUMERIC"]) { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + detailParts = [[NSMutableArray alloc] initWithArray:[detailParser splitStringByCharacter:',']]; + [detailParser setString:[detailParts objectAtIndex:0]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"length"]; + if ([detailParts count] > 1) { + [detailParser setString:[detailParts objectAtIndex:1]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"decimals"]; + } + [detailParts release]; + + // Otherwise capture the length only. + } else { + [detailParser setString:[fieldParser stringFromCharacter:'(' toCharacter:')' inclusively:NO]]; + [tableColumn setObject:[detailParser unquotedString] forKey:@"length"]; + } + [detailParser release]; + } + + // Also capture a general column type "group" to allow behavioural switches + detailString = [[NSString alloc] initWithString:[tableColumn objectForKey:@"type"]]; + if ([detailString isEqualToString:@"BIT"]) { + [tableColumn setObject:@"bit" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"TINYINT"] || [detailString isEqualToString:@"SMALLINT"] || [detailString isEqualToString:@"MEDIUMINT"] + || [detailString isEqualToString:@"INT"] || [detailString isEqualToString:@"INTEGER"] || [detailString isEqualToString:@"BIGINT"]) { + [tableColumn setObject:@"integer" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"REAL"] || [detailString isEqualToString:@"DOUBLE"] || [detailString isEqualToString:@"FLOAT"] + || [detailString isEqualToString:@"DECIMAL"] || [detailString isEqualToString:@"NUMERIC"]) { + [tableColumn setObject:@"float" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"DATE"] || [detailString isEqualToString:@"TIME"] || [detailString isEqualToString:@"TIMESTAMP"] + || [detailString isEqualToString:@"DATETIME"] || [detailString isEqualToString:@"YEAR"]) { + [tableColumn setObject:@"date" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"CHAR"] || [detailString isEqualToString:@"VARCHAR"]) { + [tableColumn setObject:@"string" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"BINARY"] || [detailString isEqualToString:@"VARBINARY"]) { + [tableColumn setObject:@"binary" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"ENUM"] || [detailString isEqualToString:@"SET"]) { + [tableColumn setObject:@"enum" forKey:@"typegrouping"]; + } else if ([detailString isEqualToString:@"TINYTEXT"] || [detailString isEqualToString:@"TEXT"] + || [detailString isEqualToString:@"MEDIUMTEXT"] || [detailString isEqualToString:@"LONGTEXT"]) { + [tableColumn setObject:@"textdata" forKey:@"typegrouping"]; + + // Default to "blobdata". This means that future and currently unsupported types - including spatial extensions - + // will be preserved unmangled. + } else { + [tableColumn setObject:@"blobdata" forKey:@"typegrouping"]; + } + + // Set up some column defaults for all columns + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"null"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"unsigned"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"binary"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"zerofill"]; + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"autoincrement"]; + + // Walk through the remaining column definition parts storing recognised details + partsArrayLength = [definitionParts count]; + for (j = 1; j < partsArrayLength; j++) { + detailString = [[NSString alloc] initWithString:[[definitionParts objectAtIndex:j] uppercaseString]]; + + // Whether numeric fields are unsigned + if ([detailString isEqualToString:@"UNSIGNED"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"unsigned"]; + + // Whether numeric fields are zerofill + } else if ([detailString isEqualToString:@"ZEROFILL"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"zerofill"]; + + // Whether text types are binary + } else if ([detailString isEqualToString:@"BINARY"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"binary"]; + + // Whether text types have a different encoding to the table + } else if ([detailString isEqualToString:@"CHARSET"] && (j + 1 < partsArrayLength)) { + if (![[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"DEFAULT"]) { + [tableColumn setValue:[definitionParts objectAtIndex:j+1] forKey:@"encoding"]; + } + j++; + } else if ([detailString isEqualToString:@"CHARACTER"] && (j + 2 < partsArrayLength) + && [[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"SET"]) { + if (![[[definitionParts objectAtIndex:j+2] uppercaseString] isEqualToString:@"DEFAULT"]) {; + [tableColumn setValue:[definitionParts objectAtIndex:j+2] forKey:@"encoding"]; + } + j = j + 2; + + // Whether text types have a different collation to the table + } else if ([detailString isEqualToString:@"COLLATE"] && (j + 1 < partsArrayLength)) { + if (![[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"DEFAULT"]) { + [tableColumn setValue:[definitionParts objectAtIndex:j+1] forKey:@"collation"]; + } + j++; + + // Whether fields are NOT NULL + } else if ([detailString isEqualToString:@"NOT"] && (j + 1 < partsArrayLength) + && [[[definitionParts objectAtIndex:j+1] uppercaseString] isEqualToString:@"NULL"]) { + [tableColumn setValue:[NSNumber numberWithBool:NO] forKey:@"null"]; + j++; + + // Whether fields are NULL + } else if ([detailString isEqualToString:@"NULL"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"null"]; + + // Whether fields should auto-increment + } else if ([detailString isEqualToString:@"AUTO_INCREMENT"]) { + [tableColumn setValue:[NSNumber numberWithBool:YES] forKey:@"autoincrement"]; + + // Field defaults + } else if ([detailString isEqualToString:@"DEFAULT"] && (j + 1 < partsArrayLength)) { + detailParser = [[SPSQLParser alloc] initWithString:[definitionParts objectAtIndex:j+1]]; + [tableColumn setValue:[detailParser unquotedString] forKey:@"default"]; + [detailParser release]; + j++; + } + + // TODO: Currently unhandled: [UNIQUE | PRIMARY] KEY | COMMENT 'foo' | COLUMN_FORMAT bar | STORAGE q | REFERENCES... + + [detailString release]; + } + + // Store the column. + [tableColumns addObject:[NSDictionary dictionaryWithDictionary:tableColumn]]; + + // TODO: Otherwise it's a key definition, constraint, check, or other 'metadata'. Would be useful to parse/display these! + } else { + + } + } + [fieldStrings release]; + [fieldsParser release]; + [definitionParts release]; + [tableColumn release]; + + // Extract the encoding from the table properties string - other details come from TABLE STATUS. + NSRange charsetDefinitionRange = [createTableParser rangeOfString:@"CHARSET=" options:NSCaseInsensitiveSearch]; + if (charsetDefinitionRange.location == NSNotFound) { + charsetDefinitionRange = [createTableParser rangeOfString:@"CHARACTER SET=" options:NSCaseInsensitiveSearch]; + } + if (charsetDefinitionRange.location != NSNotFound) { + stringStart = charsetDefinitionRange.location + charsetDefinitionRange.length; + for (i = stringStart; i < [createTableParser length]; i++) { + if ([createTableParser characterAtIndex:i] == ' ') break; + } + + // Catch the "default" character encoding: + if ([[[createTableParser substringWithRange:NSMakeRange(stringStart, i-stringStart)] lowercaseString] isEqualToString:@"default"]) { + encodingString = [[NSString alloc] initWithString:[tableDocumentInstance databaseEncoding]]; + } else { + encodingString = [[NSString alloc] initWithString:[createTableParser substringWithRange:NSMakeRange(stringStart, i-stringStart)]]; + } + + // If no DEFAULT CHARSET is present, it's likely MySQL < 4; fall back to latin1. + } else { + encodingString = [[NSString alloc] initWithString:@"latin1"]; + } + + [createTableParser release]; + [fieldParser release]; + + tableData = [NSMutableDictionary dictionary]; + [tableData setObject:[NSString stringWithString:encodingString] forKey:@"encoding"]; + [tableData setObject:[NSArray arrayWithArray:tableColumns] forKey:@"columns"]; + + [encodingString release]; + [tableColumns release]; + + return tableData; +} + + +/* + * Retrieve the status of a table as a dictionary and add it to the local cache for reuse. + */ +- (BOOL) updateStatusInformationForCurrentTable +{ + + // Catch unselected tables and return nil + if ([[tableListInstance tableName] isEqualToString:@""] || ![tableListInstance tableName]) return nil; + + // Run the status query and retrieve as a dictionary. + CMMCPResult *tableStatusResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW TABLE STATUS LIKE '%@'", [tableListInstance tableName]]]; + + // Check for any errors + if (![[mySQLConnection getLastErrorMessage] isEqualToString:@""]) { + NSRunAlertPanel(@"Error", [NSString stringWithFormat:@"An error occured while retrieving table status:\n\n%@", [mySQLConnection getLastErrorMessage]], @"OK", nil, nil); + return FALSE; + } + + // Retrieve the status as a dictionary and set as the cache + [status setDictionary:[tableStatusResult fetchRowAsDictionary]]; + + // Reassign any "Type" key - for MySQL < 4.1 - to "Engine" for consistency. + if ([status objectForKey:@"Type"]) { + [status setObject:[status objectForKey:@"Type"] forKey:@"Engine"]; + } + + return TRUE; +} + + +- (void) dealloc +{ + [columns release]; + [columnNames release]; + [status release]; + if (tableEncoding != nil) [tableEncoding release]; + + [super dealloc]; +} + +@end 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 @@ -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 )info row:(int)destinationRow dropOperation:(NSTableViewDropOperation)operation + * Having validated a drop, perform the field/column reordering to match. + */ +- (BOOL)tableView:(NSTableView*)tableView acceptDrop:(id )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; } diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 39163b74..0c4f7839 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -93,6 +93,9 @@ 4DECC48F0EC2B436008D359E /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4DECC3320EC2A170008D359E /* Sparkle.framework */; }; 4DECC4900EC2B436008D359E /* MCPKit_bundled.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4DECC3330EC2A170008D359E /* MCPKit_bundled.framework */; }; 4DECC4910EC2B436008D359E /* Growl.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4DECC3340EC2A170008D359E /* Growl.framework */; }; + 58C56EF50F438E120035701E /* SPDataCellFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C56EF40F438E120035701E /* SPDataCellFormatter.m */; }; + 58FEF16D0F23D66600518E8E /* SPSQLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF16C0F23D66600518E8E /* SPSQLParser.m */; }; + 58FEF57E0F3B4E9700518E8E /* SPTableData.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF57D0F3B4E9700518E8E /* SPTableData.m */; }; 8D15AC340486D014006FF6A4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */; }; B5E2C5FA0F2353B5007446E0 /* TablePropertyIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E2C5F90F2353B5007446E0 /* TablePropertyIcon.png */; }; B5EAC0FD0EC87FF900CC579C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5EAC0FC0EC87FF900CC579C /* Security.framework */; }; @@ -245,6 +248,12 @@ 4DECC3320EC2A170008D359E /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = ""; }; 4DECC3330EC2A170008D359E /* MCPKit_bundled.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MCPKit_bundled.framework; path = Frameworks/MCPKit_bundled.framework; sourceTree = ""; }; 4DECC3340EC2A170008D359E /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = Frameworks/Growl.framework; sourceTree = ""; }; + 58C56EF30F438E120035701E /* SPDataCellFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDataCellFormatter.h; sourceTree = ""; }; + 58C56EF40F438E120035701E /* SPDataCellFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDataCellFormatter.m; sourceTree = ""; }; + 58FEF16B0F23D66600518E8E /* SPSQLParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPSQLParser.h; sourceTree = ""; }; + 58FEF16C0F23D66600518E8E /* SPSQLParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSQLParser.m; sourceTree = ""; }; + 58FEF57C0F3B4E9700518E8E /* SPTableData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableData.h; sourceTree = ""; }; + 58FEF57D0F3B4E9700518E8E /* SPTableData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableData.m; sourceTree = ""; }; 8D15AC370486D014006FF6A4 /* Sequel Pro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sequel Pro.app"; sourceTree = BUILT_PRODUCTS_DIR; }; B5E2C5F90F2353B5007446E0 /* TablePropertyIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TablePropertyIcon.png; sourceTree = ""; }; B5EAC0FC0EC87FF900CC579C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -379,6 +388,8 @@ 17E641530EF01EF6001BC333 /* TableDump.m */, 17E641540EF01EF6001BC333 /* TableSource.h */, 17E641550EF01EF6001BC333 /* TableSource.m */, + 58FEF57C0F3B4E9700518E8E /* SPTableData.h */, + 58FEF57D0F3B4E9700518E8E /* SPTableData.m */, ); name = Controllers; sourceTree = ""; @@ -414,6 +425,7 @@ 1789343B0F30C1DD0097539A /* SPStringAdditions.m */, 17E6416F0EF01F4C001BC333 /* Keychain */, 17E641700EF01F52001BC333 /* MCPKit */, + 58FEF15E0F23D60A00518E8E /* Parsing */, 17E641710EF01F5C001BC333 /* GUI */, 17E641720EF01F6B001BC333 /* SSHTunnel */, ); @@ -449,6 +461,8 @@ 17E6417F0EF01FA8001BC333 /* CMImageView.m */, 17E641800EF01FA8001BC333 /* CMTextView.h */, 17E641810EF01FA8001BC333 /* CMTextView.m */, + 58C56EF30F438E120035701E /* SPDataCellFormatter.h */, + 58C56EF40F438E120035701E /* SPDataCellFormatter.m */, ); name = GUI; sourceTree = ""; @@ -560,6 +574,15 @@ name = Frameworks; sourceTree = ""; }; + 58FEF15E0F23D60A00518E8E /* Parsing */ = { + isa = PBXGroup; + children = ( + 58FEF16B0F23D66600518E8E /* SPSQLParser.h */, + 58FEF16C0F23D66600518E8E /* SPSQLParser.m */, + ); + name = Parsing; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -712,7 +735,10 @@ 17E641830EF01FA8001BC333 /* CMImageView.m in Sources */, 17E641840EF01FA8001BC333 /* CMTextView.m in Sources */, 17E641890EF01FB4001BC333 /* SSHTunnel.m in Sources */, + 58FEF16D0F23D66600518E8E /* SPSQLParser.m in Sources */, 1789343C0F30C1DD0097539A /* SPStringAdditions.m in Sources */, + 58FEF57E0F3B4E9700518E8E /* SPTableData.m in Sources */, + 58C56EF50F438E120035701E /* SPDataCellFormatter.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- cgit v1.2.3