//
// $Id$
//
// SPSQLExporter.m
// sequel-pro
//
// Created by Stuart Connolly (stuconnolly.com) on August 29, 2009.
// Copyright (c) 2009 Stuart Connolly. 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
#import "SPSQLExporter.h"
#import "SPTablesList.h"
#import "SPFileHandle.h"
#import "SPExportUtilities.h"
#import "SPExportFile.h"
#import "SPTableData.h"
#import "RegexKitLite.h"
#import
@interface SPSQLExporter ()
- (NSString *)_createViewPlaceholderSyntaxForView:(NSString *)viewName;
@end
@implementation SPSQLExporter
@synthesize delegate;
@synthesize sqlExportTables;
@synthesize sqlDatabaseHost;
@synthesize sqlDatabaseName;
@synthesize sqlDatabaseVersion;
@synthesize sqlExportCurrentTable;
@synthesize sqlExportErrors;
@synthesize sqlOutputIncludeUTF8BOM;
@synthesize sqlOutputEncodeBLOBasHex;
@synthesize sqlOutputIncludeErrors;
@synthesize sqlOutputIncludeAutoIncrement;
@synthesize sqlCurrentTableExportIndex;
@synthesize sqlInsertAfterNValue;
@synthesize sqlInsertDivider;
/**
* Initialise an instance of SPSQLExporter using the supplied delegate.
*
* @param exportDelegate The exporter delegate
*
* @return The initialised instance
*/
- (id)initWithDelegate:(NSObject *)exportDelegate
{
if ((self = [super init])) {
SPExportDelegateConformsToProtocol(exportDelegate, @protocol(SPSQLExporterProtocol));
[self setDelegate:exportDelegate];
[self setSqlExportCurrentTable:nil];
[self setSqlInsertDivider:SPSQLInsertEveryNDataBytes];
[self setSqlInsertAfterNValue:250000];
}
return self;
}
/**
* Start the SQL export process. This method is automatically called when an instance of this class
* is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading.
*/
- (void)main
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
sqlTableDataInstance = [[[SPTableData alloc] init] autorelease];
[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;
BOOL sqlOutputIncludeStructure;
BOOL sqlOutputIncludeContent;
BOOL sqlOutputIncludeDropSyntax;
NSMutableArray *tables = [NSMutableArray array];
NSMutableArray *procs = [NSMutableArray array];
NSMutableArray *funcs = [NSMutableArray array];
NSMutableString *metaString = [NSMutableString string];
NSMutableString *errors = [[NSMutableString alloc] init];
NSMutableString *sqlString = [[NSMutableString alloc] init];
NSMutableDictionary *viewSyntaxes = [NSMutableDictionary dictionary];
// Check that we have all the required info before starting the export
if ((![self sqlExportTables]) || ([[self sqlExportTables] count] == 0) ||
(![self sqlDatabaseHost]) || ([[self sqlDatabaseHost] isEqualToString:@""]) ||
(![self sqlDatabaseName]) || ([[self sqlDatabaseName] isEqualToString:@""]) ||
(![self sqlDatabaseVersion] || ([[self sqlDatabaseName] isEqualToString:@""])))
{
[errors release];
[sqlString release];
[pool release];
return;
}
// Inform the delegate that the export process is about to begin
[delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBegin:) withObject:self waitUntilDone:NO];
// Mark the process as running
[self setExportProcessIsRunning:YES];
// Clear errors
[self setSqlExportErrors:@""];
// Copy over the selected item names into tables in preparation for iteration
NSMutableArray *targetArray;
for (NSArray *item in [self sqlExportTables])
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
switch ([NSArrayObjectAtIndex(item, 4) intValue]) {
case SPTableTypeProc:
targetArray = procs;
break;
case SPTableTypeFunc:
targetArray = funcs;
break;
default:
targetArray = tables;
break;
}
[targetArray addObject:item];
}
// If required write the UTF-8 Byte Order Mark (BOM)
if ([self sqlOutputIncludeUTF8BOM]) {
[metaString setString:@"\xef\xbb\xbf"];
[metaString appendString:@"# ************************************************************\n"];
}
else {
[metaString setString:@"# ************************************************************\n"];
}
// Add the dump header to the dump file
[metaString appendString:@"# Sequel Pro SQL dump\n"];
[metaString appendFormat:@"# %@ %@\n#\n", NSLocalizedString(@"Version", @"export header version label"), [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
[metaString appendFormat:@"# %@\n# %@\n#\n", SPLOCALIZEDURL_HOMEPAGE, SPDevURL];
[metaString appendFormat:@"# %@: %@ (MySQL %@)\n", NSLocalizedString(@"Host", @"export header host label"), [self sqlDatabaseHost], [self sqlDatabaseVersion]];
[metaString appendFormat:@"# %@: %@\n", NSLocalizedString(@"Database", @"export header database label"), [self sqlDatabaseName]];
[metaString appendFormat:@"# %@: %@\n", NSLocalizedString(@"Generation Time", @"export header generation time label"), [NSDate date]];
[metaString appendString:@"# ************************************************************\n\n\n"];
// Add commands to store the client encodings used when importing and set to UTF8 to preserve data
[metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n"];
[metaString appendString:@"/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n"];
[metaString appendString:@"/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n"];
[metaString appendString:@"/*!40101 SET NAMES utf8 */;\n"];
[metaString appendString:@"/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n"];
[metaString appendString:@"/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n"];
[metaString appendString:@"/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n\n"];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:[self exportOutputEncoding]]];
// Loop through the selected tables
for (NSArray *table in tables)
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
[self setSqlCurrentTableExportIndex:[self sqlCurrentTableExportIndex]+1];
tableName = NSArrayObjectAtIndex(table, 0);
sqlOutputIncludeStructure = [NSArrayObjectAtIndex(table, 1) boolValue];
sqlOutputIncludeContent = [NSArrayObjectAtIndex(table, 2) boolValue];
sqlOutputIncludeDropSyntax = [NSArrayObjectAtIndex(table, 3) boolValue];
// Skip tables if not set to output any detail for them
if (!sqlOutputIncludeStructure && !sqlOutputIncludeContent && !sqlOutputIncludeDropSyntax) {
continue;
}
// Set the current table
[self setSqlExportCurrentTable:tableName];
// 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;
// Add the name of table
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# %@ %@\n# ------------------------------------------------------------\n\n", NSLocalizedString(@"Dump of table", @"sql export dump of table label"), tableName] dataUsingEncoding:[self exportOutputEncoding]]];
// Determine whether this table is a table or a view via the CREATE TABLE command, and keep the create table syntax
queryResult = [connection queryString:[NSString stringWithFormat:@"SHOW CREATE TABLE %@", [tableName backtickQuotedString]]];
[queryResult setReturnDataAsStrings:YES];
if ([queryResult numberOfRows]) {
tableDetails = [[NSDictionary alloc] initWithDictionary:[queryResult getRowAsDictionary]];
if ([tableDetails objectForKey:@"Create View"]) {
[viewSyntaxes setValue:[[[[tableDetails objectForKey:@"Create View"] copy] autorelease] createViewSyntaxPrettifier] forKey:tableName];
createTableSyntax = [self _createViewPlaceholderSyntaxForView:tableName];
tableType = SPTableTypeView;
}
else {
createTableSyntax = [[[tableDetails objectForKey:@"Create Table"] copy] autorelease];
tableType = SPTableTypeTable;
}
[tableDetails release];
}
if ([connection queryErrored]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n\n\n", [connection lastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]];
continue;
}
// Add a 'DROP TABLE' command if required
if (sqlOutputIncludeDropSyntax) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"DROP %@ IF EXISTS %@;\n\n", ((tableType == SPTableTypeTable) ? @"TABLE" : @"VIEW"), [tableName backtickQuotedString]]
dataUsingEncoding:[self exportOutputEncoding]]];
}
// Add the create syntax for the table if specified in the export dialog
if (sqlOutputIncludeStructure && createTableSyntax) {
if ([createTableSyntax isKindOfClass:[NSData class]]) {
createTableSyntax = [[[NSString alloc] initWithData:createTableSyntax encoding:[self exportOutputEncoding]] autorelease];
}
// If necessary strip out the AUTO_INCREMENT from the table structure definition
if (![self sqlOutputIncludeAutoIncrement]) {
createTableSyntax = [createTableSyntax stringByReplacingOccurrencesOfRegex:[NSString stringWithFormat:@"AUTO_INCREMENT=[0-9]+ "] withString:@""];
}
[[self exportOutputFile] writeData:[createTableSyntax dataUsingEncoding:NSUTF8StringEncoding]];
[[self exportOutputFile] writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
// Add the table content if required
if (sqlOutputIncludeContent && (tableType == SPTableTypeTable)) {
// Retrieve the table details via the data class, and use it to build an array containing column numeric status
tableDetails = [NSDictionary dictionaryWithDictionary:[sqlTableDataInstance informationForTable:tableName]];
NSUInteger colCount = [[tableDetails objectForKey:@"columns"] count];
NSMutableArray *rawColumnNames = [NSMutableArray arrayWithCapacity:colCount];
NSMutableArray *queryColumnDetails = [NSMutableArray arrayWithCapacity:colCount];
useRawDataForColumnAtIndex = malloc(sizeof(BOOL) * colCount);
useRawHexDataForColumnAtIndex = malloc(sizeof(BOOL) * colCount);
// Determine whether raw data can be used for each column during processing - safe numbers and hex-encoded data.
for (j = 0; j < colCount; j++)
{
NSDictionary *theColumnDetail = NSArrayObjectAtIndex([tableDetails objectForKey:@"columns"], j);
NSString *theTypeGrouping = [theColumnDetail objectForKey:@"typegrouping"];
// Start by setting the column as non-safe
useRawDataForColumnAtIndex[j] = NO;
useRawHexDataForColumnAtIndex[j] = NO;
// Determine whether the column should be retrieved as hex data from the server - for binary strings, to
// avoid encoding issues when processing
if ([self sqlOutputEncodeBLOBasHex]
&& [theTypeGrouping isEqualToString:@"string"]
&& ([[theColumnDetail objectForKey:@"binary"] boolValue] || [[theColumnDetail objectForKey:@"collation"] hasSuffix:@"_bin"]))
{
useRawHexDataForColumnAtIndex[j] = YES;
}
// Floats, integers and bits can be output directly assuming they're non-binary
if (![[theColumnDetail objectForKey:@"binary"] boolValue]
&& ([theTypeGrouping isEqualToString:@"bit"] || [theTypeGrouping isEqualToString:@"integer"] || [theTypeGrouping isEqualToString:@"float"]))
{
useRawDataForColumnAtIndex[j] = YES;
}
// Set up the column query string parts
[rawColumnNames addObject:[theColumnDetail objectForKey:@"name"]];
if (useRawHexDataForColumnAtIndex[j]) {
[queryColumnDetails addObject:[NSString stringWithFormat:@"HEX(%@)", [[theColumnDetail objectForKey:@"name"] mySQLBacktickQuotedString]]];
} else {
[queryColumnDetails addObject:[[theColumnDetail objectForKey:@"name"] mySQLBacktickQuotedString]];
}
}
// Retrieve the number of rows in the table for progress bar drawing
NSArray *rowArray = [[connection queryString:[NSString stringWithFormat:@"SELECT COUNT(1) FROM %@", [tableName backtickQuotedString]]] getRowAsArray];
if ([connection queryErrored] || ![rowArray count]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n\n\n", [connection lastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]];
free(useRawDataForColumnAtIndex);
free(useRawHexDataForColumnAtIndex);
continue;
}
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];
// 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;
// Lock the table for writing and disable keys if supported
[metaString setString:@""];
[metaString appendFormat:@"LOCK TABLES %@ WRITE;\n/*!40000 ALTER TABLE %@ DISABLE KEYS */;\n\n", [tableName backtickQuotedString], [tableName backtickQuotedString]];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:[self exportOutputEncoding]]];
// Construct the start of the insertion command
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES\n\t(", [tableName backtickQuotedString], [rawColumnNames componentsJoinedAndBacktickQuoted]] dataUsingEncoding:NSUTF8StringEncoding]];
// Iterate through the rows to construct a VALUES group for each
j = 0, k = 0;
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]))
{
// Check for cancellation flag
if ([self isCancelled]) {
[connection cancelCurrentQuery];
[streamingResult cancelResultLoad];
[streamingResult release];
[sqlExportPool release];
[errors release];
[sqlString release];
[pool release];
free(useRawDataForColumnAtIndex);
free(useRawHexDataForColumnAtIndex);
return;
}
j++;
k++;
[sqlString setString:@""];
// Update the progress
NSUInteger progress = (NSUInteger)(j * ([self exportMaxProgress] / rowCount));
if (progress > lastProgressValue) {
[self setExportProgressValue:progress];
lastProgressValue = progress;
// Inform the delegate that the export's progress has been updated
[delegate performSelectorOnMainThread:@selector(sqlExportProcessProgressUpdated:) withObject:self waitUntilDone:NO];
}
for (t = 0; t < colCount; t++)
{
// Check for cancellation flag
if ([self isCancelled]) {
[connection cancelCurrentQuery];
[streamingResult cancelResultLoad];
[streamingResult release];
[sqlExportPool release];
[errors release];
[sqlString release];
[pool release];
free(useRawDataForColumnAtIndex);
free(useRawHexDataForColumnAtIndex);
return;
}
id object = NSArrayObjectAtIndex(row, t);
// Add NULL values directly to the output row; use a pointer comparison to the singleton
// instance for speed.
if (object == [NSNull null]) {
[sqlString appendString:@"NULL"];
}
// Add trusted raw values directly
else if (useRawDataForColumnAtIndex[t]) {
[sqlString appendString:object];
}
// If the field is of type BIT, the values need a binary prefix of b'x'.
else if ([[NSArrayObjectAtIndex([tableDetails objectForKey:@"columns"], t) objectForKey:@"type"] isEqualToString:@"BIT"]) {
[sqlString appendFormat:@"b'%@'", [object description]];
}
// Add pre-encoded hex types (binary strings) as enclosed but otherwise trusted data
else if (useRawHexDataForColumnAtIndex[t]) {
[sqlString appendFormat:@"X'%@'", object];
}
// GEOMETRY data types directly as hex data
else if ([object isKindOfClass:[SPMySQLGeometryData class]]) {
[sqlString appendString:[connection escapeAndQuoteData:[object data]]];
}
// Add zero-length data or strings as an empty string
else if ([object length] == 0) {
[sqlString appendString:@"''"];
}
// Add other data types as hex data
else if ([object isKindOfClass:[NSData class]]) {
if ([self sqlOutputEncodeBLOBasHex]) {
[sqlString appendString:[connection escapeAndQuoteData:object]];
}
else {
NSString *data = [[NSString alloc] initWithData:object encoding:[self exportOutputEncoding]];
if (data == nil) {
data = [[NSString alloc] initWithData:object encoding:NSASCIIStringEncoding];
}
[sqlString appendFormat:@"'%@'", data];
[data release];
}
}
// Otherwise add a quoted string with special characters escaped
else {
[sqlString appendString:[connection escapeAndQuoteString:object]];
}
// Add the field separator if this isn't the last cell in the row
if (t != ([row count] - 1)) [sqlString appendString:@","];
}
queryLength += [sqlString length];
// Close this VALUES group and set up the next one if appropriate
if (j != rowCount) {
// If required start a new INSERT statment
if ((([self sqlInsertDivider] == SPSQLInsertEveryNDataBytes) && (queryLength >= ([self sqlInsertAfterNValue] * 1024))) ||
(([self sqlInsertDivider] == SPSQLInsertEveryNRows) && (k == [self sqlInsertAfterNValue])))
{
[sqlString appendString:@");\n\nINSERT INTO "];
[sqlString appendString:[tableName backtickQuotedString]];
[sqlString appendString:@" ("];
[sqlString appendString:[rawColumnNames componentsJoinedAndBacktickQuoted]];
[sqlString appendString:@")\nVALUES\n\t("];
queryLength = 0, k = 0;
// Use the opportunity to drain and reset the autorelease pool
[sqlExportPool release];
sqlExportPool = [[NSAutoreleasePool alloc] init];
}
else {
[sqlString appendString:@"),\n\t("];
}
}
else {
[sqlString appendString:@")"];
}
// Write this row to the file
[[self exportOutputFile] writeData:[sqlString dataUsingEncoding:NSUTF8StringEncoding]];
}
// Complete the command
[[self exportOutputFile] writeData:[[NSString stringWithString:@";\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
// Unlock the table and re-enable keys if supported
[metaString setString:@""];
[metaString appendFormat:@"/*!40000 ALTER TABLE %@ ENABLE KEYS */;\nUNLOCK TABLES;\n", [tableName backtickQuotedString]];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
// Drain the autorelease pool
[sqlExportPool release];
// Release the result set
[streamingResult release];
}
free(useRawDataForColumnAtIndex);
free(useRawHexDataForColumnAtIndex);
if ([connection queryErrored]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
if ([self sqlOutputIncludeErrors]) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection lastErrorMessage]]
dataUsingEncoding:NSUTF8StringEncoding]];
}
}
}
// Add triggers if the structure export was enabled
if (sqlOutputIncludeStructure) {
queryResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW TRIGGERS WHERE `Table` = %@ */", [tableName tickQuotedString]]];
[queryResult setReturnDataAsStrings:YES];
if ([queryResult numberOfRows]) {
[metaString setString:@"\n"];
[metaString appendString:@"DELIMITER ;;\n"];
for (s = 0; s < [queryResult numberOfRows]; s++)
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
NSDictionary *triggers = [[NSDictionary alloc] initWithDictionary:[queryResult getRowAsDictionary]];
// Definer is user@host but we need to escape it to `user`@`host`
NSArray *triggersDefiner = [[triggers objectForKey:@"Definer"] componentsSeparatedByString:@"@"];
[metaString appendFormat:@"/*!50003 SET SESSION SQL_MODE=\"%@\" */;;\n/*!50003 CREATE */ ", [triggers objectForKey:@"sql_mode"]];
[metaString appendFormat:@"/*!50017 DEFINER=%@@%@ */ /*!50003 TRIGGER %@ %@ %@ ON %@ FOR EACH ROW %@ */;;\n",
[NSArrayObjectAtIndex(triggersDefiner, 0) backtickQuotedString],
[NSArrayObjectAtIndex(triggersDefiner, 1) backtickQuotedString],
[[triggers objectForKey:@"Trigger"] backtickQuotedString],
[triggers objectForKey:@"Timing"],
[triggers objectForKey:@"Event"],
[[triggers objectForKey:@"Table"] backtickQuotedString],
[triggers objectForKey:@"Statement"]
];
[triggers release];
}
[metaString appendString:@"DELIMITER ;\n/*!50003 SET SESSION SQL_MODE=@OLD_SQL_MODE */;\n"];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
}
if ([connection queryErrored]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
if ([self sqlOutputIncludeErrors]) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection lastErrorMessage]]
dataUsingEncoding:NSUTF8StringEncoding]];
}
}
}
// Add an additional separator between tables
[[self exportOutputFile] writeData:[[NSString stringWithString:@"\n\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
// Process any deferred views, adding commands to delete the placeholder tables and add the actual views
for (tableName in viewSyntaxes)
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
[metaString setString:@"\n\n"];
// Add the name of table
[metaString appendFormat:@"# Replace placeholder table for %@ with correct view syntax\n# ------------------------------------------------------------\n\n", tableName];
[metaString appendFormat:@"DROP TABLE %@;\n\n", [tableName backtickQuotedString]];
[metaString appendFormat:@"%@;\n", [viewSyntaxes objectForKey:tableName]];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
}
// Export procedures and functions
for (NSString *procedureType in [NSArray arrayWithObjects:@"PROCEDURE", @"FUNCTION", nil])
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
// Retrieve the array of selected procedures or functions, and skip export if not selected
NSMutableArray *items;
if ([procedureType isEqualToString:@"PROCEDURE"]) items = procs;
else items = funcs;
if ([items count] == 0) continue;
// Retrieve the definitions
queryResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW %@ STATUS WHERE `Db` = %@ */", procedureType,
[[self sqlDatabaseName] tickQuotedString]]];
[queryResult setReturnDataAsStrings:YES];
if ([queryResult numberOfRows]) {
[metaString setString:@"\n"];
[metaString appendFormat:@"--\n-- Dumping routines (%@) for database %@\n--\nDELIMITER ;;\n\n", procedureType,
[[self sqlDatabaseName] tickQuotedString]];
// Loop through the definitions, exporting if enabled
for (s = 0; s < [queryResult numberOfRows]; s++)
{
// Check for cancellation flag
if ([self isCancelled]) {
[errors release];
[sqlString release];
[pool release];
return;
}
NSDictionary *proceduresList = [[NSDictionary alloc] initWithDictionary:[queryResult getRowAsDictionary]];
NSString *procedureName = [NSString stringWithFormat:@"%@", [proceduresList objectForKey:@"Name"]];
// Only proceed if the item is in the list of items
BOOL itemFound = NO;
for (NSArray *item in items)
{
// Check for cancellation flag
if ([self isCancelled]) {
[proceduresList release];
[errors release];
[sqlString release];
[pool release];
return;
}
if ([NSArrayObjectAtIndex(item, 0) isEqualToString:procedureName]) {
itemFound = YES;
sqlOutputIncludeStructure = [NSArrayObjectAtIndex(item, 1) boolValue];
sqlOutputIncludeContent = [NSArrayObjectAtIndex(item, 2) boolValue];
sqlOutputIncludeDropSyntax = [NSArrayObjectAtIndex(item, 3) boolValue];
break;
}
}
if (!itemFound) {
[proceduresList release];
continue;
}
if (sqlOutputIncludeStructure || sqlOutputIncludeDropSyntax)
[metaString appendFormat:@"# Dump of %@ %@\n# ------------------------------------------------------------\n\n", procedureType, procedureName];
// Add the 'DROP' command if required
if (sqlOutputIncludeDropSyntax) {
[metaString appendFormat:@"/*!50003 DROP %@ IF EXISTS %@ */;;\n", procedureType,
[procedureName backtickQuotedString]];
}
// Only continue if the 'CREATE SYNTAX' is required
if (!sqlOutputIncludeStructure) {
[proceduresList release];
continue;
}
// Definer is user@host but we need to escape it to `user`@`host`
NSArray *procedureDefiner = [[proceduresList objectForKey:@"Definer"] componentsSeparatedByString:@"@"];
NSString *escapedDefiner = [NSString stringWithFormat:@"%@@%@",
[NSArrayObjectAtIndex(procedureDefiner, 0) backtickQuotedString],
[NSArrayObjectAtIndex(procedureDefiner, 1) backtickQuotedString]
];
SPMySQLResult *createProcedureResult = [connection queryString:[NSString stringWithFormat:@"/*!50003 SHOW CREATE %@ %@ */", procedureType,
[procedureName backtickQuotedString]]];
[createProcedureResult setReturnDataAsStrings:YES];
if ([connection queryErrored]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
if ([self sqlOutputIncludeErrors]) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection lastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]];
}
[proceduresList release];
continue;
}
NSDictionary *procedureInfo = [[NSDictionary alloc] initWithDictionary:[createProcedureResult getRowAsDictionary]];
[metaString appendFormat:@"/*!50003 SET SESSION SQL_MODE=\"%@\"*/;;\n", [procedureInfo objectForKey:@"sql_mode"]];
NSString *createProcedure = [procedureInfo objectForKey:[NSString stringWithFormat:@"Create %@", [procedureType capitalizedString]]];
// A NULL result indicates a permission problem
if ([createProcedure isNSNull]) {
NSString *errorString = [NSString stringWithFormat:NSLocalizedString(@"Could not export the %@ '%@' because of a permisions error.\n", @"Procedure/function export permission error"), procedureType, procedureName];
[errors appendString:errorString];
if ([self sqlOutputIncludeErrors]) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n", errorString] dataUsingEncoding:NSUTF8StringEncoding]];
}
[proceduresList release];
[procedureInfo release];
continue;
}
NSRange procedureRange = [createProcedure rangeOfString:procedureType options:NSCaseInsensitiveSearch];
NSString *procedureBody = [createProcedure substringFromIndex:procedureRange.location];
// /*!50003 CREATE*/ /*!50020 DEFINER=`sequelpro`@`%`*/ /*!50003 PROCEDURE `p`()
// BEGIN
// /* This procedure does nothing */
// END */;;
//
// Build the CREATE PROCEDURE string to include MySQL Version limiters
[metaString appendFormat:@"/*!50003 CREATE*/ /*!50020 DEFINER=%@*/ /*!50003 %@ */;;\n\n/*!50003 SET SESSION SQL_MODE=@OLD_SQL_MODE */;;\n", escapedDefiner, procedureBody];
[procedureInfo release];
[proceduresList release];
}
[metaString appendString:@"DELIMITER ;\n"];
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
}
if ([connection queryErrored]) {
[errors appendFormat:@"%@\n", [connection lastErrorMessage]];
if ([self sqlOutputIncludeErrors]) {
[[self exportOutputFile] writeData:[[NSString stringWithFormat:@"# Error: %@\n", [connection lastErrorMessage]] dataUsingEncoding:NSUTF8StringEncoding]];
}
}
}
// Restore unique checks, foreign key checks, and other settings saved at the start
[metaString setString:@"\n"];
[metaString appendString:@"/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n"];
[metaString appendString:@"/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n"];
[metaString appendString:@"/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n"];
// Restore the client encoding to the original encoding before import
[metaString appendString:@"/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n"];
[metaString appendString:@"/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n"];
[metaString appendString:@"/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n"];
// Write footer-type information to the file
[[self exportOutputFile] writeData:[metaString dataUsingEncoding:NSUTF8StringEncoding]];
// Set export errors
[self setSqlExportErrors:errors];
[errors release];
[sqlString release];
// Close the file
[[self exportOutputFile] close];
// Mark the process as not running
[self setExportProcessIsRunning:NO];
// Inform the delegate that the export process is complete
[delegate performSelectorOnMainThread:@selector(sqlExportProcessComplete:) withObject:self waitUntilDone:NO];
[pool release];
}
/**
* Returns whether or not any export errors occurred by examing the length of the errors string.
*
* @return A BOOL indicating the occurrence of errors
*/
- (BOOL)didExportErrorsOccur
{
return [[self sqlExportErrors] length];
}
/**
* Retrieve information for a view and use that to construct a CREATE TABLE string for an equivalent basic
* table. Allows the construction of placeholder tables to resolve view interdependencies within dumps.
*
* @param viewName The name of the view for which the placeholder is to be created for.
*
* @return The CREATE TABLE placeholder syntax
*/
- (NSString *)_createViewPlaceholderSyntaxForView:(NSString *)viewName
{
NSUInteger i, j;
NSMutableString *placeholderSyntax;
// Get structured information for the view via the SPTableData parsers
NSDictionary *viewInformation = [sqlTableDataInstance informationForView:viewName];
if (!viewInformation) return nil;
NSArray *viewColumns = [viewInformation objectForKey:@"columns"];
// Set up the start of the placeholder string and initialise an empty field string
placeholderSyntax = [[NSMutableString alloc] initWithFormat:@"CREATE TABLE %@ (\n", [viewName backtickQuotedString]];
NSMutableString *fieldString = [[NSMutableString alloc] init];
// Loop through the columns, creating an appropriate column definition for each and appending it to the syntax string
for (i = 0; i < [viewColumns count]; i++)
{
NSDictionary *column = NSArrayObjectAtIndex(viewColumns, i);
[fieldString setString:[[column objectForKey:@"name"] backtickQuotedString]];
// Add the type and length information as appropriate
if ([column objectForKey:@"length"]) {
[fieldString appendFormat:@" %@(%@)", [column objectForKey:@"type"], [column objectForKey:@"length"]];
}
else if ([column objectForKey:@"values"]) {
[fieldString appendFormat:@" %@(", [column objectForKey:@"type"]];
for (j = 0; j < [[column objectForKey:@"values"] count]; j++)
{
[fieldString appendString:[connection escapeAndQuoteString:NSArrayObjectAtIndex([column objectForKey:@"values"], j)]];
if ((j + 1) != [[column objectForKey:@"values"] count]) {
[fieldString appendString:@","];
}
}
[fieldString appendString:@")"];
}
else {
[fieldString appendFormat:@" %@", [column objectForKey:@"type"]];
}
// Field specification details
if ([[column objectForKey:@"unsigned"] integerValue] == 1) [fieldString appendString:@" UNSIGNED"];
if ([[column objectForKey:@"zerofill"] integerValue] == 1) [fieldString appendString:@" ZEROFILL"];
if ([[column objectForKey:@"binary"] integerValue] == 1) [fieldString appendString:@" BINARY"];
if ([[column objectForKey:@"null"] integerValue] == 0) [fieldString appendString:@" NOT NULL"];
// Provide the field default if appropriate
if ([column objectForKey:@"default"]) {
// Some MySQL server versions show a default of NULL for NOT NULL columns - don't export those.
// Check against the NSNull singleton instance for speed.
if ([column objectForKey:@"default"] == [NSNull null]) {
if ([[column objectForKey:@"null"] integerValue]) {
[fieldString appendString:@" DEFAULT NULL"];
}
}
else if ([[column objectForKey:@"type"] isEqualToString:@"TIMESTAMP"] && [column objectForKey:@"default"] != [NSNull null] && [[[column objectForKey:@"default"] uppercaseString] isEqualToString:@"CURRENT_TIMESTAMP"]) {
[fieldString appendString:@" DEFAULT CURRENT_TIMESTAMP"];
}
else {
[fieldString appendFormat:@" DEFAULT %@", [connection escapeAndQuoteString:[column objectForKey:@"default"]]];
}
}
// Extras aren't required for the temp table
// Add the field string to the syntax string
[placeholderSyntax appendFormat:@" %@%@\n", fieldString, (i == [viewColumns count] - 1) ? @"" : @","];
}
// Append the remainder of the table string
[placeholderSyntax appendString:@") ENGINE=MyISAM"];
// Clean up and return
[fieldString release];
return [placeholderSyntax autorelease];
}
/**
* Dealloc
*/
- (void)dealloc
{
[sqlExportTables release], sqlExportTables = nil;
[sqlDatabaseHost release], sqlDatabaseHost = nil;
[sqlDatabaseName release], sqlDatabaseName = nil;
[sqlExportCurrentTable release], sqlExportCurrentTable = nil;
[sqlDatabaseVersion release], sqlDatabaseVersion = nil;
[sqlExportErrors release], sqlExportErrors = nil;
[super dealloc];
}
@end