aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Frameworks/PostgresKit/README.md (renamed from Frameworks/PostgresKit/README)29
-rw-r--r--Frameworks/SPMySQLFramework/LICENSE26
-rw-r--r--Frameworks/SPMySQLFramework/README.md43
-rw-r--r--Frameworks/SPMySQLFramework/Readme.txt69
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h1
-rw-r--r--Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m67
-rw-r--r--Resources/Plists/PreferenceDefaults.plist2
-rw-r--r--Source/SPAppController.m10
-rw-r--r--Source/SPConnectionController.m12
-rw-r--r--Source/SPConstants.h4
-rw-r--r--Source/SPConstants.m1
-rw-r--r--Source/SPFieldEditorController.h1
-rw-r--r--Source/SPFieldEditorController.m30
-rw-r--r--Source/SPJSONFormatter.h123
-rw-r--r--Source/SPJSONFormatter.m364
-rw-r--r--Source/SPSQLExporter.m40
-rw-r--r--Source/SPSSHTunnel.m47
-rw-r--r--Source/SPTablesPreferencePane.m6
-rw-r--r--UnitTests/SPJSONFormatterTests.m141
-rw-r--r--readme.md4
-rw-r--r--sequel-pro.xcodeproj/project.pbxproj18
21 files changed, 893 insertions, 145 deletions
diff --git a/Frameworks/PostgresKit/README b/Frameworks/PostgresKit/README.md
index d4cdefea..063dacc4 100644
--- a/Frameworks/PostgresKit/README
+++ b/Frameworks/PostgresKit/README.md
@@ -1,10 +1,9 @@
-POSTGRESKIT README
-------------------
+# PostgresKit
PostgresKit is a fork and heavily modified version of the PostgresClientKit
code from the PostgresKit project:
- http://code.google.com/p/postgres-kit/
+[http://code.google.com/p/postgres-kit/](http://code.google.com/p/postgres-kit/)
PostgresClientKit was originally written by David Thorpe and is licensed under
version 2 of the Apache License.
@@ -12,7 +11,9 @@ version 2 of the Apache License.
This PostgresKit fork was created by Stuart Connolly on July 22, 2012 and
is to be developed as part of the Sequel Pro project:
- http://sequelpro.com/
+[http://sequelpro.com/](http://sequelpro.com/)
+
+## License
Any new code added during it's development is licensed under the MIT license
and is copyrighted by the respective developer and the Sequel Pro team.
@@ -28,7 +29,7 @@ libpq is licensed under The PostgreSQL License and is copyrighted by:
Full license:
- http://www.postgresql.org/about/licence/
+[https://www.postgresql.org/about/licence/](https://www.postgresql.org/about/licence/)
libpqtypes is licensed under the BSD license and is copyrighted by:
@@ -36,14 +37,13 @@ libpqtypes is licensed under the BSD license and is copyrighted by:
Full License:
- http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE
+[http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE](http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE)
The entire framework is dual licensed under both version 2 of the Apache
license and the MIT license. Use of it must carry both of the following
licenses to indicate this:
-
-APACHE 2 LICENSE
+### Apache 2 License
Copyright (c) 2008-2009 David Thorpe, djt@mutablelogic.com
@@ -51,7 +51,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@@ -59,11 +59,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+### MIT License
-MIT LICENSE
-
-Copyright (c) 2012 Sequel Pro Team. All rights reserved.
-
+Copyright (c) 2017 Sequel Pro Team. All rights reserved.
+
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
@@ -72,10 +71,10 @@ copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
-
+
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
-
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
diff --git a/Frameworks/SPMySQLFramework/LICENSE b/Frameworks/SPMySQLFramework/LICENSE
new file mode 100644
index 00000000..fe855559
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2017 Rowan Beentje (rowan.beent.je) and the Sequel Pro team.
+
+All rights reserved.
+
+http://sequelpro.com/
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Frameworks/SPMySQLFramework/README.md b/Frameworks/SPMySQLFramework/README.md
new file mode 100644
index 00000000..f65141d0
--- /dev/null
+++ b/Frameworks/SPMySQLFramework/README.md
@@ -0,0 +1,43 @@
+# SPMySQL.framework
+
+The SPMySQL Framework is intended to provide a stable MySQL connection framework, with the ability to run text-based queries and rapidly retrieve result sets with conversion from MySQL data types to Cocoa objects.
+
+SPMySQL.framework has an interface loosely based around that provided by MCPKit by Serge Cohen and Bertrand Mansion ([http://mysql-cocoa.sourceforge.net/](http://mysql-cocoa.sourceforge.net/)), and in particular the heavily modified Sequel Pro version ([http://www.sequelpro.com/](http://www.sequelpro.com/)). It is a full rewrite of the original framework, although it includes code from patches implementing the following Sequel Pro functionality, largely contributed by Hans-Jörg Bibiko, Stuart Connolly, Jakob Egger and Rowan Beentje:
+
+* Connection locking (Jakob et al.)
+* Ping & keepalive (Rowan et al.)
+* Query cancellation (Rowan et al.)
+* Delegate setup (Stuart et al.)
+* SSL support (Rowan et al.)
+* Connection checking (Rowan et al.)
+* Version state (Stuart et al.)
+* Maximum packet size control (Hans et al.)
+* Result multithreading and streaming (Rowan et al.)
+* Improved encoding support & switching (Rowan et al.)
+* Database structure; moved to inside the app (Hans et al.)
+* Query reattempts and error-handling approach (Rowan et al.)
+* Geometry result class (Hans et al.)
+* Connection proxy (Stuart et al.)
+
+## Integration
+
+SPMySQL.framework can be added to your project as a standard Cocoa framework, or the entire project
+can be added as a subproject in Xcode.
+
+To add as a subproject in Xcode:
+
+1. Add the SPMySQL framework's `.xcodeproj` to your current project
+2. Choose an existing target, Get Info, and under direct dependenies add a new dependency. Choose the SPMySQL.framework target from the sub-project.
+3. Expand the subproject to see its child target - SPMySQL.framework. Drag this to the "Link Binary With Libraries" build phase of any targets using the framework.
+4. If you don't have a Copy Frameworks phase, add one; drag the SPMySQL.framework child target to this phase.
+5. In your build settings, add a User Header Search Path; make it a recursive path to the SPMySQL project folder location (for example `${PROJECT_DIR}/Frameworks/SPMySQLFramework`). This should allow you to `#include "SPMySQL.h"` and have everything function.
+
+As a last resort jump onto IRC and join #sequel-pro on irc.freenode.net and any of the
+developers will be more than happy to help you out.
+
+## License
+
+Copyright (c) 2017 Rowan Beentje (rowan.beent.je) & the Sequel Pro team. All rights reserved.
+
+SPMySQLFramework is free and open source software, licensed under [MIT](https://opensource.org/licenses/MIT). See [LICENSE](https://github.com/sequelpro/sequelpro/blob/master/Frameworks/SPMySQLFramework/LICENSE) for full details.
+
diff --git a/Frameworks/SPMySQLFramework/Readme.txt b/Frameworks/SPMySQLFramework/Readme.txt
deleted file mode 100644
index 01e4c4b1..00000000
--- a/Frameworks/SPMySQLFramework/Readme.txt
+++ /dev/null
@@ -1,69 +0,0 @@
-The SPMySQL Framework is intended to provide a stable MySQL connection framework, with the ability
-to run text-based queries and rapidly retrieve result sets with conversion from MySQL data types
-to Cocoa objects.
-
-SPMySQL.framework has an interface loosely based around that provided by MCPKit by Serge Cohen and
-Bertrand Mansion (http://mysql-cocoa.sourceforge.net/), and in particular the heavily modified
-Sequel Pro version (http://www.sequelpro.com/). It is a full rewrite of the original framework,
-although it includes code from patches implementing the following Sequel Pro functionality, largely
-contributed by Hans-Jörg Bibiko, Stuart Connolly, Jakob Egger, and Rowan Beentje:
-
- - Connection locking (Jakob et al)
- - Ping & keepalive (Rowan et al)
- - Query cancellation (Rowan et al)
- - Delegate setup (Stuart et al)
- - SSL support (Rowan et al)
- - Connection checking (Rowan et al)
- - Version state (Stuart et al)
- - Maximum packet size control (Hans et al)
- - Result multithreading and streaming (Rowan et al)
- - Improved encoding support & switching (Rowan et al)
- - Database structure; moved to inside the app (Hans et al)
- - Query reattempts and error-handling approach (Rowan et al)
- - Geometry result class (Hans et al)
- - Connection proxy (Stuart et al)
-
-
-INTEGRATION
-
-SPMySQL.framework can be added to your project as a standard Cocoa framework, or the entire project
-can be added as a subproject in Xcode.
-
-To add as a subproject in Xcode:
-
- 1) Add the SPMySQL framework's .xcodeproj to your current project
- 2) Choose an existing target, Get Info, and under direct dependenies add a new dependency. Choose the SPMySQL.framework target from the sub-project
- 3) Expand the subproject to see its child target - SPMySQL.framework. Drag this to the "Link Binary With Libraries" build phase of any targets using the framework.
- 4) If you don't have a Copy Frameworks phase, add one; drag the SPMySQL.framework child target to this phase.
- 5) In your build settings, add a User Header Search Path; make it a recursive path to the SPMySQL project folder location (for example ${PROJECT_DIR}/Frameworks/SPMySQLFramework). This should allow you to #include "SPMySQL.h" and have everything function.
-
-As a last resort jump onto IRC and join #sequel-pro on irc.freenode.net and any of the
-developers will be more than happy to help you out.
-
-
-LICENSE
-
-Copyright (c) 2012 Rowan Beentje (rowan.beent.je) and the Sequel Pro team.
-
-The SPMySQL framework is offered under the MIT license:
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h
index 1e2a8c14..99daca77 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h
@@ -71,6 +71,7 @@
@interface SPMySQLConnection (Max_Packet_Size_Private_API)
+- (NSInteger)_queryMaxAllowedPacketWithSQL:(NSString *)query resultInColumn:(NSUInteger)colIdx;
- (void)_updateMaxQuerySize;
- (void)_updateMaxQuerySizeEditability;
- (BOOL)_attemptMaxQuerySizeIncreaseTo:(NSUInteger)targetSize;
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m
index dc453624..76d1dfe7 100644
--- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m
+++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m
@@ -93,38 +93,57 @@
@implementation SPMySQLConnection (Max_Packet_Size_Private_API)
/**
- * Update the max_allowed_packet size - the largest supported query size - from the server.
+ * Executes a passed query for max_allowed_packet and returns the resulting number in bytes
+ *
+ * @return -1 => if the query failed
+ * 0 => if the query did not fail, but also did not contain a (valid) result
+ * * => if the query succeeded and the value is a valid integer (NOTE: this may also include -1 and 0)
*/
-- (void)_updateMaxQuerySize
+- (NSInteger)_queryMaxAllowedPacketWithSQL:(NSString *)query resultInColumn:(NSUInteger)colIdx
{
-
- // Determine which query to run based on server version
- NSString *packetQueryString;
- if ([self serverMajorVersion] == 3) {
- packetQueryString = @"SHOW VARIABLES LIKE 'max_allowed_packet'";
- } else {
- packetQueryString = @"SELECT @@global.max_allowed_packet";
- }
-
// Make a standard query to the server to retrieve the information
- SPMySQLResult *result = [self queryString:packetQueryString];
- if(!result) { // query fails on sphinxql
- NSLog(@"Query for max_allowed_packet failed: %@ (%lu) (on %@)", [self lastErrorMessage], [self lastErrorID], [self serverVersionString]);
- return;
+ SPMySQLResult *result = [self queryString:query];
+ if(!result) {
+ NSLog(@"Query (%@) for max_allowed_packet failed: %@ (%lu) (on %@)", query, [self lastErrorMessage], [self lastErrorID], [self serverVersionString]);
+ return -1;
}
[result setReturnDataAsStrings:YES];
// Get the maximum size string
- NSString *maxQuerySizeString = nil;
- if ([self serverMajorVersion] == 3) {
- maxQuerySizeString = [[result getRowAsArray] objectAtIndex:1];
- } else {
- maxQuerySizeString = [[result getRowAsArray] objectAtIndex:0];
- }
+ NSString *maxQuerySizeString = [[result getRowAsArray] objectAtIndex:colIdx];
+
+ NSInteger _maxQuerySize = maxQuerySizeString ? [maxQuerySizeString integerValue] : 0;
- // If a valid size was returned, update the instance variable
- if (maxQuerySizeString) {
- maxQuerySize = (NSUInteger)[maxQuerySizeString integerValue];
+ if(_maxQuerySize == 0)
+ NSLog(@"Query (%@) for max_allowed_packet returned invalid value: %ld (raw value: %@) (on %@)", query, _maxQuerySize, maxQuerySizeString, [self serverVersionString]);
+
+ return _maxQuerySize;
+}
+
+/**
+ * Update the max_allowed_packet size - the largest supported query size - from the server.
+ */
+- (void)_updateMaxQuerySize
+{
+ struct {
+ NSString *sql;
+ NSUInteger col;
+ } queryVariants[] = {
+ { .sql = @"SELECT @@global.max_allowed_packet", .col = 0 }, //works on mysql 4+
+ { .sql = @"SHOW VARIABLES LIKE 'max_allowed_packet'", .col = 1 }, //works on mysql 3, sphinx
+ { .sql = nil, .col = 0 }, //terminator element
+ };
+
+ int i = 0;
+ while(queryVariants[i].sql) {
+ NSInteger _maxQuerySize = [self _queryMaxAllowedPacketWithSQL:queryVariants[i].sql resultInColumn:queryVariants[i].col];
+ //see #2653
+ if(_maxQuerySize >= 34) { // the max_allowed_packet query above has at least 34 bytes, so any value less than that would be nonsense
+ // If a valid size was returned, update the instance variable
+ maxQuerySize = (NSUInteger)_maxQuerySize;
+ return;
+ }
+ i++;
}
}
diff --git a/Resources/Plists/PreferenceDefaults.plist b/Resources/Plists/PreferenceDefaults.plist
index 1d55e65a..952b37c5 100644
--- a/Resources/Plists/PreferenceDefaults.plist
+++ b/Resources/Plists/PreferenceDefaults.plist
@@ -127,8 +127,6 @@
<false/>
<key>FilterTableDefaultOperator</key>
<string>LIKE &apos;%@%&apos;</string>
- <key>GlobalResultTableFont</key>
- <data>BAtzdHJlYW10eXBlZIHoA4QBQISEhAZOU0ZvbnQehIQITlNPYmplY3QAhYQBaSSEBVszNmNdBgAAABoAAAD//kwAdQBjAGkAZABhAEcAcgBhAG4AZABlAAAAhAFmC4QBYwCYAZgAmACG</data>
<key>GrowlEnabled</key>
<true/>
<key>KeepAliveInterval</key>
diff --git a/Source/SPAppController.m b/Source/SPAppController.m
index 458c62f9..24b981af 100644
--- a/Source/SPAppController.m
+++ b/Source/SPAppController.m
@@ -104,8 +104,16 @@
*/
+ (void)initialize
{
+ NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
+ NSMutableDictionary *preferenceDefaults = [NSMutableDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:SPPreferenceDefaultsFile ofType:@"plist"]];
+
+ if (![prefs objectForKey:SPGlobalResultTableFont]) {
+ [preferenceDefaults setObject:[NSArchiver archivedDataWithRootObject:[NSFont systemFontOfSize:11]] forKey:SPGlobalResultTableFont];
+ }
+
// Register application defaults
- [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PreferenceDefaults" ofType:@"plist"]]];
+ [prefs registerDefaults:preferenceDefaults];
// Upgrade prefs before any other parts of the app pick up on the values
SPApplyRevisionChanges();
diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m
index 4a509796..a2159b4f 100644
--- a/Source/SPConnectionController.m
+++ b/Source/SPConnectionController.m
@@ -71,6 +71,14 @@ static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist";
@end
#endif
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_11
+@interface NSOpenPanel (NSOpenPanel_ElCaptian)
+
+@property (getter=isAccessoryViewDisclosed) BOOL accessoryViewDisclosed;
+
+@end
+#endif
+
/**
* This is a utility function to validate SSL key/certificate files
* @param fileData The contents of the file
@@ -468,6 +476,10 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2,
keySelectionPanel = [[NSOpenPanel openPanel] retain]; // retain/release needed on OS X ≤ 10.6 according to Apple doc
[keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]];
[keySelectionPanel setAccessoryView:accessoryView];
+ //on os x 10.11+ the accessory view will be hidden by default and has to be made visible
+ if(accessoryView && [keySelectionPanel respondsToSelector:@selector(setAccessoryViewDisclosed:)]) {
+ [keySelectionPanel setAccessoryViewDisclosed:YES];
+ }
[keySelectionPanel setDelegate:self];
[keySelectionPanel beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:^(NSInteger returnCode)
{
diff --git a/Source/SPConstants.h b/Source/SPConstants.h
index 26fdc1ab..161b5b0b 100644
--- a/Source/SPConstants.h
+++ b/Source/SPConstants.h
@@ -274,6 +274,7 @@ extern NSString *SPFavoritesDataFile;
extern NSString *SPHTMLPrintTemplate;
extern NSString *SPHTMLTableInfoPrintTemplate;
extern NSString *SPHTMLHelpTemplate;
+extern NSString *SPPreferenceDefaultsFile;
// SPF file types
extern NSString *SPFExportSettingsContentType;
@@ -674,6 +675,9 @@ void _SPClear(id *addr);
#ifndef __MAC_10_10
#define __MAC_10_10 101000
#endif
+#ifndef __MAC_10_11
+#define __MAC_10_11 101100
+#endif
// This enum is available since 10.5 but only got a "name" in 10.10
#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10
diff --git a/Source/SPConstants.m b/Source/SPConstants.m
index 16f59f7a..7ae37df3 100644
--- a/Source/SPConstants.m
+++ b/Source/SPConstants.m
@@ -67,6 +67,7 @@ NSString *SPFavoritesDataFile = @"Favorites.plist";
NSString *SPHTMLPrintTemplate = @"SPPrintTemplate";
NSString *SPHTMLTableInfoPrintTemplate = @"SPTableInfoPrintTemplate";
NSString *SPHTMLHelpTemplate = @"SPMySQLHelpTemplate";
+NSString *SPPreferenceDefaultsFile = @"PreferenceDefaults";
// Folder names
NSString *SPThemesSupportFolder = @"Themes";
diff --git a/Source/SPFieldEditorController.h b/Source/SPFieldEditorController.h
index ec36c9d2..0c0d0a0e 100644
--- a/Source/SPFieldEditorController.h
+++ b/Source/SPFieldEditorController.h
@@ -188,6 +188,7 @@
NSInteger editSheetReturnCode;
BOOL _isGeometry;
+ BOOL _isJSON;
NSUndoManager *esUndoManager;
NSDictionary *editedFieldInfo;
diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m
index 8c5c72ae..0298973d 100644
--- a/Source/SPFieldEditorController.m
+++ b/Source/SPFieldEditorController.m
@@ -37,6 +37,7 @@
#include <objc/objc-runtime.h>
#import "SPCustomQuery.h"
#import "SPTableContent.h"
+#import "SPJSONFormatter.h"
#import <SPMySQL/SPMySQL.h>
@@ -195,6 +196,7 @@ typedef enum {
}
#endif
+ [self setEditedFieldInfo:nil];
if ( sheetEditData ) SPClear(sheetEditData);
#ifndef SP_CODA
if ( qlTypes ) SPClear(qlTypes);
@@ -237,6 +239,7 @@ typedef enum {
callerInstance = sender;
_isGeometry = ([[fieldType uppercaseString] isEqualToString:@"GEOMETRY"]) ? YES : NO;
+ _isJSON = ([[fieldType uppercaseString] isEqualToString:SPMySQLJsonType]);
// Set field label
NSMutableString *label = [NSMutableString string];
@@ -249,7 +252,8 @@ typedef enum {
if ([fieldType length])
[label appendString:fieldType];
- if (maxTextLength > 0)
+ //skip length for JSON type since it's a constant and MySQL doesn't display it either
+ if (maxTextLength > 0 && !_isJSON)
[label appendFormat:@"(%lld) ", maxTextLength];
if (!_allowNULL)
@@ -352,7 +356,8 @@ typedef enum {
encoding = anEncoding;
- _isBlob = isFieldBlob;
+ // we don't want the hex/image controls for JSON
+ _isBlob = (!_isJSON && isFieldBlob);
BOOL isBinary = ([[fieldType uppercaseString] isEqualToString:@"BINARY"] || [[fieldType uppercaseString] isEqualToString:@"VARBINARY"]);
@@ -442,7 +447,18 @@ typedef enum {
[editTextScrollView setHidden:NO];
}
else {
- stringValue = [sheetEditData retain];
+ // If the input is a JSON type column we can format it.
+ // Since MySQL internally stores JSON in binary, it does not retain any formatting
+ do {
+ if(_isJSON) {
+ NSString *formatted = [SPJSONFormatter stringByFormattingString:sheetEditData];
+ if(formatted) {
+ stringValue = [formatted retain];
+ break;
+ }
+ }
+ stringValue = [sheetEditData retain];
+ } while(0);
[hexTextView setString:@""];
@@ -651,6 +667,12 @@ typedef enum {
if(callerInstance) {
id returnData = ( editSheetReturnCode && _isEditable ) ? (_isGeometry) ? [editTextView string] : sheetEditData : nil;
+ //for MySQLs JSON type remove the formatting again, since it won't be stored anyway
+ if(_isJSON) {
+ NSString *unformatted = [SPJSONFormatter stringByUnformattingString:returnData];
+ if(unformatted) returnData = unformatted;
+ }
+
#ifdef SP_CODA /* patch */
if ( [callerInstance isKindOfClass:[SPCustomQuery class]] )
[(SPCustomQuery*)callerInstance processFieldEditorResult:returnData contextInfo:contextInfo];
@@ -1358,7 +1380,7 @@ typedef enum {
if([notification object] == editTextView) {
// Do nothing if user really didn't changed text (e.g. for font size changing return)
if(!editTextViewWasChanged && (editSheetWillBeInitialized
- || (([[[notification object] textStorage] editedRange].length == 0)
+ || (([[[notification object] textStorage] editedRange].location == NSNotFound)
&& ([[[notification object] textStorage] changeInLength] == 0)))) {
// Inform the undo-grouping about the caret movement
selectionChanged = YES;
diff --git a/Source/SPJSONFormatter.h b/Source/SPJSONFormatter.h
new file mode 100644
index 00000000..fd0b7afd
--- /dev/null
+++ b/Source/SPJSONFormatter.h
@@ -0,0 +1,123 @@
+//
+// SPJSONFormatter.h
+// sequel-pro
+//
+// Created by Max Lohrmann on 10.02.17.
+// Copyright (c) 2017 Max Lohrmann. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <https://github.com/sequelpro/sequelpro>
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(UInt8, SPJSONToken) {
+ JSON_TOK_EOF,
+ JSON_TOK_CURLY_BRACE_OPEN,
+ JSON_TOK_CURLY_BRACE_CLOSE,
+ JSON_TOK_SQUARE_BRACE_OPEN,
+ JSON_TOK_SQUARE_BRACE_CLOSE,
+ JSON_TOK_DOUBLE_QUOTE,
+ JSON_TOK_COLON,
+ JSON_TOK_COMMA,
+ JSON_TOK_OTHER,
+ JSON_TOK_STRINGDATA
+};
+
+typedef NS_ENUM(UInt8, SPJSONContext) {
+ JSON_ROOT_CONTEXT,
+ JSON_STRING_CONTEXT
+};
+
+typedef struct {
+ const char *str;
+ size_t len;
+ size_t pos;
+ SPJSONContext ctxt;
+} SPJSONTokenizerState;
+
+typedef struct {
+ SPJSONToken tok;
+ size_t pos;
+ size_t len;
+} SPJSONTokenInfo;
+
+/**
+ * Initializes a caller defined SPJSONTokenizerState structure to the string that is passed.
+ * The string is not retained. The caller is responsible for making sure it stays around as long
+ * as the tokenizer is used!
+ *
+ * @return 0 on success, -1 if an argument was NULL.
+ */
+int SPJSONTokenizerInit(NSString *input, SPJSONTokenizerState *stateInfo);
+
+/**
+ * This function returns the token that is at the current position of the input string or following
+ * it most closely and forward the input string accordingly.
+ *
+ * The JSON_TOK_EOF token is a zero length token that is returned after the last character in the input
+ * string has been read and tokenized. Any call to this function after JSON_TOK_EOF has been returned
+ * will return the same.
+ *
+ * JSON_TOK_OTHER and JSON_TOK_STRINGDATA are variable length tokens (but never 0) that represent whitespace,
+ * numbers, true/false/null and the contents of strings (without the double quotes).
+ *
+ * The remaining tokens correspond to the respective control characters in JSON and are always a single
+ * character long.
+ *
+ * The token/position/length information will be assigned to the tokenMatch argument given by the caller.
+ *
+ * @return 1 If a token was successfully matched
+ * 0 If the matched token was JSON_TOK_EOF (tokenMatch will still be set, like for 1)
+ * -1 If the passed arguments were invalid (tokenMatch will not be updated)
+ *
+ * DO NOT try to build a parser/syntax validator based on this code! It is much too lenient for those purposes!
+ */
+int SPJSONTokenizerGetNextToken(SPJSONTokenizerState *stateInfo, SPJSONTokenInfo *tokenMatch);
+
+
+@interface SPJSONFormatter : NSObject
+
+/**
+ * This method will return a formatted copy of the input string.
+ *
+ * - A line break is inserted after every ",".
+ * - There will be a line break after every "{" and "[" (except if they are empty) and the indent
+ * of the following lines is increased by 1.
+ * - There will be a line break before "]" and "}" (except if they are empty) and the indent of this line
+ * and the following lines will be decreased by 1.
+ * - A line break will be inserted after "]" and "}", except if a "," follows.
+ * - Indenting is done using a single "\t" character per level.
+ *
+ * @return The formatted string or nil if formatting failed.
+ */
++ (NSString *)stringByFormattingString:(NSString *)input;
+
+/**
+ * This method will return a compact copy of the input string.
+ * All whitespace (outside of strings) will be removed (except for a single space after ":" and ",")
+ *
+ * @return The unformatted string or nil if unformatting failed.
+ */
++ (NSString *)stringByUnformattingString:(NSString *)input;
+
+@end
diff --git a/Source/SPJSONFormatter.m b/Source/SPJSONFormatter.m
new file mode 100644
index 00000000..258845c9
--- /dev/null
+++ b/Source/SPJSONFormatter.m
@@ -0,0 +1,364 @@
+//
+// SPJSONFormatter.m
+// sequel-pro
+//
+// Created by Max Lohrmann on 10.02.17.
+// Copyright (c) 2017 Max Lohrmann. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <https://github.com/sequelpro/sequelpro>
+
+#import "SPJSONFormatter.h"
+
+
+static char GetNextANSIChar(SPJSONTokenizerState *stateInfo);
+
+
+@implementation SPJSONFormatter
+
++ (NSString *)stringByFormattingString:(NSString *)input
+{
+ SPJSONTokenizerState stateInfo;
+ if(SPJSONTokenizerInit(input,&stateInfo) == -1) return nil;
+
+ NSUInteger idLevel = 0;
+
+ NSCharacterSet *wsNlCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet];
+ NSMutableString *formatted = [[NSMutableString alloc] init];
+
+ SPJSONToken prevTokenType = JSON_TOK_EOF;
+ SPJSONTokenInfo curToken;
+ if(SPJSONTokenizerGetNextToken(&stateInfo,&curToken) == -1) {
+ [formatted release];
+ return nil;
+ }
+
+ BOOL needIndent = NO;
+ SPJSONTokenInfo nextToken;
+ do {
+ //we need to know the next token to do meaningful formatting
+ if(SPJSONTokenizerGetNextToken(&stateInfo,&nextToken) == -1) {
+ [formatted release];
+ return nil;
+ }
+
+ if(idLevel > 0 && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE))
+ idLevel--;
+
+ //if this token is a "]" or "}" and there was no ",", "[" or "{" directly before it, add a linebreak before
+ if(prevTokenType != JSON_TOK_CURLY_BRACE_OPEN && prevTokenType != JSON_TOK_SQUARE_BRACE_OPEN && prevTokenType != JSON_TOK_COMMA && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE)) {
+ [formatted appendString:@"\n"];
+ needIndent = YES;
+ }
+
+ //if this token is on a new line indent it
+ if(needIndent && idLevel > 0) {
+ //32 tabs pool (with fallback for even deeper nesting)
+ static NSString *tabs = @"\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+ NSUInteger myIdLevel = idLevel;
+ while(myIdLevel > [tabs length]) {
+ [formatted appendString:tabs];
+ myIdLevel -= [tabs length];
+ }
+ [formatted appendString:[tabs substringWithRange:NSMakeRange(0, myIdLevel)]];
+ needIndent = NO;
+ }
+
+ //save ourselves the overhead of creating an NSString if we already know what it will contain
+ NSString *curTokenString;
+ id freeMe = nil;
+ switch (curToken.tok) {
+ case JSON_TOK_CURLY_BRACE_OPEN:
+ curTokenString = @"{";
+ break;
+
+ case JSON_TOK_CURLY_BRACE_CLOSE:
+ curTokenString = @"}";
+ break;
+
+ case JSON_TOK_SQUARE_BRACE_OPEN:
+ curTokenString = @"[";
+ break;
+
+ case JSON_TOK_SQUARE_BRACE_CLOSE:
+ curTokenString = @"]";
+ break;
+
+ case JSON_TOK_DOUBLE_QUOTE:
+ curTokenString = @"\"";
+ break;
+
+ case JSON_TOK_COLON:
+ curTokenString = @": "; //add a space after ":" for readability
+ break;
+
+ case JSON_TOK_COMMA:
+ curTokenString = @",";
+ break;
+
+ //JSON_TOK_OTHER
+ //JSON_TOK_STRINGDATA
+ default:
+ curTokenString = [[NSString alloc] initWithBytesNoCopy:(void *)(&stateInfo.str[curToken.pos]) length:curToken.len encoding:NSUTF8StringEncoding freeWhenDone:NO];
+ //for everything except strings get rid of surrounding whitespace
+ if(curToken.tok != JSON_TOK_STRINGDATA) {
+ NSString *newTokenString = [[curTokenString stringByTrimmingCharactersInSet:wsNlCharset] retain];
+ [curTokenString release];
+ curTokenString = newTokenString;
+ }
+ freeMe = curTokenString;
+ }
+
+ [formatted appendString:curTokenString];
+
+ if(freeMe) [freeMe release];
+
+ //if the current token is a "[", "{" or "," and the next token is not a "]" or "}" add a line break afterwards
+ if(
+ curToken.tok == JSON_TOK_COMMA ||
+ (curToken.tok == JSON_TOK_CURLY_BRACE_OPEN && nextToken.tok != JSON_TOK_CURLY_BRACE_CLOSE) ||
+ (curToken.tok == JSON_TOK_SQUARE_BRACE_OPEN && nextToken.tok != JSON_TOK_SQUARE_BRACE_CLOSE)
+ ) {
+ [formatted appendString:@"\n"];
+ needIndent = YES;
+ }
+
+ if(curToken.tok == JSON_TOK_CURLY_BRACE_OPEN || curToken.tok == JSON_TOK_SQUARE_BRACE_OPEN)
+ idLevel++;
+
+ prevTokenType = curToken.tok;
+ curToken = nextToken;
+ } while(curToken.tok != JSON_TOK_EOF); //SPJSONTokenizerGetNextToken() will always return JSON_TOK_EOF once it has reached that state
+
+ return [formatted autorelease];
+}
+
++ (NSString *)stringByUnformattingString:(NSString *)input
+{
+ SPJSONTokenizerState stateInfo;
+ if(SPJSONTokenizerInit(input,&stateInfo) == -1) return nil;
+
+ NSCharacterSet *wsNlCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet];
+ NSMutableString *unformatted = [[NSMutableString alloc] init];
+
+ do {
+ SPJSONTokenInfo curToken;
+ if(SPJSONTokenizerGetNextToken(&stateInfo,&curToken) == -1) {
+ [unformatted release];
+ return nil;
+ }
+
+ if(curToken.tok == JSON_TOK_EOF) break;
+
+ //save ourselves the overhead of creating an NSString from input if we already know what it will contain
+ NSString *curTokenString;
+ id freeMe = nil;
+ switch (curToken.tok) {
+ case JSON_TOK_CURLY_BRACE_OPEN:
+ curTokenString = @"{";
+ break;
+
+ case JSON_TOK_CURLY_BRACE_CLOSE:
+ curTokenString = @"}";
+ break;
+
+ case JSON_TOK_SQUARE_BRACE_OPEN:
+ curTokenString = @"[";
+ break;
+
+ case JSON_TOK_SQUARE_BRACE_CLOSE:
+ curTokenString = @"]";
+ break;
+
+ case JSON_TOK_DOUBLE_QUOTE:
+ curTokenString = @"\"";
+ break;
+
+ case JSON_TOK_COLON:
+ curTokenString = @": "; //add a space after ":" to match MySQL
+ break;
+
+ case JSON_TOK_COMMA:
+ curTokenString = @", "; //add a space after "," to match MySQL
+ break;
+
+ //JSON_TOK_OTHER
+ //JSON_TOK_STRINGDATA
+ default:
+ curTokenString = [[NSString alloc] initWithBytesNoCopy:(void *)(&stateInfo.str[curToken.pos]) length:curToken.len encoding:NSUTF8StringEncoding freeWhenDone:NO];
+ //for everything except strings get rid of surrounding whitespace
+ if(curToken.tok != JSON_TOK_STRINGDATA) {
+ NSString *newTokenString = [[curTokenString stringByTrimmingCharactersInSet:wsNlCharset] retain];
+ [curTokenString release];
+ curTokenString = newTokenString;
+ }
+ freeMe = curTokenString;
+ }
+
+ [unformatted appendString:curTokenString];
+
+ if(freeMe) [freeMe release];
+
+ } while(1);
+
+ return [unformatted autorelease];
+}
+
+
+@end
+
+/**
+ * This function returns the char at the current position in the input string and forwards the read pointer to the next char.
+ * If the character is part of an UTF8 multibyte sequence, the function will skip forward until a single byte character is found again
+ * or EOF is reached (whichever comes first).
+ *
+ * stateInfo MUST be valid or this will crash!
+ *
+ * @return Either a char in the range 0-127 or -1 if EOF is reached.
+ */
+char GetNextANSIChar(SPJSONTokenizerState *stateInfo) {
+ do {
+ if(stateInfo->pos >= stateInfo->len)
+ return -1;
+ char val = stateInfo->str[stateInfo->pos++];
+ // all utf8 multibyte characters start with the most significant bit being 1 for all of their bytes
+ // but since all JSON control characters are in the single byte ANSI compatible plane, we can just ignore any MB chars
+ if((val & 0x80) == 0)
+ return val;
+ } while(1);
+}
+
+int SPJSONTokenizerInit(NSString *input, SPJSONTokenizerState *stateInfo) {
+ if(!input || ![input respondsToSelector:@selector(UTF8String)] || stateInfo == NULL)
+ return -1;
+
+ stateInfo->ctxt = JSON_ROOT_CONTEXT;
+ stateInfo->pos = 0;
+ stateInfo->str = [input UTF8String];
+ stateInfo->len = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+
+ return 0;
+}
+
+int SPJSONTokenizerGetNextToken(SPJSONTokenizerState *stateInfo, SPJSONTokenInfo *tokenMatch) {
+ if(tokenMatch == NULL || stateInfo == NULL || stateInfo->str == NULL)
+ return -1;
+
+ size_t posBefore = stateInfo->pos;
+ do {
+ char c = GetNextANSIChar(stateInfo);
+ if(stateInfo->ctxt == JSON_STRING_CONTEXT) {
+ //the only characters inside a string that are relevant to us are backslash and doublequote
+ if(c == '"' || c == -1) {
+ //if the string has contents, return that first
+ if((stateInfo->pos - posBefore) > 1) {
+ tokenMatch->tok = JSON_TOK_STRINGDATA;
+ tokenMatch->pos = posBefore;
+ if(c == '"')
+ stateInfo->pos--; //rewind to read it again
+ tokenMatch->len = stateInfo->pos - posBefore;
+ return 1;
+ }
+ //string is terminated by EOF (invalid JSON)
+ if(c == -1) {
+ //switch to root context and try again to reach EOF branch below
+ stateInfo->ctxt = JSON_ROOT_CONTEXT;
+ continue;
+ }
+ stateInfo->ctxt = JSON_ROOT_CONTEXT;
+ tokenMatch->tok = JSON_TOK_DOUBLE_QUOTE;
+ tokenMatch->pos = posBefore;
+ tokenMatch->len = stateInfo->pos - posBefore;
+ return 1;
+ }
+ else if(c == '\\') {
+ //for backslash we need to skip the next byte
+ // We don't care for the value of the next byte since we don't really want to parse JSON, but only format it.
+ // Thus we only have to pay attention to differntiate backslash-dquote and dquote.
+ stateInfo->pos++;
+ }
+ }
+ else if(c == -1) {
+ //if there is still unreturned input, return that first
+ if(posBefore < stateInfo->len) {
+ tokenMatch->tok = JSON_TOK_OTHER;
+ tokenMatch->pos = posBefore;
+ tokenMatch->len = stateInfo->pos - posBefore;
+ return 1;
+ }
+ tokenMatch->tok = JSON_TOK_EOF;
+ tokenMatch->pos = stateInfo->pos; //EOF sits after the last character
+ tokenMatch->len = 0; // EOF has no length
+ return 0;
+ }
+ else {
+ SPJSONToken tokFound = JSON_TOK_EOF;
+
+ switch(c) {
+ case '"':
+ stateInfo->ctxt = JSON_STRING_CONTEXT;
+ tokFound = JSON_TOK_DOUBLE_QUOTE;
+ break;
+
+ case '{':
+ tokFound = JSON_TOK_CURLY_BRACE_OPEN;
+ break;
+
+ case '}':
+ tokFound = JSON_TOK_CURLY_BRACE_CLOSE;
+ break;
+
+ case '[':
+ tokFound = JSON_TOK_SQUARE_BRACE_OPEN;
+ break;
+
+ case ']':
+ tokFound = JSON_TOK_SQUARE_BRACE_CLOSE;
+ break;
+
+ case ':':
+ tokFound = JSON_TOK_COLON;
+ break;
+
+ case ',':
+ tokFound = JSON_TOK_COMMA;
+ break;
+ }
+
+ //if we found a token, but had to walk more than 1 char there was something else
+ //between the previous token and this token, which we should report first
+ if(tokFound != JSON_TOK_EOF && (stateInfo->pos - posBefore) > 1) {
+ stateInfo->ctxt = JSON_ROOT_CONTEXT;
+ stateInfo->pos--; //rewind so we will read the token again next time
+ tokFound = JSON_TOK_OTHER;
+ }
+
+ if(tokFound != JSON_TOK_EOF) {
+ tokenMatch->tok = tokFound;
+ tokenMatch->pos = posBefore;
+ tokenMatch->len = stateInfo->pos - posBefore;
+ return 1;
+ }
+ }
+ } while(1);
+}
diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m
index 8d53d0b3..cb085e39 100644
--- a/Source/SPSQLExporter.m
+++ b/Source/SPSQLExporter.m
@@ -90,16 +90,13 @@
[sqlTableDataInstance setConnection:connection];
SPMySQLResult *queryResult;
- SPMySQLStreamingResult *streamingResult;
- NSArray *row;
NSString *tableName;
NSDictionary *tableDetails;
- BOOL *useRawDataForColumnAtIndex, *useRawHexDataForColumnAtIndex;
SPTableType tableType = SPTableTypeTable;
id createTableSyntax = nil;
- NSUInteger j, k, t, s, rowCount, queryLength, lastProgressValue, cleanAutoReleasePool = NO;
+ NSUInteger j, s;
BOOL sqlOutputIncludeStructure;
BOOL sqlOutputIncludeContent;
@@ -232,7 +229,7 @@
// Inform the delegate that we are about to start fetcihing data for the current table
[delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginFetchingData:) withObject:self waitUntilDone:NO];
- lastProgressValue = 0;
+ NSUInteger lastProgressValue = 0;
// Add the name of table
[self writeString:[NSString stringWithFormat:@"# %@ %@\n# ------------------------------------------------------------\n\n", NSLocalizedString(@"Dump of table", @"sql export dump of table label"), tableName]];
@@ -297,8 +294,8 @@
NSMutableArray *rawColumnNames = [NSMutableArray arrayWithCapacity:colCount];
NSMutableArray *queryColumnDetails = [NSMutableArray arrayWithCapacity:colCount];
- useRawDataForColumnAtIndex = calloc(colCount, sizeof(BOOL));
- useRawHexDataForColumnAtIndex = calloc(colCount, sizeof(BOOL));
+ BOOL *useRawDataForColumnAtIndex = calloc(colCount, sizeof(BOOL));
+ BOOL *useRawHexDataForColumnAtIndex = calloc(colCount, sizeof(BOOL));
// Determine whether raw data can be used for each column during processing - safe numbers and hex-encoded data.
for (j = 0; j < colCount; j++)
@@ -347,17 +344,17 @@
continue;
}
- rowCount = [NSArrayObjectAtIndex(rowArray, 0) integerValue];
+ NSUInteger rowCount = [NSArrayObjectAtIndex(rowArray, 0) integerValue];
if (rowCount) {
// Set up a result set in streaming mode
- streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT %@ FROM %@", [queryColumnDetails componentsJoinedByString:@", "], [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain];
+ SPMySQLStreamingResult *streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT %@ FROM %@", [queryColumnDetails componentsJoinedByString:@", "], [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain];
// Inform the delegate that we are about to start writing data for the current table
[delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO];
- queryLength = 0;
+ NSUInteger queryLength = 0;
// Lock the table for writing and disable keys if supported
[metaString setString:@""];
@@ -369,14 +366,17 @@
[self writeUTF8String:[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES", [tableName backtickQuotedString], [rawColumnNames componentsJoinedAndBacktickQuoted]]];
// Iterate through the rows to construct a VALUES group for each
- j = 0, k = 0;
+ NSUInteger rowsWrittenForTable = 0;
+ NSUInteger rowsWrittenForCurrentStmt = 0;
+ BOOL cleanAutoReleasePool = NO;
NSAutoreleasePool *sqlExportPool = [[NSAutoreleasePool alloc] init];
// Inform the delegate that we are about to start writing the data to disk
[delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO];
- while ((row = [streamingResult getRowAsArray]))
+ NSArray *row;
+ while ((row = [streamingResult getRowAsArray]))
{
// Check for cancellation flag
if ([self isCancelled]) {
@@ -392,11 +392,8 @@
return;
}
- j++;
- k++;
-
// Update the progress
- NSUInteger progress = (NSUInteger)(j * ([self exportMaxProgress] / rowCount));
+ NSUInteger progress = (NSUInteger)((rowsWrittenForTable + 1) * ([self exportMaxProgress] / rowCount));
if (progress > lastProgressValue) {
[self setExportProgressValue:progress];
@@ -410,7 +407,7 @@
// Set up the new row as appropriate. If a new INSERT statement should be created,
// set one up; otherwise, set up a new row
if ((([self sqlInsertDivider] == SPSQLInsertEveryNDataBytes) && (queryLength >= ([self sqlInsertAfterNValue] * 1024))) ||
- (([self sqlInsertDivider] == SPSQLInsertEveryNRows) && (k == [self sqlInsertAfterNValue])))
+ (([self sqlInsertDivider] == SPSQLInsertEveryNRows) && (rowsWrittenForCurrentStmt == [self sqlInsertAfterNValue])))
{
[sqlString setString:@";\n\nINSERT INTO "];
[sqlString appendString:[tableName backtickQuotedString]];
@@ -418,19 +415,19 @@
[sqlString appendString:[rawColumnNames componentsJoinedAndBacktickQuoted]];
[sqlString appendString:@")\nVALUES\n\t("];
- queryLength = 0, k = 0;
+ queryLength = 0, rowsWrittenForCurrentStmt = 0;
// Use the opportunity to drain and reset the autorelease pool at the end of this row
cleanAutoReleasePool = YES;
}
- else if (j == 1) {
+ else if (rowsWrittenForTable == 0) {
[sqlString setString:@"\n\t("];
}
else {
[sqlString setString:@",\n\t("];
}
- for (t = 0; t < colCount; t++)
+ for (NSUInteger t = 0; t < colCount; t++)
{
id object = NSArrayObjectAtIndex(row, t);
@@ -506,6 +503,9 @@
sqlExportPool = [[NSAutoreleasePool alloc] init];
cleanAutoReleasePool = NO;
}
+
+ rowsWrittenForTable++;
+ rowsWrittenForCurrentStmt++;
}
// Complete the command
diff --git a/Source/SPSSHTunnel.m b/Source/SPSSHTunnel.m
index 390c516c..1306b16f 100644
--- a/Source/SPSSHTunnel.m
+++ b/Source/SPSSHTunnel.m
@@ -383,6 +383,7 @@ static unsigned short getRandomPort();
} else {
TA(@"-L", ([NSString stringWithFormat:@"%ld:%@:%ld", (long)localPort, remoteHost, (long)remotePort]));
}
+#undef TA
[task setArguments:taskArguments];
@@ -414,10 +415,54 @@ static unsigned short getRandomPort();
[task setStandardError:standardError];
[[ NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(standardErrorHandler:)
- name:@"NSFileHandleDataAvailableNotification"
+ name:NSFileHandleDataAvailableNotification
object:[standardError fileHandleForReading]];
[[standardError fileHandleForReading] waitForDataInBackgroundAndNotify];
+ {
+ static BOOL hasCheckedTTY = NO;
+ if(!hasCheckedTTY) {
+ int fd = open("/dev/tty", O_RDWR);
+ if(fd >= 0) {
+ close(fd);
+ fprintf(stderr, (
+ "!!!\n"
+ "!!! You are running Sequel Pro from a TTY.\n"
+ "!!! Any SSH connections that require user input (e.g. a password/passphrase) will fail\n"
+ "!!! and appear stalled indefinitely.\n"
+ "!!! Sorry!\n"
+ "!!!\n"
+ ));
+ fflush(stderr);
+ // Explanation:
+ // OpenSSH by default requests passwords AND yes/no questions directly from the TTY,
+ // if it is part of a session group that has a controlling terminal (which is the case for
+ // processes created by Terminal.app).
+ //
+ // But this won't work, because only the foreground process group can read from /dev/tty and
+ // NSTask will create a new (background) process group for OpenSSH on launch.
+ // Side note: The internal method called from -[NSTask launch]
+ // -[NSConcreteTask launchWithDictionary:] accepts key @"_NSTaskNoNewProcessGroup" to skip that.
+ //
+ // Now, there are two preconditions for OpenSSH to use our SSH_ASKPASS utility instead:
+ // 1) The "DISPLAY" envvar has to be set
+ // 2) There must be no controlling terminal (ie. open("/dev/tty") fails)
+ // (See readpass.c#read_passphrase() in OpenSSH for the relevant code)
+ //
+ // -[NSTask launch] internally uses posix_spawn() and according to its documentation
+ // "The new process also inherits the following attributes from the calling
+ // process: [...] control terminal [...]"
+ // So if we wanted to avoid that, we would have to reimplement the whole NSTask class
+ // and use fork()+exec*()+setsid() instead (or use GNUStep's NSTask which already does this).
+ //
+ // We could also do ioctl(fd, TIOCNOTTY, 0); before launching the child process, but
+ // changing our own controlling terminal does not seem like a good idea in the middle
+ // of the application lifecycle, when we don't know what other Cocoa code may use it...
+ }
+ hasCheckedTTY = YES;
+ }
+ }
+
@try {
// Launch and run the tunnel
[task launch]; //throws for invalid paths, missing +x permission
diff --git a/Source/SPTablesPreferencePane.m b/Source/SPTablesPreferencePane.m
index f89849ec..ff3296c2 100644
--- a/Source/SPTablesPreferencePane.m
+++ b/Source/SPTablesPreferencePane.m
@@ -54,10 +54,8 @@
* Updates the displayed font according to the user's preferences.
*/
- (void)updateDisplayedTableFontName
-{
- NSFont *font = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]];
-
- [globalResultTableFontName setFont:font];
+{
+ [globalResultTableFontName setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]];
}
#pragma mark -
diff --git a/UnitTests/SPJSONFormatterTests.m b/UnitTests/SPJSONFormatterTests.m
new file mode 100644
index 00000000..658169a4
--- /dev/null
+++ b/UnitTests/SPJSONFormatterTests.m
@@ -0,0 +1,141 @@
+//
+// SPJSONFormatterTests.m
+// sequel-pro
+//
+// Created by Max Lohrmann on 12.02.17.
+// Copyright (c) 2017 Max Lohrmann. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <https://github.com/sequelpro/sequelpro>
+
+#import "SPJSONFormatter.h"
+#import <Cocoa/Cocoa.h>
+#import <XCTest/XCTest.h>
+
+@interface SPJSONFormatterTests : XCTestCase
+
+- (void)testFormatting;
+- (void)testUnformatting;
+
+@end
+
+@implementation SPJSONFormatterTests
+
+- (void)testFormatting
+{
+
+ //invalid input
+ XCTAssertNil([SPJSONFormatter stringByFormattingString:nil],@"nil output on nil input");
+
+ //empty string
+ XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:@""], @"", @"empty string stays empty");
+
+ //scalars on their own should not get changed
+ {
+ NSArray *scalars = @[@"true",@"false",@"null",@"123.45",@"1.4e-5",@"\"string\""];
+ for (NSString *scalar in scalars) {
+ XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:scalar], scalar, @"scalar only input stays as is");
+ }
+ }
+
+ //simple test involving all types
+ {
+ NSString *unf = @"{\"key\": null, \"foo\": [true, false], \"ba\\\"r\": [{},{\"key2\": -1.98}]}";
+ NSString *fmt = @"{\n\t\"key\": null,\n\t\"foo\": [\n\t\ttrue,\n\t\tfalse\n\t],\n\t\"ba\\\"r\": [\n\t\t{},\n\t\t{\n\t\t\t\"key2\": -1.98\n\t\t}\n\t]\n}";
+
+ XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:unf], fmt, @"simple formatting test");
+ }
+
+ //other tests
+ {
+ NSArray *tests = @[
+ @[@"{\"key\": \"v\0al\"}",@"{\n\t\"key\": \"v\0al\"\n}", @"NUL in input (invalid JSON)"],
+ @[@"[\"\",\"\"\",\"", @"[\n\t\"\",\n\t\"\"\",\"", @"series of dquotes (invalid JSON)"],
+ @[@"{[{\"ab\\u0090c\",",@"{\n\t[\n\t\t{\n\t\t\t\"ab\\u0090c\",\n",@"unterminated elements (invalid JSON)"],
+ @[@"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[null]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]",@"[\n\t[\n\t\t[\n\t\t\t[\n\t\t\t\t[\n\t\t\t\t\t[\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnull\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t]\n\t\t]\n\t]\n]",@"34 levels of indent"],
+ @[@"{\"a\":\"bcd}",@"{\n\t\"a\": \"bcd}",@"unterminated string (invalid JSON)"],
+ @[@"[1,\"ab\ncd\",3]",@"[\n\t1,\n\t\"ab\ncd\",\n\t3\n]",@"multiline string (invalid JSON)"],
+ @[@"{}}},false]",@"{}\n}\n},\nfalse\n]",@"closing something that is not open (invalid JSON)"],
+ @[@"[[123e4}}",@"[\n\t[\n\t\t123e4\n\t}\n}",@"unmatched braces (invalid JSON)"],
+ @[@"[{]}",@"[\n\t{\n\t]\n}",@"unmatched braces 2 (invalid JSON)"],
+ @[@"[ true , \n false \t ] \t \n",@"[\n\ttrue,\n\tfalse\n]",@"whitespace reformatting"],
+ @[@"[1,2,]",@"[\n\t1,\n\t2,\n]",@"trailing comma (valid for some parsers)"],
+ @[@"{}/{|}],-\"}[|[{}\\\"]:{~]\",,}{]\{|::[\\|\"],};]*}]",@"{}/{\n\t|\n}\n],\n-\"}[|[{}\\\"]:{~]\",\n,\n}{\n]{\n\t|: : [\n\t\t\\|\"],};]*}]",@"random garbage"],
+ ];
+
+ for (NSArray *pair in tests) {
+ XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:[pair objectAtIndex:0]], [pair objectAtIndex:1], @"%@", [pair objectAtIndex:2]);
+ }
+ }
+}
+
+- (void)testUnformatting
+{
+ //invalid input
+ XCTAssertNil([SPJSONFormatter stringByUnformattingString:nil],@"nil output on nil input");
+
+ //empty string
+ XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:@""], @"", @"empty string stays empty");
+
+ //scalars on their own should not get changed
+ {
+ NSArray *scalars = @[@"true",@"false",@"null",@"123.45",@"1.4e-5",@"\"string\""];
+ for (NSString *scalar in scalars) {
+ XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:scalar], scalar, @"scalar only input stays as is");
+ }
+ }
+
+ //simple test involving all types
+ {
+ NSString *unf = @"{\"key\": null, \"foo\": [true, false], \"ba\\\"r\": [{}, {\"key2\": -1.98}]}";
+ NSString *fmt = @"{\n\t\"key\": null,\n\t\"foo\": [\n\t\ttrue,\n\t\tfalse\n\t],\n\t\"ba\\\"r\": [\n\t\t{},\n\t\t{\n\t\t\t\"key2\": -1.98\n\t\t}\n\t]\n}";
+
+ XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:fmt], unf, @"simple unformatting test");
+ }
+
+ //other tests
+ {
+ NSArray *tests = @[
+ @[@"{\n\t\"key\": \"v\0al\"\n}", @"{\"key\": \"v\0al\"}", @"NUL in input (invalid JSON)"],
+ @[@"[\n\t\"\",\n\t\"\"\",\"", @"[\"\", \"\"\",\"", @"series of dquotes (invalid JSON)"],
+ @[@"{\n\t[\n\t\t{\n\t\t\t\"ab\\u0090c\",\n",@"{[{\"ab\\u0090c\", ",@"unterminated elements (invalid JSON)"],
+ @[@"[\n\t[\n\t\t[\n\t\t\t[\n\t\t\t\t[\n\t\t\t\t\t[\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnull\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t]\n\t\t]\n\t]\n]", @"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[null]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]",@"34 levels of indent"],
+ @[@"{\n\t\"a\": \"bcd}", @"{\"a\": \"bcd}",@"unterminated string (invalid JSON)"],
+ @[@"[\n\t1,\n\t\"ab\ncd\",\n\t3\n]",@"[1, \"ab\ncd\", 3]",@"multiline string (invalid JSON)"],
+ @[@"{}\n}\n},\nfalse\n]", @"{}}}, false]",@"closing something that is not open (invalid JSON)"],
+ @[@"[\n\t[\n\t\t123e4\n\t}\n}", @"[[123e4}}",@"unmatched braces (invalid JSON)"],
+ @[@"[\n\t{\n\t]\n}", @"[{]}",@"unmatched braces 2 (invalid JSON)"],
+ @[@"[ true , \n false \t ] \t \n",@"[true, false]",@"whitespace reformatting"],
+ @[@"[\n\t1,\n\t2,\n]", @"[1, 2, ]",@"trailing comma (valid for some parsers)"],
+ @[@"{}/{\n\t|\n}\n],\n-\"}[|[{}\\\"]:{~]\",\n,\n}{\n]{\n\t|: : [\n\t\t\\|\"],};]*}]", @"{}/{|}], -\"}[|[{}\\\"]:{~]\", , }{]\{|: : [\\|\"],};]*}]",@"random garbage"],
+ ];
+
+ for (NSArray *pair in tests) {
+ XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:[pair objectAtIndex:0]], [pair objectAtIndex:1], @"%@", [pair objectAtIndex:2]);
+ }
+ }
+
+
+}
+
+@end
diff --git a/readme.md b/readme.md
index b62d9743..957e2cdc 100644
--- a/readme.md
+++ b/readme.md
@@ -1,11 +1,11 @@
-Sequel Pro
+Sequel Pro <img alt="Logo" src="https://sequelpro.com/images/logo.png" align="right" height="50">
==========
Sequel Pro is a fast, easy-to-use Mac database management application for working with MySQL databases.
You can find more details on our website: [sequelpro.com](http://sequelpro.com)
-![Screenshot](http://www.sequelpro.com/assets/images/NewAdvancedFilter.jpg)
+![Screenshot](https://sequelpro.com/images/browse.png)
Build Instructions
==================
diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj
index 272944ff..b84b20e4 100644
--- a/sequel-pro.xcodeproj/project.pbxproj
+++ b/sequel-pro.xcodeproj/project.pbxproj
@@ -224,6 +224,8 @@
5080229C1BF7C0FE0052A9B2 /* MGTemplateStandardMarkers.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AD0F909194002A3258 /* MGTemplateStandardMarkers.m */; };
5080229D1BF7C0FE0052A9B2 /* MGTemplateStandardFilters.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8B40F909194002A3258 /* MGTemplateStandardFilters.m */; };
50805B0D1BF2A068005F7A99 /* SPPopUpButtonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 50805B0C1BF2A068005F7A99 /* SPPopUpButtonCell.m */; };
+ 50837F741E50DCD4004FAE8A /* SPJSONFormatterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */; };
+ 50837F771E50E007004FAE8A /* SPJSONFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 73F70A951E4E547500636550 /* SPJSONFormatter.m */; };
5089B0271BE714E300E226CD /* SPIdMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 5089B0261BE714E300E226CD /* SPIdMenu.m */; };
50A9F8B119EAD4B90053E571 /* SPGotoDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */; };
50D3C3491A75B8A800B5429C /* GotoDatabaseDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 50D3C34B1A75B8A800B5429C /* GotoDatabaseDialog.xib */; };
@@ -415,6 +417,7 @@
58FEEF471676D160009CD478 /* SQL.icns in Resources */ = {isa = PBXBuildFile; fileRef = 58FEEF441676D160009CD478 /* SQL.icns */; };
58FEF16D0F23D66600518E8E /* SPSQLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF16C0F23D66600518E8E /* SPSQLParser.m */; };
58FEF57E0F3B4E9700518E8E /* SPTableData.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF57D0F3B4E9700518E8E /* SPTableData.m */; };
+ 73F70A961E4E547500636550 /* SPJSONFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 73F70A951E4E547500636550 /* SPJSONFormatter.m */; };
8D15AC340486D014006FF6A4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */; };
B51D6B9E114C310C0074704E /* toolbar-switch-to-table-triggers.png in Resources */ = {isa = PBXBuildFile; fileRef = B51D6B9D114C310C0074704E /* toolbar-switch-to-table-triggers.png */; };
B52460D70F8EF92300171639 /* SPArrayAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = B52460D40F8EF92300171639 /* SPArrayAdditions.m */; };
@@ -966,6 +969,7 @@
508022941BF7BA470052A9B2 /* English */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = English; path = English.lproj/SPQLPluginExportSettingsTemplate.html; sourceTree = "<group>"; };
50805B0B1BF2A068005F7A99 /* SPPopUpButtonCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPPopUpButtonCell.h; sourceTree = "<group>"; };
50805B0C1BF2A068005F7A99 /* SPPopUpButtonCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPPopUpButtonCell.m; sourceTree = "<group>"; };
+ 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPJSONFormatterTests.m; sourceTree = "<group>"; };
5089B0251BE714E300E226CD /* SPIdMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPIdMenu.h; sourceTree = "<group>"; };
5089B0261BE714E300E226CD /* SPIdMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPIdMenu.m; sourceTree = "<group>"; };
50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPGotoDatabaseController.h; sourceTree = "<group>"; };
@@ -1217,6 +1221,8 @@
58FEF16C0F23D66600518E8E /* SPSQLParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSQLParser.m; sourceTree = "<group>"; };
58FEF57C0F3B4E9700518E8E /* SPTableData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableData.h; sourceTree = "<group>"; };
58FEF57D0F3B4E9700518E8E /* SPTableData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableData.m; sourceTree = "<group>"; };
+ 73F70A941E4E547500636550 /* SPJSONFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPJSONFormatter.h; sourceTree = "<group>"; };
+ 73F70A951E4E547500636550 /* SPJSONFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPJSONFormatter.m; sourceTree = "<group>"; };
8D15AC370486D014006FF6A4 /* Sequel Pro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sequel Pro.app"; sourceTree = BUILT_PRODUCTS_DIR; };
B51D6B9D114C310C0074704E /* toolbar-switch-to-table-triggers.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "toolbar-switch-to-table-triggers.png"; sourceTree = "<group>"; };
B52460D30F8EF92300171639 /* SPArrayAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPArrayAdditions.h; sourceTree = "<group>"; };
@@ -1671,13 +1677,13 @@
BC27779F11514B940034DF6A /* SPNavigatorController.m */,
17A7773211C52D8E001E27B4 /* SPIndexesController.h */,
17A7773311C52D8E001E27B4 /* SPIndexesController.m */,
- 17846B9D170C95D800414499 /* Process List */,
- 17381853151FB29C0078FFE2 /* User Manager */,
- 1713C73D140D88D400CFD461 /* Query Controller */,
50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */,
50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */,
506CE92F1A311C6C0039F736 /* SPTableContentFilterController.h */,
506CE9301A311C6C0039F736 /* SPTableContentFilterController.m */,
+ 1713C73D140D88D400CFD461 /* Query Controller */,
+ 17381853151FB29C0078FFE2 /* User Manager */,
+ 17846B9D170C95D800414499 /* Process List */,
);
name = "Subview Controllers";
sourceTree = "<group>";
@@ -2469,6 +2475,7 @@
children = (
50D3C35B1A771C4C00B5429C /* SPParserUtilsTest.m */,
503B02CE1AE95C2C0060CAB1 /* SPTableFilterParserTest.m */,
+ 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */,
);
name = Other;
sourceTree = "<group>";
@@ -2664,6 +2671,8 @@
50D3C3511A77135F00B5429C /* SPParserUtils.h */,
503B02C81AE82C5E0060CAB1 /* SPTableFilterParser.h */,
503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */,
+ 73F70A941E4E547500636550 /* SPJSONFormatter.h */,
+ 73F70A951E4E547500636550 /* SPJSONFormatter.m */,
);
name = Parsing;
sourceTree = "<group>";
@@ -3175,6 +3184,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 50837F771E50E007004FAE8A /* SPJSONFormatter.m in Sources */,
505F56901BCEE491007467DD /* SPOSInfo.m in Sources */,
505F568F1BCEE485007467DD /* SPFunctions.m in Sources */,
502D21F81BA50966000D4CE7 /* SPDataAdditions.m in Sources */,
@@ -3184,6 +3194,7 @@
503B02CF1AE95C2C0060CAB1 /* SPTableFilterParserTest.m in Sources */,
50EA92681AB23EFC008D3C4F /* SPTableCopy.m in Sources */,
507FF1621BBF0D5000104523 /* SPTableCopyTest.m in Sources */,
+ 50837F741E50DCD4004FAE8A /* SPJSONFormatterTests.m in Sources */,
50EA926A1AB246B8008D3C4F /* SPDatabaseActionTest.m in Sources */,
50EA92651AB23EC8008D3C4F /* SPDatabaseAction.m in Sources */,
50EA92641AB23EAD008D3C4F /* SPDatabaseCopy.m in Sources */,
@@ -3311,6 +3322,7 @@
1740FABB0FC4372F00CF3699 /* SPDatabaseData.m in Sources */,
17C058880FC9FC390077E9CF /* SPNarrowDownCompletion.m in Sources */,
177E7A230FCB6A2E00E9E122 /* SPExtendedTableInfo.m in Sources */,
+ 73F70A961E4E547500636550 /* SPJSONFormatter.m in Sources */,
58CDB3300FCE138D00F8ACA3 /* SPSSHTunnel.m in Sources */,
29A1B7E50FD1293A000B88E8 /* SPPrintAccessory.m in Sources */,
BC1847EA0FE6EC8400094BFB /* SPEditSheetTextView.m in Sources */,