Files
HeidiSQL/source/mysql_connection.pas
Ansgar Becker c0da6e5a21 Refactor "Copy table" dialog:
* Replace checkboxes, radio buttons and checklistbox by a VirtualTree using checkbox support
* Support selecting/deselecting single indexes
* Support foreign keys
* Place a SynMemo at the bottom in which the user can type an optional WHERE clause to filter incoming data. Fixes issue #2000.
* Move code for SQL generation into TTableColumn etc.
2010-06-14 00:21:33 +00:00

2550 lines
80 KiB
ObjectPascal

unit mysql_connection;
interface
uses
Classes, SysUtils, windows, mysql_api, mysql_structures, SynRegExpr, Contnrs, Generics.Collections, Generics.Defaults,
DateUtils, Types, ShellApi, Math, Dialogs;
type
{ TDBObjectList and friends }
TListNodeType = (lntNone, lntDb, lntTable, lntView, lntFunction, lntProcedure, lntTrigger, lntEvent, lntColumn);
TListNodeTypes = Set of TListNodeType;
TDBObject = class(TPersistent)
private
function GetObjType: String;
function GetImageIndex: Integer;
public
Name, Database, Engine, Comment, RowFormat, CreateOptions, Collation: String;
Created, Updated, LastChecked: TDateTime;
Rows, Size, Version, AvgRowLen, MaxDataLen, IndexLen, DataLen, DataFree, AutoInc, CheckSum: Int64;
NodeType: TListNodeType;
constructor Create;
procedure Assign(Source: TPersistent); override;
property ObjType: String read GetObjType;
property ImageIndex: Integer read GetImageIndex;
end;
PDBObject = ^TDBObject;
TDBObjectList = class(TObjectList<TDBObject>)
private
FDatabase: String;
FDataSize: Int64;
FLastUpdate: TDateTime;
public
property Database: String read FDatabase;
property DataSize: Int64 read FDataSize;
property LastUpdate: TDateTime read FLastUpdate;
end;
TDatabaseList = TObjectList<TDBObjectList>; // A list of db object lists, used for caching
TDBObjectComparer = class(TComparer<TDBObject>)
function Compare(const Left, Right: TDBObject): Integer; override;
end;
// General purpose editing status flag
TEditingStatus = (esUntouched, esModified, esDeleted, esAddedUntouched, esAddedModified, esAddedDeleted);
TColumnDefaultType = (cdtNothing, cdtText, cdtTextUpdateTS, cdtNull, cdtNullUpdateTS, cdtCurTS, cdtCurTSUpdateTS, cdtAutoInc);
// Column object, many of them in a TObjectList
TTableColumn = class(TObject)
private
procedure SetStatus(Value: TEditingStatus);
public
Name, OldName: String;
DataType: TDatatype;
LengthSet: String;
Unsigned, AllowNull: Boolean;
DefaultType: TColumnDefaultType;
DefaultText: String;
Comment, Collation: String;
FStatus: TEditingStatus;
constructor Create;
destructor Destroy; override;
function SQLCode: String;
property Status: TEditingStatus read FStatus write SetStatus;
end;
PTableColumn = ^TTableColumn;
TTableColumnList = TObjectList<TTableColumn>;
TTableKey = class(TObject)
public
Name, OldName: String;
IndexType, Algorithm: String;
Columns, SubParts: TStringList;
Modified, Added: Boolean;
constructor Create;
destructor Destroy; override;
procedure Modification(Sender: TObject);
function SQLCode: String;
end;
TTableKeyList = TObjectList<TTableKey>;
// Helper object to manage foreign keys in a TObjectList
TForeignKey = class(TObject)
public
KeyName, OldKeyName, ReferenceTable, OnUpdate, OnDelete: String;
Columns, ForeignColumns: TStringList;
Modified, Added, KeyNameWasCustomized: Boolean;
constructor Create;
destructor Destroy; override;
function SQLCode: String;
end;
TForeignKeyList = TObjectList<TForeignKey>;
TRoutineParam = class(TObject)
public
Name, Context, Datatype: String;
end;
TRoutineParamList = TObjectList<TRoutineParam>;
// Structures for in-memory changes of a TMySQLQuery
TCellData = class(TObject)
NewText, OldText: String;
NewIsNull, OldIsNull: Boolean;
Modified: Boolean;
end;
TRowData = class(TObjectList<TCellData>)
RecNo: Int64;
Inserted: Boolean;
end;
TUpdateData = TObjectList<TRowData>;
// Custom exception class for any connection or database related error
EDatabaseError = class(Exception);
{$M+} // Needed to add published properties
{ TConnectionParameters and friends }
TNetType = (ntTCPIP, ntNamedPipe, ntSSHtunnel);
TMySQLClientOption = (
opCompress, // CLIENT_COMPRESS
opConnectWithDb, // CLIENT_CONNECT_WITH_DB
opFoundRows, // CLIENT_FOUND_ROWS
opIgnoreSigpipe, // CLIENT_IGNORE_SIGPIPE
opIgnoreSpace, // CLIENT_IGNORE_SPACE
opInteractive, // CLIENT_INTERACTIVE
opLocalFiles, // CLIENT_LOCAL_FILES
opLongFlag, // CLIENT_LONG_FLAG
opLongPassword, // CLIENT_LONG_PASSWORD
opMultiResults, // CLIENT_MULTI_RESULTS
opMultiStatements, // CLIENT_MULTI_STATEMENTS
opNoSchema, // CLIENT_NO_SCHEMA
opODBC, // CLIENT_ODBC
opProtocol41, // CLIENT_PROTOCOL_41
opRememberOptions, // CLIENT_REMEMBER_OPTIONS
opReserved, // CLIENT_RESERVED
opSecureConnection, // CLIENT_SECURE_CONNECTION
opSSL, // CLIENT_SSL
opTransactions // CLIENT_TRANSACTIONS
);
TMySQLClientOptions = set of TMySQLClientOption;
TConnectionParameters = class(TObject)
strict private
FNetType: TNetType;
FHostname, FUsername, FPassword, FAllDatabases, FStartupScriptFilename,
FSSLPrivateKey, FSSLCertificate, FSSLCACertificate,
FSSHHost, FSSHUser, FSSHPassword, FSSHPlinkExe, FSSHPrivateKey: String;
FPort, FSSHPort, FSSHLocalPort: Integer;
FOptions: TMySQLClientOptions;
public
constructor Create;
published
property NetType: TNetType read FNetType write FNetType;
property Hostname: String read FHostname write FHostname;
property Port: Integer read FPort write FPort;
property Username: String read FUsername write FUsername;
property Password: String read FPassword write FPassword;
property AllDatabases: String read FAllDatabases write FAllDatabases;
property StartupScriptFilename: String read FStartupScriptFilename write FStartupScriptFilename;
property Options: TMySQLClientOptions read FOptions write FOptions;
property SSHHost: String read FSSHHost write FSSHHost;
property SSHPort: Integer read FSSHPort write FSSHPort;
property SSHUser: String read FSSHUser write FSSHUser;
property SSHPassword: String read FSSHPassword write FSSHPassword;
property SSHPrivateKey: String read FSSHPrivateKey write FSSHPrivateKey;
property SSHLocalPort: Integer read FSSHLocalPort write FSSHLocalPort;
property SSHPlinkExe: String read FSSHPlinkExe write FSSHPlinkExe;
property SSLPrivateKey: String read FSSLPrivateKey write FSSLPrivateKey;
property SSLCertificate: String read FSSLCertificate write FSSLCertificate;
property SSLCACertificate: String read FSSLCACertificate write FSSLCACertificate;
end;
{ TMySQLConnection }
TMySQLLogCategory = (lcInfo, lcSQL, lcUserFiredSQL, lcError, lcDebug);
TMySQLLogEvent = procedure(Msg: String; Category: TMySQLLogCategory=lcInfo) of object;
TMySQLDatabaseEvent = procedure(Database: String) of object;
TMySQLQuery = class;
TMySQLConnection = class(TComponent)
private
FHandle: PMYSQL;
FActive: Boolean;
FConnectionStarted: Integer;
FServerStarted: Integer;
FParameters: TConnectionParameters;
FDatabase: String;
FLogPrefix: String;
FOnLog: TMySQLLogEvent;
FOnDatabaseChanged: TMySQLDatabaseEvent;
FOnDBObjectsCleared: TMySQLDatabaseEvent;
FRowsFound: Int64;
FRowsAffected: Int64;
FServerOS: String;
FServerVersionUntouched: String;
FRealHostname: String;
FLastQueryDuration, FLastQueryNetworkDuration: Cardinal;
FIsUnicode: Boolean;
FTableEngines: TStringList;
FTableEngineDefault: String;
FCollationTable: TMySQLQuery;
FCharsetTable: TMySQLQuery;
FInformationSchemaObjects: TStringList;
FDatabases: TDatabaseList;
FObjectNamesInSelectedDB: TStrings;
FPlinkProcInfo: TProcessInformation;
procedure SetActive(Value: Boolean);
procedure ClosePlink;
procedure SetDatabase(Value: String);
function GetThreadId: Cardinal;
function GetCharacterSet: String;
procedure SetCharacterSet(CharsetName: String);
function GetLastError: String;
function GetServerVersionStr: String;
function GetServerVersionInt: Integer;
function GetAllDatabases: TStringList;
function GetTableEngines: TStringList;
function GetCollationTable: TMySQLQuery;
function GetCollationList: TStringList;
function GetCharsetTable: TMySQLQuery;
function GetCharsetList: TStringList;
function GetInformationSchemaObjects: TStringList;
function GetConnectionUptime: Integer;
function GetServerUptime: Integer;
function DecodeAPIString(a: AnsiString): String;
procedure Log(Category: TMySQLLogCategory; Msg: String);
procedure ClearCache;
procedure SetObjectNamesInSelectedDB;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function Query(SQL: String; DoStoreResult: Boolean=False; LogCategory: TMySQLLogCategory=lcSQL): PMYSQL_RES;
function EscapeString(Text: String; ProcessJokerChars: Boolean=False): String;
function escChars(const Text: String; EscChar, Char1, Char2, Char3, Char4: Char): String;
class function QuoteIdent(Identifier: String): String;
function DeQuoteIdent(Identifier: String): String;
function ConvertServerVersion(Version: Integer): String;
function GetResults(SQL: String): TMySQLQuery;
function GetCol(SQL: String; Column: Integer=0): TStringList;
function GetVar(SQL: String; Column: Integer=0): String; overload;
function GetVar(SQL: String; Column: String): String; overload;
function Ping: Boolean;
function GetDBObjects(db: String; Refresh: Boolean=False): TDBObjectList;
function DbObjectsCached(db: String): Boolean;
function ParseDateTime(Str: String): TDateTime;
function GetKeyColumns(Columns: TTableColumnList; Keys: TTableKeyList): TStringList;
function ConnectionInfo: TStringList;
procedure ClearDbObjects(db: String);
procedure ClearAllDbObjects;
property Parameters: TConnectionParameters read FParameters write FParameters;
property ThreadId: Cardinal read GetThreadId;
property ConnectionUptime: Integer read GetConnectionUptime;
property ServerUptime: Integer read GetServerUptime;
property CharacterSet: String read GetCharacterSet write SetCharacterSet;
property LastError: String read GetLastError;
property ServerOS: String read FServerOS;
property ServerVersionUntouched: String read FServerVersionUntouched;
property ServerVersionStr: String read GetServerVersionStr;
property ServerVersionInt: Integer read GetServerVersionInt;
property RowsFound: Int64 read FRowsFound;
property RowsAffected: Int64 read FRowsAffected;
property LastQueryDuration: Cardinal read FLastQueryDuration;
property LastQueryNetworkDuration: Cardinal read FLastQueryNetworkDuration;
property IsUnicode: Boolean read FIsUnicode;
property AllDatabases: TStringList read GetAllDatabases;
property TableEngines: TStringList read GetTableEngines;
property TableEngineDefault: String read FTableEngineDefault;
property CollationTable: TMySQLQuery read GetCollationTable;
property CollationList: TStringList read GetCollationList;
property CharsetTable: TMySQLQuery read GetCharsetTable;
property CharsetList: TStringList read GetCharsetList;
property InformationSchemaObjects: TStringList read GetInformationSchemaObjects;
property ObjectNamesInSelectedDB: TStrings read FObjectNamesInSelectedDB write FObjectNamesInSelectedDB;
published
property Active: Boolean read FActive write SetActive default False;
property Database: String read FDatabase write SetDatabase;
property LogPrefix: String read FLogPrefix write FLogPrefix;
// Events
property OnLog: TMySQLLogEvent read FOnLog write FOnLog;
property OnDatabaseChanged: TMySQLDatabaseEvent read FOnDatabaseChanged write FOnDatabaseChanged;
property OnDBObjectsCleared: TMySQLDatabaseEvent read FOnDBObjectsCleared write FOnDBObjectsCleared;
end;
{ TMySQLQuery }
TMySQLQuery = class(TComponent)
private
FSQL: String;
FConnection: TMySQLConnection;
FRecNo,
FRecordCount: Int64;
FColumnNames: TStringList;
FColumnOrgNames: TStringList;
FColumnTypes: Array of TDatatype;
FColumnLengths: TIntegerDynArray;
FColumnFlags: TCardinalDynArray;
FResultList: Array of PMYSQL_RES;
FCurrentResults: PMYSQL_RES;
FCurrentRow: PMYSQL_ROW;
FCurrentUpdateRow: TRowData;
FEof: Boolean;
FLogCategory: TMySQLLogCategory;
FStoreResult: Boolean;
FColumns: TTableColumnList;
FKeys: TTableKeyList;
FForeignKeys: TForeignKeyList;
FEditingPrepared: Boolean;
FUpdateData: TUpdateData;
procedure SetSQL(Value: String);
procedure SetRecNo(Value: Int64);
procedure SetColumnOrgNames(Value: TStringList);
procedure CreateUpdateRow;
function GetKeyColumns: TStringList;
function GetWhereClause: String;
function ColAttributes(Column: Integer): TTableColumn;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Execute(AddResult: Boolean=False);
procedure First;
procedure Next;
function ColumnCount: Integer;
function Col(Column: Integer; IgnoreErrors: Boolean=False): String; overload;
function Col(ColumnName: String; IgnoreErrors: Boolean=False): String; overload;
function BinColAsHex(Column: Integer; IgnoreErrors: Boolean=False): String;
function DataType(Column: Integer): TDataType;
function MaxLength(Column: Integer): Int64;
function ValueList(Column: Integer): TStringList;
function ColExists(Column: String): Boolean;
function ColIsPrimaryKeyPart(Column: Integer): Boolean;
function ColIsUniqueKeyPart(Column: Integer): Boolean;
function ColIsKeyPart(Column: Integer): Boolean;
function IsNull(Column: Integer): Boolean; overload;
function IsNull(Column: String): Boolean; overload;
function HasResult: Boolean;
procedure CheckEditable;
function DeleteRow: Boolean;
function InsertRow: Cardinal;
procedure SetCol(Column: Integer; NewText: String; Null: Boolean);
function EnsureFullRow: Boolean;
function HasFullData: Boolean;
function Modified(Column: Integer): Boolean; overload;
function Modified: Boolean; overload;
function Inserted: Boolean;
function SaveModifications: Boolean;
function DatabaseName: String;
function TableName: String;
function QuotedDbAndTableName: String;
procedure DiscardModifications;
procedure PrepareEditing;
property RecNo: Int64 read FRecNo write SetRecNo;
property Eof: Boolean read FEof;
property RecordCount: Int64 read FRecordCount;
property ColumnNames: TStringList read FColumnNames;
property LogCategory: TMySQLLogCategory read FLogCategory write FLogCategory;
property StoreResult: Boolean read FStoreResult write FStoreResult;
property ColumnOrgNames: TStringList read FColumnOrgNames write SetColumnOrgNames;
published
property SQL: String read FSQL write SetSQL;
property Connection: TMySQLConnection read FConnection write FConnection;
end;
implementation
uses helpers;
{ TConnectionParameters }
constructor TConnectionParameters.Create;
begin
FNetType := ntTCPIP;
FHostname := DEFAULT_HOST;
FUsername := DEFAULT_USER;
FPassword := '';
FPort := DEFAULT_PORT;
FSSHPort := DEFAULT_SSHPORT;
FSSHLocalPort := FPort + 1;
FSSLPrivateKey := '';
FSSLCertificate := '';
FSSLCACertificate := '';
FStartupScriptFilename := '';
FOptions := [opCompress, opLocalFiles, opInteractive, opProtocol41, opMultiStatements];
end;
{ TMySQLConnection }
constructor TMySQLConnection.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FParameters := TConnectionParameters.Create;
FRowsFound := 0;
FRowsAffected := 0;
FConnectionStarted := 0;
FLastQueryDuration := 0;
FLastQueryNetworkDuration := 0;
FLogPrefix := '';
FIsUnicode := False;
FDatabases := TDatabaseList.Create(True);
end;
destructor TMySQLConnection.Destroy;
begin
if Active then Active := False;
FOnDBObjectsCleared := nil;
ClearCache;
inherited Destroy;
end;
{**
(Dis-)Connect to/from server
}
procedure TMySQLConnection.SetActive( Value: Boolean );
var
Connected: PMYSQL;
ClientFlags, FinalPort: Integer;
Error, tmpdb, FinalHost, FinalSocket, PlinkCmd: String;
SSLResult: Byte;
UsingPass, Protocol, CurCharset: String;
StartupInfo: TStartupInfo;
ExitCode: LongWord;
begin
if Value and (FHandle = nil) then begin
// Get handle
FHandle := mysql_init(nil);
// Prepare connection
case FParameters.NetType of
ntTCPIP: Protocol := 'TCP/IP';
ntNamedPipe: Protocol := 'named pipe';
ntSSHtunnel: Protocol := 'SSH tunnel';
end;
if FParameters.Password <> '' then UsingPass := 'Yes' else UsingPass := 'No';
Log(lcInfo, 'Connecting to '+FParameters.Hostname+' via '+Protocol+
', username '+FParameters.Username+
', using password: '+UsingPass+' ...');
// Prepare special stuff for SSL and SSH tunnel
FinalHost := FParameters.Hostname;
FinalSocket := '';
FinalPort := FParameters.Port;
case FParameters.NetType of
ntTCPIP: begin
if (FParameters.SSLPrivateKey <> '') and
(FParameters.SSLCertificate <> '') and
(FParameters.SSLCACertificate <> '') then begin
FParameters.Options := FParameters.Options + [opSSL];
{ TODO : Use Cipher and CAPath parameters }
SSLResult := mysql_ssl_set(
FHandle,
PansiChar(AnsiString(FParameters.SSLPrivateKey)),
PansiChar(AnsiString(FParameters.SSLCertificate)),
PansiChar(AnsiString(FParameters.SSLCACertificate)),
{PansiChar(AnsiString(FParameters.CApath))}nil,
{PansiChar(AnsiString(FParameters.Cipher))}nil);
if SSLresult <> 0 then
raise EDatabaseError.CreateFmt('Could not connect using SSL (Error %d)', [SSLresult]);
end;
end;
ntNamedPipe: begin
FinalHost := '.';
FinalSocket := FParameters.Hostname;
end;
ntSSHtunnel: begin
// Build plink.exe command line
// plink bob@domain.com -pw myPassw0rd1 -P 22 -i "keyfile.pem" -L 55555:localhost:3306
PlinkCmd := FParameters.SSHPlinkExe + ' ';
if FParameters.SSHUser <> '' then
PlinkCmd := PlinkCmd + FParameters.SSHUser + '@';
if FParameters.SSHHost <> '' then
PlinkCmd := PlinkCmd + FParameters.SSHHost
else
PlinkCmd := PlinkCmd + FParameters.Hostname;
if FParameters.SSHPassword <> '' then
PlinkCmd := PlinkCmd + ' -pw ' + FParameters.SSHPassword;
if FParameters.SSHPort > 0 then
PlinkCmd := PlinkCmd + ' -P ' + IntToStr(FParameters.SSHPort);
if FParameters.SSHPrivateKey <> '' then
PlinkCmd := PlinkCmd + ' -i "' + FParameters.SSHPrivateKey + '"';
PlinkCmd := PlinkCmd + ' -L ' + IntToStr(FParameters.SSHLocalPort) + ':' + FParameters.Hostname + ':' + IntToStr(FParameters.Port);
Log(lcInfo, 'Attempt to create plink.exe process ...');
// Create plink.exe process
FillChar(FPlinkProcInfo, SizeOf(TProcessInformation), 0);
FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
StartupInfo.cb := SizeOf(TStartupInfo);
if CreateProcess(nil, PChar(PlinkCmd), nil, nil, false,
CREATE_DEFAULT_ERROR_MODE + NORMAL_PRIORITY_CLASS + CREATE_NO_WINDOW,
nil, nil, StartupInfo, FPlinkProcInfo) then begin
WaitForSingleObject(FPlinkProcInfo.hProcess, 1000);
GetExitCodeProcess(FPlinkProcInfo.hProcess, ExitCode);
if ExitCode <> STILL_ACTIVE then
raise EDatabaseError.Create('PLink exited unexpected. Command line was:'+CRLF+PlinkCmd);
end else begin
ClosePlink;
raise EDatabaseError.Create('Couldn''t execute PLink: '+CRLF+PlinkCmd);
end;
FinalHost := 'localhost';
FinalPort := FParameters.SSHLocalPort;
end;
end;
// Gather client options
ClientFlags := 0;
if opRememberOptions in FParameters.Options then ClientFlags := ClientFlags or CLIENT_REMEMBER_OPTIONS;
if opLongPassword in FParameters.Options then ClientFlags := ClientFlags or CLIENT_LONG_PASSWORD;
if opFoundRows in FParameters.Options then ClientFlags := ClientFlags or CLIENT_FOUND_ROWS;
if opLongFlag in FParameters.Options then ClientFlags := ClientFlags or CLIENT_LONG_FLAG;
if opConnectWithDb in FParameters.Options then ClientFlags := ClientFlags or CLIENT_CONNECT_WITH_DB;
if opNoSchema in FParameters.Options then ClientFlags := ClientFlags or CLIENT_NO_SCHEMA;
if opCompress in FParameters.Options then ClientFlags := ClientFlags or CLIENT_COMPRESS;
if opODBC in FParameters.Options then ClientFlags := ClientFlags or CLIENT_ODBC;
if opLocalFiles in FParameters.Options then ClientFlags := ClientFlags or CLIENT_LOCAL_FILES;
if opIgnoreSpace in FParameters.Options then ClientFlags := ClientFlags or CLIENT_IGNORE_SPACE;
if opProtocol41 in FParameters.Options then ClientFlags := ClientFlags or CLIENT_PROTOCOL_41;
if opInteractive in FParameters.Options then ClientFlags := ClientFlags or CLIENT_INTERACTIVE;
if opSSL in FParameters.Options then ClientFlags := ClientFlags or CLIENT_SSL;
if opIgnoreSigpipe in FParameters.Options then ClientFlags := ClientFlags or CLIENT_IGNORE_SIGPIPE;
if opTransactions in FParameters.Options then ClientFlags := ClientFlags or CLIENT_TRANSACTIONS;
if opReserved in FParameters.Options then ClientFlags := ClientFlags or CLIENT_RESERVED;
if opSecureConnection in FParameters.Options then ClientFlags := ClientFlags or CLIENT_SECURE_CONNECTION;
if opMultiStatements in FParameters.Options then ClientFlags := ClientFlags or CLIENT_MULTI_STATEMENTS;
if opMultiResults in FParameters.Options then ClientFlags := ClientFlags or CLIENT_MULTI_RESULTS;
if opRememberOptions in FParameters.Options then ClientFlags := ClientFlags or CLIENT_REMEMBER_OPTIONS;
Connected := mysql_real_connect(
FHandle,
PAnsiChar(Utf8Encode(FinalHost)),
PAnsiChar(Utf8Encode(FParameters.Username)),
PAnsiChar(Utf8Encode(FParameters.Password)),
nil,
FinalPort,
PAnsiChar(Utf8Encode(FinalSocket)),
ClientFlags
);
if Connected = nil then begin
Error := LastError;
Log(lcError, Error);
FConnectionStarted := 0;
FHandle := nil;
ClosePlink;
raise EDatabaseError.Create(Error);
end else begin
Log(lcInfo, 'Connected. Thread-ID: '+IntToStr(ThreadId));
FActive := True;
CharacterSet := 'utf8';
CurCharset := CharacterSet;
Log(lcInfo, 'Characterset: '+CurCharset);
FIsUnicode := CurCharset = 'utf8';
FConnectionStarted := GetTickCount div 1000;
FServerStarted := FConnectionStarted - StrToIntDef(GetVar('SHOW STATUS LIKE ''Uptime''', 1), 1);
FServerVersionUntouched := DecodeAPIString(mysql_get_server_info(FHandle));
FServerOS := GetVar('SHOW VARIABLES LIKE ' + esc('version_compile_os'), 1);
FRealHostname := GetVar('SHOW VARIABLES LIKE ' + esc('hostname'), 1);;
if FDatabase <> '' then begin
tmpdb := FDatabase;
FDatabase := '';
try
Database := tmpdb;
except
// Trigger OnDatabaseChange event for <no db> if wanted db is not available
FDatabase := tmpdb;
Database := '';
end;
end;
end;
end
else if (not Value) and (FHandle <> nil) then begin
mysql_close(FHandle);
FActive := False;
FConnectionStarted := 0;
FHandle := nil;
ClosePlink;
Log(lcInfo, 'Connection to '+FParameters.Hostname+' closed at '+DateTimeToStr(Now));
end;
end;
function TMySQLConnection.Ping: Boolean;
begin
if FActive and ((FHandle=nil) or (mysql_ping(FHandle) <> 0)) then
Active := False;
Result := FActive;
end;
procedure TMySQLConnection.ClosePlink;
begin
if FPlinkProcInfo.hProcess <> 0 then begin
Log(lcInfo, 'Closing plink.exe process #'+IntToStr(FPlinkProcInfo.dwProcessId)+' ...');
TerminateProcess(FPlinkProcInfo.hProcess, 0);
CloseHandle(FPlinkProcInfo.hProcess);
end;
end;
{**
Executes a query
}
function TMySQLConnection.Query(SQL: String; DoStoreResult: Boolean=False; LogCategory: TMySQLLogCategory=lcSQL): PMYSQL_RES;
var
querystatus, i: Integer;
NativeSQL: AnsiString;
TimerStart: Cardinal;
NextResult: PMYSQL_RES;
begin
if not Ping then
Active := True;
Log(LogCategory, SQL);
if IsUnicode then
NativeSQL := UTF8Encode(SQL)
else
NativeSQL := AnsiString(SQL);
TimerStart := GetTickCount;
querystatus := mysql_real_query(FHandle, PAnsiChar(NativeSQL), Length(NativeSQL));
FLastQueryDuration := GetTickCount - TimerStart;
FLastQueryNetworkDuration := 0;
if querystatus <> 0 then begin
// Most errors will show up here, some others slightly later, after mysql_store_result()
Log(lcError, GetLastError);
raise EDatabaseError.Create(GetLastError);
end else begin
// We must call mysql_store_result() + mysql_free_result() to unblock the connection
// See: http://dev.mysql.com/doc/refman/5.0/en/mysql-store-result.html
FRowsAffected := mysql_affected_rows(FHandle);
TimerStart := GetTickCount;
Result := mysql_store_result(FHandle);
FLastQueryNetworkDuration := GetTickCount - TimerStart;
if (Result = nil) and (FRowsAffected = -1) then begin
// Indicates a late error, e.g. triggered by mysql_store_result(), after selecting a stored
// function with invalid SQL body. Also SHOW TABLE STATUS on older servers.
Log(lcError, GetLastError);
raise EDatabaseError.Create(GetLastError);
end;
if Result <> nil then begin
FRowsFound := mysql_num_rows(Result);
FRowsAffected := 0;
Log(lcDebug, IntToStr(RowsFound)+' rows found.');
if not DoStoreResult then begin
mysql_free_result(Result);
Result := nil;
end;
// No support for real multi results yet, throw them away, so mysql_ping() does not crash on the *next* query.
i := 1;
while mysql_next_result(FHandle) = 0 do begin
Inc(i);
Log(lcDebug, 'Storing and freeing result #'+IntToStr(i)+' from multiple result set ...');
NextResult := mysql_store_result(FHandle);
if NextResult <> nil then
mysql_free_result(NextResult);
end;
end else begin
// Query did not return a result
FRowsFound := 0;
Log(lcDebug, IntToStr(RowsAffected)+' rows affected.');
if UpperCase(Copy(SQL, 1, 3)) = 'USE' then begin
FDatabase := Trim(Copy(SQL, 4, Length(SQL)-3));
FDatabase := DeQuoteIdent(FDatabase);
Log(lcDebug, 'Database "'+FDatabase+'" selected');
if Assigned(FOnDatabaseChanged) then
FOnDatabaseChanged(Database);
end;
end;
end;
end;
{**
Set "Database" property and select that db if connected
}
procedure TMySQLConnection.SetDatabase(Value: String);
begin
if Value <> FDatabase then begin
if Value = '' then begin
FDatabase := Value;
if Assigned(FOnDatabaseChanged) then
FOnDatabaseChanged(Value);
end else
Query('USE '+QuoteIdent(Value), False);
SetObjectNamesInSelectedDB;
end;
end;
{**
Return current thread id
}
function TMySQLConnection.GetThreadId: Cardinal;
begin
Result := mysql_thread_id(FHandle);
end;
{**
Return currently used character set
}
function TMySQLConnection.GetCharacterSet: String;
begin
Result := DecodeAPIString(mysql_character_set_name(FHandle));
end;
{**
Switch character set
}
procedure TMySQLConnection.SetCharacterSet(CharsetName: String);
begin
mysql_set_character_set(FHandle, PAnsiChar(Utf8Encode(CharsetName)));
end;
{**
Return the last error nicely formatted
}
function TMySQLConnection.GetLastError: String;
var
Msg, Additional: String;
rx: TRegExpr;
begin
Msg := DecodeAPIString(mysql_error(FHandle));
// Find "(errno: 123)" in message and add more meaningful message from perror.exe
rx := TRegExpr.Create;
rx.Expression := '.+\(errno\:\s+(\d+)\)';
if rx.Exec(Msg) then begin
Additional := MySQLErrorCodes.Values[rx.Match[1]];
if Additional <> '' then
Msg := Msg + CRLF + CRLF + Additional;
end;
rx.Free;
Result := Format('SQL Error (%d): %s', [mysql_errno(FHandle), Msg]);
end;
{**
Get version string as normalized integer
"5.1.12-beta-community-123" => 50112
}
function TMySQLConnection.GetServerVersionInt: Integer;
var
i, dots: Byte;
v1, v2, v3: String;
begin
Result := -1;
dots := 0;
v1 := '';
v2 := '';
v3 := '';
for i:=1 to Length(FServerVersionUntouched) do begin
if FServerVersionUntouched[i] = '.' then begin
inc(dots);
// We expect exactly 2 dots.
if dots > 2 then
break;
end else if CharInSet(FServerVersionUntouched[i], ['0'..'9']) then begin
if dots = 0 then
v1 := v1 + FServerVersionUntouched[i]
else if dots = 1 then
v2 := v2 + FServerVersionUntouched[i]
else if dots = 2 then
v3 := v3 + FServerVersionUntouched[i];
end else // Don't include potential numbers of trailing string
break;
end;
// Concat tokens
if (Length(v1)>0) and (Length(v2)>0) and (Length(v3)>0) then begin
Result := StrToIntDef(v1, 0) *10000 +
StrToIntDef(v2, 0) *100 +
StrToIntDef(v3, 0);
end;
end;
function TMySQLConnection.GetServerVersionStr: String;
begin
Result := ConvertServerVersion(ServerVersionInt);
end;
function TMySQLConnection.GetAllDatabases: TStringList;
begin
if FParameters.AllDatabases <> '' then begin
Result := TStringList.Create;
Result.Delimiter := ';';
Result.StrictDelimiter := True;
Result.DelimitedText := FParameters.AllDatabases;
end else
Result := GetCol('SHOW DATABASES');
end;
{**
Convert integer version to real version string
}
function TMySQLConnection.ConvertServerVersion(Version: Integer): String;
var
v : String;
v1, v2 : Byte;
begin
v := IntToStr( Version );
v1 := StrToIntDef( v[2]+v[3], 0 );
v2 := StrToIntDef( v[4]+v[5], 0 );
Result := v[1] + '.' + IntToStr(v1) + '.' + IntToStr(v2);
end;
function TMySQLConnection.GetResults(SQL: String): TMySQLQuery;
begin
Result := TMySQLQuery.Create(Self);
Result.Connection := Self;
Result.SQL := SQL;
try
Result.Execute;
except
FreeAndNil(Result);
Raise;
end;
end;
{**
Call log event if assigned to object
}
procedure TMySQLConnection.Log(Category: TMySQLLogCategory; Msg: String);
begin
if Assigned(FOnLog) then
FOnLog(FLogPrefix+Msg, Category);
end;
{**
Escapes a string for usage in SQL queries
- single-backslashes which represent normal parts of the text and not escape-sequences
- characters which MySQL doesn't strictly care about, but which might confuse editors etc.
- single and double quotes in a text string
- joker-chars for LIKE-comparisons
Finally, surround the text by single quotes.
@param string Text to escape
@param boolean Escape text so it can be used in a LIKE-comparison
@return string
}
function TMySQLConnection.EscapeString(Text: String; ProcessJokerChars: Boolean=false): String;
var
c1, c2, c3, c4, EscChar: Char;
begin
c1 := '''';
c2 := '\';
c3 := '%';
c4 := '_';
EscChar := '\';
if not ProcessJokerChars then begin
// Do not escape joker-chars which are used in a LIKE-clause
c4 := '''';
c3 := '''';
end;
Result := escChars(Text, EscChar, c1, c2, c3, c4);
// Remove characters that SynEdit chokes on, so that
// the SQL file can be non-corruptedly loaded again.
c1 := #13;
c2 := #10;
c3 := #0;
c4 := #0;
// TODO: SynEdit also chokes on Char($2028) and possibly Char($2029).
Result := escChars(Result, EscChar, c1, c2, c3, c4);
if not ProcessJokerChars then begin
// Add surrounding single quotes only for non-LIKE-values
// because in all cases we're using ProcessLIKEChars we
// need to add leading and/or trailing joker-chars by hand
// without being escaped
Result := Char(#39) + Result + Char(#39);
end;
end;
{***
Attempt to do string replacement faster than StringReplace
}
function TMySQLConnection.escChars(const Text: String; EscChar, Char1, Char2, Char3, Char4: Char): String;
const
// Attempt to match whatever the CPU cache will hold.
block: Cardinal = 65536;
var
bstart, bend, matches, i: Cardinal;
// These could be bumped to uint64 if necessary.
len, respos: Cardinal;
next: Char;
begin
len := Length(Text);
Result := '';
bend := 0;
respos := 0;
repeat
bstart := bend + 1;
bend := bstart + block - 1;
if bend > len then bend := len;
matches := 0;
for i := bstart to bend do if
(Text[i] = Char1) or
(Text[i] = Char2) or
(Text[i] = Char3) or
(Text[i] = Char4)
then Inc(matches);
SetLength(Result, bend + 1 - bstart + matches + respos);
for i := bstart to bend do begin
next := Text[i];
if
(next = Char1) or
(next = Char2) or
(next = Char3) or
(next = Char4)
then begin
Inc(respos);
Result[respos] := EscChar;
// Special values for MySQL escape.
if next = #13 then next := 'r';
if next = #10 then next := 'n';
if next = #0 then next := '0';
end;
Inc(respos);
Result[respos] := next;
end;
until bend = len;
end;
{**
Add backticks to identifier
Todo: Support ANSI style
}
class function TMySQLConnection.QuoteIdent(Identifier: String): String;
begin
Result := Identifier;
Result := StringReplace(Result, '`', '``', [rfReplaceAll]);
Result := StringReplace(Result, '.', '`.`', [rfReplaceAll]);
Result := '`' + Result + '`';
end;
function TMySQLConnection.DeQuoteIdent(Identifier: String): String;
begin
Result := Identifier;
if (Result[1] = '`') and (Result[Length(Identifier)] = '`') then
Result := Copy(Result, 2, Length(Result)-2);
end;
function TMySQLConnection.GetCol(SQL: String; Column: Integer=0): TStringList;
var
Results: TMySQLQuery;
begin
Results := GetResults(SQL);
Result := TStringList.Create;
if Results.RecordCount > 0 then while not Results.Eof do begin
Result.Add(Results.Col(Column));
Results.Next;
end;
FreeAndNil(Results);
end;
{**
Get single cell value via SQL query, identified by column number
}
function TMySQLConnection.GetVar(SQL: String; Column: Integer=0): String;
var
Results: TMySQLQuery;
begin
Results := GetResults(SQL);
if Results.RecordCount > 0 then
Result := Results.Col(Column)
else
Result := '';
FreeAndNil(Results);
end;
{**
Get single cell value via SQL query, identified by column name
}
function TMySQLConnection.GetVar(SQL: String; Column: String): String;
var
Results: TMySQLQuery;
begin
Results := GetResults(SQL);
if Results.RecordCount > 0 then
Result := Results.Col(Column)
else
Result := '';
FreeAndNil(Results);
end;
function TMySQLConnection.GetTableEngines: TStringList;
var
ShowEngines, HaveEngines: TMySQLQuery;
engineName, engineSupport: String;
PossibleEngines: TStringList;
begin
if not Assigned(FTableEngines) then begin
FTableEngines := TStringList.Create;
try
ShowEngines := GetResults('SHOW ENGINES');
while not ShowEngines.Eof do begin
engineName := ShowEngines.Col('Engine');
engineSupport := LowerCase(ShowEngines.Col('Support'));
// Add to dropdown if supported
if engineSupport <> 'no' then
FTableEngines.Add(engineName);
// Check if this is the default engine
if engineSupport = 'default' then
FTableEngineDefault := engineName;
ShowEngines.Next;
end;
except
// Ignore errors on old servers and try a fallback:
// Manually fetch available engine types by analysing have_* options
// This is for servers below 4.1 or when the SHOW ENGINES statement has
// failed for some other reason
HaveEngines := GetResults('SHOW VARIABLES LIKE ''have%''');
// Add default engines which will not show in a have_* variable:
FTableEngines.CommaText := 'MyISAM,MRG_MyISAM,HEAP';
FTableEngineDefault := 'MyISAM';
// Possible other engines:
PossibleEngines := TStringList.Create;
PossibleEngines.CommaText := 'ARCHIVE,BDB,BLACKHOLE,CSV,EXAMPLE,FEDERATED,INNODB,ISAM';
while not HaveEngines.Eof do begin
engineName := copy(HaveEngines.Col(0), 6, Length(HaveEngines.Col(0)));
// Strip additional "_engine" suffix, fx from "have_blackhole_engine"
if Pos('_', engineName) > 0 then
engineName := copy(engineName, 0, Pos('_', engineName)-1);
engineName := UpperCase(engineName);
// Add engine to list if it's a) in HaveEngineList and b) activated
if (PossibleEngines.IndexOf(engineName) > -1)
and (LowerCase(HaveEngines.Col(1)) = 'yes') then
FTableEngines.Add(engineName);
HaveEngines.Next;
end;
end;
end;
Result := FTableEngines;
end;
function TMySQLConnection.GetCollationTable: TMySQLQuery;
begin
if (not Assigned(FCollationTable)) and (ServerVersionInt >= 40100) then
FCollationTable := GetResults('SHOW COLLATION');
if Assigned(FCollationTable) then
FCollationTable.First;
Result := FCollationTable;
end;
function TMySQLConnection.GetCollationList: TStringList;
var
c: TMySQLQuery;
begin
c := CollationTable;
Result := TStringList.Create;
if Assigned(c) then while not c.Eof do begin
Result.Add(c.Col('Collation'));
c.Next;
end;
end;
function TMySQLConnection.GetCharsetTable: TMySQLQuery;
begin
if (not Assigned(FCharsetTable)) and (ServerVersionInt >= 40100) then
FCharsetTable := GetResults('SHOW CHARSET');
Result := FCharsetTable;
end;
function TMySQLConnection.GetCharsetList: TStringList;
var
c: TMySQLQuery;
begin
c := CharsetTable;
Result := TStringList.Create;
if Assigned(c) then begin
c.First;
while not c.Eof do begin
Result.Add(c.Col('Description') + ' (' + c.Col('Charset') + ')');
c.Next;
end;
end;
end;
function TMySQLConnection.GetInformationSchemaObjects: TStringList;
begin
if not Assigned(FInformationSchemaObjects) then try
FInformationSchemaObjects := GetCol('SHOW TABLES FROM '+QuoteIdent('information_schema'));
except
// Gracefully return an empty list on old servers
FInformationSchemaObjects := TStringList.Create;
end;
Result := FInformationSchemaObjects;
end;
function TMySQLConnection.GetConnectionUptime: Integer;
begin
// Return seconds since last connect
if not FActive then
Result := 0
else
Result := Integer(GetTickCount div 1000) - FConnectionStarted;
end;
function TMySQLConnection.GetServerUptime: Integer;
begin
// Return server uptime in seconds
Result := Integer(GetTickCount div 1000) - FServerStarted;
end;
procedure TMySQLConnection.ClearCache;
begin
// Free cached lists and results. Called when the connection was closed and/or destroyed
FreeAndNil(FCollationTable);
FreeAndNil(FCharsetTable);
FreeAndNil(FTableEngines);
FreeAndNil(FInformationSchemaObjects);
ClearAllDbObjects;
FTableEngineDefault := '';
end;
procedure TMySQLConnection.ClearDbObjects(db: String);
var
i: Integer;
begin
// Free cached database object list
for i:=0 to FDatabases.Count-1 do begin
if FDatabases[i].Database = db then begin
FDatabases.Delete(i);
if Assigned(FOnDBObjectsCleared) then
FOnDBObjectsCleared(db);
break;
end;
end;
end;
procedure TMySQLConnection.ClearAllDbObjects;
var
i: Integer;
begin
for i:=FDatabases.Count-1 downto 0 do
ClearDbObjects(FDatabases[i].Database);
end;
function TMySQLConnection.DbObjectsCached(db: String): Boolean;
var
i: Integer;
begin
// Check if a table list is stored in cache
Result := False;
for i:=0 to FDatabases.Count-1 do begin
if FDatabases[i].Database = db then begin
Result := True;
break;
end;
end;
end;
function TMySQLConnection.ParseDateTime(Str: String): TDateTime;
var
rx: TRegExpr;
begin
// Parse a MySQL date / time string value into a TDateTime
Result := 0;
rx := TRegExpr.Create;
rx.Expression := '^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})\:(\d{2})\:(\d{2})$';
if rx.Exec(Str) then try
Result := EncodeDateTime(
StrToIntDef(rx.Match[1], 0),
StrToIntDef(rx.Match[2], 1),
StrToIntDef(rx.Match[3], 1),
StrToIntDef(rx.Match[4], 0),
StrToIntDef(rx.Match[5], 0),
StrToIntDef(rx.Match[6], 0),
0 // milliseconds, unused
);
except
Result := 0;
end;
end;
function TMySQLConnection.GetDbObjects(db: String; Refresh: Boolean=False): TDBObjectList;
var
obj: TDBObject;
Results: TMySQLQuery;
rx: TRegExpr;
i: Integer;
begin
// Cache and return a db's table list
if Refresh then
ClearDbObjects(db);
// Find list in cache
Result := nil;
for i:=0 to FDatabases.Count-1 do begin
if FDatabases[i].Database = db then begin
Result := FDatabases[i];
break;
end;
end;
if not Assigned(Result) then begin
Result := TDBObjectList.Create(TDBObjectComparer.Create);
Result.FLastUpdate := 0;
Result.FDataSize := 0;
Result.FDatabase := db;
Results := nil;
rx := TRegExpr.Create;
rx.ModifierI := True;
// Tables and views
try
Results := GetResults('SHOW TABLE STATUS FROM '+QuoteIdent(db));
except
end;
if Assigned(Results) then begin
while not Results.Eof do begin
obj := TDBObject.Create;
Result.Add(obj);
obj.Name := Results.Col('Name');
obj.Database := db;
obj.Rows := StrToInt64Def(Results.Col('Rows'), -1);
if Results.IsNull('Data_length') or Results.IsNull('Index_length') then
Obj.Size := -1
else begin
Obj.Size := StrToInt64Def(Results.Col('Data_length'), 0) + StrToInt64Def(Results.Col('Index_length'), 0);
Inc(Result.FDataSize, Obj.Size);
end;
Obj.NodeType := lntTable;
if Results.IsNull(1) and Results.IsNull(2) then // Engine column is NULL for views
Obj.NodeType := lntView;
Obj.Created := ParseDateTime(Results.Col('Create_time'));
Obj.Updated := ParseDateTime(Results.Col('Update_time'));
if Results.ColExists('Type') then
Obj.Engine := Results.Col('Type')
else
Obj.Engine := Results.Col('Engine');
Obj.Comment := Results.Col('Comment');
// Sanitize comment from automatically appendage
rx.Expression := '(;\s*)?InnoDB\s*free\:.*$';
Obj.Comment := rx.Replace(Obj.Comment, '', False);
Obj.Version := StrToInt64Def(Results.Col('Version', True), -1);
Obj.AutoInc := StrToInt64Def(Results.Col('Auto_increment'), -1);
Obj.RowFormat := Results.Col('Row_format');
Obj.AvgRowLen := StrToInt64Def(Results.Col('Avg_row_length'), -1);
Obj.MaxDataLen := StrToInt64Def(Results.Col('Max_data_length'), -1);
Obj.IndexLen := StrToInt64Def(Results.Col('Index_length'), -1);
Obj.DataLen := StrToInt64Def(Results.Col('Data_length'), -1);
Obj.DataFree := StrToInt64Def(Results.Col('Data_free'), -1);
Obj.LastChecked := ParseDateTime(Results.Col('Check_time'));
Obj.Collation := Results.Col('Collation', True);
Obj.CheckSum := StrToInt64Def(Results.Col('Checksum', True), -1);
Obj.CreateOptions := Results.Col('Create_options');
Results.Next;
end;
FreeAndNil(Results);
end;
// Stored functions
if ServerVersionInt >= 50000 then try
Results := GetResults('SHOW FUNCTION STATUS WHERE '+QuoteIdent('Db')+'='+EscapeString(db));
except
end;
if Assigned(Results) then begin
while not Results.Eof do begin
obj := TDBObject.Create;
Result.Add(obj);
obj.Name := Results.Col('Name');
obj.Database := db;
obj.Rows := -1;
Obj.Size := -1;
Obj.NodeType := lntFunction;
Obj.Created := ParseDateTime(Results.Col('Created'));
Obj.Updated := ParseDateTime(Results.Col('Modified'));
Obj.Engine := '';
Obj.Comment := Results.Col('Comment');
Obj.Version := -1;
Obj.AutoInc := -1;
Obj.RowFormat := '';
Obj.AvgRowLen := -1;
Obj.MaxDataLen := -1;
Obj.IndexLen := -1;
Obj.DataLen := -1;
Obj.DataFree := -1;
Obj.LastChecked := 0;
Obj.Collation := '';
Obj.CheckSum := -1;
Obj.CreateOptions := '';
Results.Next;
end;
FreeAndNil(Results);
end;
// Stored procedures
if ServerVersionInt >= 50000 then try
Results := GetResults('SHOW PROCEDURE STATUS WHERE '+QuoteIdent('Db')+'='+EscapeString(db));
except
end;
if Assigned(Results) then begin
while not Results.Eof do begin
obj := TDBObject.Create;
Result.Add(obj);
obj.Name := Results.Col('Name');
obj.Database := db;
obj.Rows := -1;
Obj.Size := -1;
Obj.NodeType := lntProcedure;
Obj.Created := ParseDateTime(Results.Col('Created'));
Obj.Updated := ParseDateTime(Results.Col('Modified'));
Obj.Engine := '';
Obj.Comment := Results.Col('Comment');
Obj.Version := -1;
Obj.AutoInc := -1;
Obj.RowFormat := '';
Obj.AvgRowLen := -1;
Obj.MaxDataLen := -1;
Obj.IndexLen := -1;
Obj.DataLen := -1;
Obj.DataFree := -1;
Obj.LastChecked := 0;
Obj.Collation := '';
Obj.CheckSum := -1;
Obj.CreateOptions := '';
Results.Next;
end;
FreeAndNil(Results);
end;
// Triggers
if ServerVersionInt >= 50010 then try
Results := GetResults('SHOW TRIGGERS FROM '+QuoteIdent(db));
except
end;
if Assigned(Results) then begin
while not Results.Eof do begin
obj := TDBObject.Create;
Result.Add(obj);
obj.Name := Results.Col('Trigger');
obj.Database := db;
obj.Rows := -1;
Obj.Size := -1;
Obj.NodeType := lntTrigger;
Obj.Created := ParseDateTime(Results.Col('Created'));
Obj.Updated := 0;
Obj.Engine := '';
Obj.Comment := Results.Col('Timing')+' '+Results.Col('Event')+' in table '+QuoteIdent(Results.Col('Table'));
Obj.Version := -1;
Obj.AutoInc := -1;
Obj.RowFormat := '';
Obj.AvgRowLen := -1;
Obj.MaxDataLen := -1;
Obj.IndexLen := -1;
Obj.DataLen := -1;
Obj.DataFree := -1;
Obj.LastChecked := 0;
Obj.Collation := '';
Obj.CheckSum := -1;
Obj.CreateOptions := '';
Results.Next;
end;
FreeAndNil(Results);
end;
// Events
if ServerVersionInt >= 50100 then try
Results := GetResults('SHOW EVENTS FROM '+QuoteIdent(db));
except
end;
if Assigned(Results) then begin
while not Results.Eof do begin
if Results.Col('Db') = db then begin
Obj := TDBObject.Create;
Result.Add(obj);
Obj.Name := Results.Col('Name');
Obj.Database := db;
Obj.Rows := -1;
Obj.Size := -1;
Obj.NodeType := lntEvent;
Obj.Created := 0;
Obj.Updated := 0;
Obj.Engine := '';
Obj.Comment := '';
Obj.Version := -1;
Obj.AutoInc := -1;
Obj.RowFormat := '';
Obj.AvgRowLen := -1;
Obj.MaxDataLen := -1;
Obj.IndexLen := -1;
Obj.DataLen := -1;
Obj.DataFree := -1;
Obj.LastChecked := 0;
Obj.Collation := '';
Obj.CheckSum := -1;
Obj.CreateOptions := '';
end;
Results.Next;
end;
FreeAndNil(Results);
end;
// Find youngest last update
for i:=0 to Result.Count-1 do
Result.FLastUpdate := Max(Result.FLastUpdate, Max(Result[i].Updated, Result[i].Created));
// Sort list like it get sorted in MainForm.vstCompareNodes
Result.Sort;
// Add list of objects in this database to cached list of all databases
FDatabases.Add(Result);
SetObjectNamesInSelectedDB;
end;
end;
procedure TMySQLConnection.SetObjectNamesInSelectedDB;
var
i: Integer;
Objects: TDBObjectList;
ObjNames: String;
begin
// Add object names to additional stringlist
if Assigned(FObjectNamesInSelectedDB) then begin
if DbObjectsCached(FDatabase) then begin
Objects := GetDbObjects(FDatabase);
for i:=0 to Objects.Count-1 do
ObjNames := ObjNames + Objects[i].Name + CRLF;
end else
ObjNames := '';
if FObjectNamesInSelectedDB.Text <> ObjNames then
FObjectNamesInSelectedDB.Text := ObjNames;
end;
end;
function TMySQLConnection.GetKeyColumns(Columns: TTableColumnList; Keys: TTableKeyList): TStringList;
var
i: Integer;
AllowsNull: Boolean;
Key: TTableKey;
Col: TTableColumn;
begin
Result := TStringList.Create;
// Find best key for updates
// 1. round: find a primary key
for Key in Keys do begin
if Key.Name = 'PRIMARY' then
Result.Assign(Key.Columns);
end;
if Result.Count = 0 then begin
// no primary key available -> 2. round: find a unique key
for Key in Keys do begin
if Key.IndexType = UKEY then begin
// We found a UNIQUE key - better than nothing. Check if one of the key
// columns allows NULLs which makes it dangerous to use in UPDATES + DELETES.
AllowsNull := False;
for i:=0 to Key.Columns.Count-1 do begin
for Col in Columns do begin
if Col.Name = Key.Columns[i] then
AllowsNull := Col.AllowNull;
if AllowsNull then break;
end;
if AllowsNull then break;
end;
if not AllowsNull then begin
Result.Assign(Key.Columns);
break;
end;
end;
end;
end;
end;
function TMySQLConnection.DecodeAPIString(a: AnsiString): String;
begin
if IsUnicode then
Result := Utf8ToString(a)
else
Result := String(a);
end;
function TMySQLConnection.ConnectionInfo: TStringList;
var
Infos, Val: String;
rx: TRegExpr;
function EvalBool(B: Boolean): String;
begin
if B then Result := 'Yes' else Result := 'No';
end;
begin
Result := TStringList.Create;
if Assigned(Parameters) then
Result.Values['Hostname'] := Parameters.Hostname;
Ping;
Result.Values['Connected'] := EvalBool(FActive);
if FActive then begin
Result.Values['Real Hostname'] := FRealHostname;
Result.Values['Server OS'] := ServerOS;
Result.Values['Server version'] := FServerVersionUntouched;
Result.Values['Client version (libmysql)'] := DecodeApiString(mysql_get_client_info);
Result.Values['Connection port'] := IntToStr(Parameters.Port);
Result.Values['Compressed protocol'] := EvalBool(opCompress in Parameters.Options);
Result.Values['Unicode enabled'] := EvalBool(IsUnicode);
Infos := DecodeApiString(mysql_stat(FHandle));
rx := TRegExpr.Create;
rx.ModifierG := False;
rx.Expression := '(\S.*)\:\s+(\S*)(\s+|$)';
if rx.Exec(Infos) then while True do begin
Val := rx.Match[2];
if LowerCase(rx.Match[1]) = 'uptime' then
Val := FormatTimeNumber(StrToIntDef(Val, 0))
else
Val := FormatNumber(Val);
Result.Values[rx.Match[1]] := Val;
if not rx.ExecNext then
break;
end;
rx.Free;
end;
end;
{ TMySQLQuery }
constructor TMySQLQuery.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FRecNo := -1;
FRecordCount := 0;
FColumnNames := TStringList.Create;
FColumnNames.CaseSensitive := True;
FColumnOrgNames := TStringList.Create;
FColumnOrgNames.CaseSensitive := True;
FStoreResult := True;
FLogCategory := lcSQL;
end;
destructor TMySQLQuery.Destroy;
var
i: Integer;
begin
FreeAndNil(FColumnNames);
FreeAndNil(FColumnOrgNames);
FreeAndNil(FColumns);
FreeAndNil(FKeys);
SetLength(FColumnFlags, 0);
SetLength(FColumnLengths, 0);
if HasResult then for i:=Low(FResultList) to High(FResultList) do
mysql_free_result(FResultList[i]);
SetLength(FResultList, 0);
inherited Destroy;
end;
procedure TMySQLQuery.SetSQL(Value: String);
begin
FSQL := Value;
end;
procedure TMySQLQuery.Execute(AddResult: Boolean=False);
var
i, j, NumFields: Integer;
NumResults: Int64;
Field: PMYSQL_FIELD;
IsBinary: Boolean;
FLastResult: PMYSQL_RES;
begin
FLastResult := Connection.Query(FSQL, FStoreResult, FLogCategory);
if AddResult and (Length(FResultList) = 0) then
AddResult := False;
if AddResult then
NumResults := Length(FResultList)+1
else begin
for i:=Low(FResultList) to High(FResultList) do
mysql_free_result(FResultList[i]);
NumResults := 1;
FRecordCount := 0;
FEditingPrepared := False;
end;
if FLastResult <> nil then begin
Connection.Log(lcDebug, 'Result #'+IntToStr(NumResults)+' fetched.');
SetLength(FResultList, NumResults);
FResultList[NumResults-1] := FLastResult;
FRecordCount := FRecordCount + FLastResult.row_count;
end;
if not AddResult then begin
if HasResult then begin
// FCurrentResults is normally done in SetRecNo, but never if result has no rows
FCurrentResults := FLastResult;
NumFields := mysql_num_fields(FLastResult);
SetLength(FColumnTypes, NumFields);
SetLength(FColumnLengths, NumFields);
SetLength(FColumnFlags, NumFields);
FColumnNames.Clear;
FColumnOrgNames.Clear;
for i:=0 to NumFields-1 do begin
Field := mysql_fetch_field_direct(FLastResult, i);
FColumnNames.Add(Connection.DecodeAPIString(Field.name));
if Connection.ServerVersionInt >= 40100 then
FColumnOrgNames.Add(Connection.DecodeAPIString(Field.org_name))
else
FColumnOrgNames.Add(Connection.DecodeAPIString(Field.name));
FColumnFlags[i] := Field.flags;
FColumnTypes[i] := Datatypes[Low(Datatypes)];
if (Field.flags and ENUM_FLAG) = ENUM_FLAG then
FColumnTypes[i] := Datatypes[Integer(dtEnum)]
else if (Field.flags and SET_FLAG) = SET_FLAG then
FColumnTypes[i] := Datatypes[Integer(dtSet)]
else for j:=Low(Datatypes) to High(Datatypes) do begin
if Field._type = Datatypes[j].NativeType then begin
if Datatypes[j].Index in [dtTinytext, dtText, dtMediumtext, dtLongtext] then begin
// Text and Blob types share the same constants (see FIELD_TYPEs in mysql_api)
// Some function results return binary collation up to the latest versions. Work around
// that by checking if this field is a real table field
// See http://bugs.mysql.com/bug.php?id=10201
if Connection.IsUnicode then
IsBinary := (Field.charsetnr = COLLATION_BINARY) and (Field.org_table <> '')
else
IsBinary := (Field.flags and BINARY_FLAG) = BINARY_FLAG;
if IsBinary then
continue;
end;
FColumnTypes[i] := Datatypes[j];
break;
end;
end;
end;
RecNo := 0;
end else begin
SetLength(FColumnTypes, 0);
SetLength(FColumnLengths, 0);
SetLength(FColumnFlags, 0);
end;
end;
end;
procedure TMySQLQuery.SetColumnOrgNames(Value: TStringList);
begin
// Retrieve original column names from caller
FColumnOrgNames.Text := Value.Text;
end;
procedure TMySQLQuery.First;
begin
RecNo := 0;
end;
procedure TMySQLQuery.Next;
begin
RecNo := RecNo + 1;
end;
procedure TMySQLQuery.SetRecNo(Value: Int64);
var
LengthPointer: PLongInt;
i, j: Integer;
NumRows, WantedLocalRecNo: Int64;
Row: TRowData;
RowFound: Boolean;
begin
if Value = FRecNo then
Exit;
if (not FEditingPrepared) and (Value >= RecordCount) then begin
FRecNo := RecordCount;
FEof := True;
end else begin
// Find row in edited data
RowFound := False;
if FEditingPrepared then begin
for Row in FUpdateData do begin
if Row.RecNo = Value then begin
FCurrentRow := nil;
FCurrentUpdateRow := Row;
for i:=Low(FColumnLengths) to High(FColumnLengths) do
FColumnLengths[i] := Length(FCurrentUpdateRow[i].NewText);
RowFound := True;
break;
end;
end;
end;
// Row not edited data - find it in normal result
if not RowFound then begin
NumRows := 0;
for i:=Low(FResultList) to High(FResultList) do begin
Inc(NumRows, FResultList[i].row_count);
if NumRows > Value then begin
FCurrentResults := FResultList[i];
// Do not seek if FCurrentRow points to the previous row of the wanted row
WantedLocalRecNo := FCurrentResults.row_count-(NumRows-Value);
if (WantedLocalRecNo = 0) or (FRecNo+1 <> Value) or (FCurrentRow = nil) then
mysql_data_seek(FCurrentResults, WantedLocalRecNo);
FCurrentRow := mysql_fetch_row(FCurrentResults);
FCurrentUpdateRow := nil;
// Remember length of column contents. Important for Col() so contents of cells with #0 chars are not cut off
LengthPointer := mysql_fetch_lengths(FCurrentResults);
for j:=Low(FColumnLengths) to High(FColumnLengths) do
FColumnLengths[j] := PInteger(Integer(LengthPointer) + j * SizeOf(Integer))^;
break;
end;
end;
end;
FRecNo := Value;
FEof := False;
end;
end;
function TMySQLQuery.ColumnCount: Integer;
begin
Result := ColumnNames.Count;
end;
function TMySQLQuery.Col(Column: Integer; IgnoreErrors: Boolean=False): String;
var
AnsiStr: AnsiString;
begin
if (Column > -1) and (Column < ColumnCount) then begin
if FEditingPrepared and Assigned(FCurrentUpdateRow) then begin
// Row was edited and only valid in a TRowData
Result := FCurrentUpdateRow[Column].NewText;
end else begin
// The normal case: Fetch cell from mysql result
SetString(AnsiStr, FCurrentRow[Column], FColumnLengths[Column]);
if Datatype(Column).Category in [dtcBinary, dtcSpatial] then
Result := String(AnsiStr)
else
Result := Connection.DecodeAPIString(AnsiStr);
end;
end else if not IgnoreErrors then
Raise EDatabaseError.CreateFmt('Column #%d not available. Query returned %d columns and %d rows.', [Column, ColumnCount, RecordCount]);
end;
function TMySQLQuery.Col(ColumnName: String; IgnoreErrors: Boolean=False): String;
var
idx: Integer;
begin
idx := ColumnNames.IndexOf(ColumnName);
if idx > -1 then
Result := Col(idx)
else if not IgnoreErrors then
Raise EDatabaseError.CreateFmt('Column "%s" not available.', [ColumnName]);
end;
function TMySQLQuery.BinColAsHex(Column: Integer; IgnoreErrors: Boolean=False): String;
var
BinLen: Integer;
Ansi: AnsiString;
begin
// Return a binary column value as hex AnsiString
Result := Col(Column, IgnoreErrors);
Ansi := AnsiString(Result);
BinLen := FColumnLengths[Column];
SetLength(Result, BinLen*2);
BinToHex(PAnsiChar(Ansi), PChar(Result), BinLen);
end;
function TMySQLQuery.DataType(Column: Integer): TDataType;
var
Col: TTableColumn;
begin
Col := ColAttributes(Column);
if Assigned(Col) then
Result := Col.DataType
else
Result := FColumnTypes[Column];
end;
function TMySQLQuery.MaxLength(Column: Integer): Int64;
var
ColAttr: TTableColumn;
begin
// Return maximum posible length of values in given columns
// Note: PMYSQL_FIELD.max_length holds the maximum existing value in that column, which is useless here
Result := MaxInt;
ColAttr := ColAttributes(Column);
if Assigned(ColAttr) then begin
case ColAttr.DataType.Index of
dtChar, dtVarchar, dtBinary, dtVarBinary: Result := MakeInt(ColAttr.LengthSet);
dtTinyText, dtTinyBlob: Result := 255;
dtText, dtBlob: Result := 65535;
dtMediumText, dtMediumBlob: Result := 16777215;
dtLongText, dtLongBlob: Result := 4294967295;
end;
end;
end;
function TMySQLQuery.ValueList(Column: Integer): TStringList;
var
ColAttr: TTableColumn;
begin
Result := TStringList.Create;
Result.QuoteChar := '''';
Result.Delimiter := ',';
ColAttr := ColAttributes(Column);
if Assigned(ColAttr) and (ColAttr.DataType.Index in [dtEnum, dtSet]) then
Result.DelimitedText := ColAttr.LengthSet;
end;
function TMySQLQuery.ColAttributes(Column: Integer): TTableColumn;
var
i: Integer;
begin
Result := nil;
if (Column = -1) or (Column >= FColumnOrgNames.Count) then
raise EDatabaseError.Create('Column #'+IntToStr(Column)+' not available.');
if FEditingPrepared then begin
for i:=0 to FColumns.Count-1 do begin
if FColumns[i].Name = FColumnOrgNames[Column] then begin
Result := FColumns[i];
break;
end;
end;
end;
end;
function TMySQLQuery.ColExists(Column: String): Boolean;
begin
Result := (ColumnNames <> nil) and (ColumnNames.IndexOf(Column) > -1);
end;
function TMySQLQuery.ColIsPrimaryKeyPart(Column: Integer): Boolean;
begin
Result := (FColumnFlags[Column] and PRI_KEY_FLAG) = PRI_KEY_FLAG;
end;
function TMySQLQuery.ColIsUniqueKeyPart(Column: Integer): Boolean;
begin
Result := (FColumnFlags[Column] and UNIQUE_KEY_FLAG) = UNIQUE_KEY_FLAG;
end;
function TMySQLQuery.ColIsKeyPart(Column: Integer): Boolean;
begin
Result := (FColumnFlags[Column] and MULTIPLE_KEY_FLAG) = MULTIPLE_KEY_FLAG;
end;
function TMySQLQuery.IsNull(Column: Integer): Boolean;
begin
if FEditingPrepared and Assigned(FCurrentUpdateRow) then
Result := FCurrentUpdateRow[Column].NewIsNull
else
Result := FCurrentRow[Column] = nil;
end;
function TMySQLQuery.IsNull(Column: String): Boolean;
begin
Result := IsNull(FColumnNames.IndexOf(Column));
end;
function TMySQLQuery.HasResult: Boolean;
begin
Result := Length(FResultList) > 0;
end;
procedure TMySQLQuery.PrepareEditing;
var
CreateTable: String;
begin
// Try to fetch column names and keys
if FEditingPrepared then
Exit;
CreateTable := Connection.GetVar('SHOW CREATE TABLE ' + QuotedDbAndTableName, 1);
FColumns := TTableColumnList.Create;
FKeys := TTableKeyList.Create;
FForeignKeys := TForeignKeyList.Create;
ParseTableStructure(CreateTable, FColumns, FKeys, FForeignKeys);
FUpdateData := TUpdateData.Create(True);
FEditingPrepared := True;
end;
function TMySQLQuery.DeleteRow: Boolean;
var
sql: String;
IsVirtual: Boolean;
begin
// Delete current row from result
PrepareEditing;
IsVirtual := Assigned(FCurrentUpdateRow) and FCurrentUpdateRow.Inserted;
if not IsVirtual then begin
sql := 'DELETE FROM ' + QuotedDbAndTableName + ' WHERE ' + GetWhereClause + ' LIMIT 1';
Connection.Query(sql);
end;
if Assigned(FCurrentUpdateRow) then begin
FUpdateData.Remove(FCurrentUpdateRow);
FCurrentUpdateRow := nil;
end;
Result := True;
end;
function TMySQLQuery.InsertRow: Cardinal;
var
Row, OtherRow: TRowData;
c: TCellData;
i: Integer;
ColAttr: TTableColumn;
InUse: Boolean;
begin
// Add new row and return row number
PrepareEditing;
Row := TRowData.Create(True);
for i:=0 to ColumnCount-1 do begin
c := TCellData.Create;
Row.Add(c);
c.OldText := '';
c.OldIsNull := False;
ColAttr := ColAttributes(i);
if Assigned(ColAttr) then begin
c.OldIsNull := ColAttr.DefaultType in [cdtNull, cdtNullUpdateTS, cdtAutoInc];
if ColAttr.DefaultType in [cdtText, cdtTextUpdateTS] then
c.OldText := ColAttr.DefaultText;
end;
c.NewText := c.OldText;
c.NewIsNull := c.OldIsNull;
c.Modified := False;
end;
Row.Inserted := True;
// Find highest unused recno of inserted rows and use that for this row
Result := High(Cardinal);
while True do begin
InUse := False;
for OtherRow in FUpdateData do begin
InUse := OtherRow.RecNo = Result;
if InUse then break;
end;
if not InUse then break;
Dec(Result);
end;
Row.RecNo := Result;
FUpdateData.Add(Row);
end;
procedure TMySQLQuery.SetCol(Column: Integer; NewText: String; Null: Boolean);
begin
PrepareEditing;
if not Assigned(FCurrentUpdateRow) then begin
CreateUpdateRow;
EnsureFullRow;
end;
FCurrentUpdateRow[Column].NewIsNull := Null;
if Null then
FCurrentUpdateRow[Column].NewText := ''
else
FCurrentUpdateRow[Column].NewText := NewText;
FCurrentUpdateRow[Column].Modified := (FCurrentUpdateRow[Column].NewText <> FCurrentUpdateRow[Column].OldText) or
(FCurrentUpdateRow[Column].NewIsNull <> FCurrentUpdateRow[Column].OldIsNull);
end;
procedure TMySQLQuery.CreateUpdateRow;
var
i: Integer;
c: TCellData;
Row: TRowData;
begin
Row := TRowData.Create(True);
for i:=0 to ColumnCount-1 do begin
c := TCellData.Create;
Row.Add(c);
c.OldText := Col(i);
c.NewText := c.OldText;
c.OldIsNull := IsNull(i);
c.NewIsNull := c.OldIsNull;
c.Modified := False;
end;
Row.Inserted := False;
Row.RecNo := RecNo;
FCurrentUpdateRow := Row;
FUpdateData.Add(FCurrentUpdateRow);
end;
function TMySQLQuery.EnsureFullRow: Boolean;
var
i: Integer;
sql: String;
Data: TMySQLQuery;
begin
// Load full column values
Result := True;
if not HasFullData then try
PrepareEditing;
for i:=0 to FColumnOrgNames.Count-1 do begin
if sql <> '' then
sql := sql + ', ';
sql := sql + Connection.QuoteIdent(FColumnOrgNames[i]);
end;
Data := Connection.GetResults('SELECT '+sql+' FROM '+QuotedDbAndTableName+' WHERE '+GetWhereClause+' LIMIT 1');
Result := Data.RecordCount = 1;
if Result then begin
if not Assigned(FCurrentUpdateRow) then
CreateUpdateRow;
for i:=0 to Data.ColumnCount-1 do begin
FCurrentUpdateRow[i].OldText := Data.Col(i);
FCurrentUpdateRow[i].NewText := FCurrentUpdateRow[i].OldText;
FCurrentUpdateRow[i].OldIsNull := Data.IsNull(i);
FCurrentUpdateRow[i].NewIsNull := FCurrentUpdateRow[i].OldIsNull;
end;
Data.Free;
end;
except on E:EDatabaseError do
Result := False;
end;
end;
function TMySQLQuery.HasFullData: Boolean;
var
Val: String;
i: Integer;
begin
Result := True;
for i:=0 to ColumnCount-1 do begin
if not (Datatype(i).Category in [dtcText, dtcBinary]) then
continue;
Val := Col(i);
if Length(Val) = GRIDMAXDATA then begin
Result := False;
break;
end;
end;
end;
function TMySQLQuery.SaveModifications: Boolean;
var
i: Integer;
Row: TRowData;
Cell: TCellData;
sqlUpdate, sqlInsertColumns, sqlInsertValues, Val: String;
RowModified: Boolean;
ColAttr: TTableColumn;
begin
Result := True;
if not FEditingPrepared then
raise EDatabaseError.Create('Internal error: Cannot post modifications before editing was prepared.');
for Row in FUpdateData do begin
// Prepare update and insert queries
RecNo := Row.RecNo;
sqlUpdate := '';
sqlInsertColumns := '';
sqlInsertValues := '';
RowModified := False;
for i:=0 to ColumnCount-1 do begin
Cell := Row[i];
if not Cell.Modified then
continue;
RowModified := True;
if sqlUpdate <> '' then begin
sqlUpdate := sqlUpdate + ', ';
sqlInsertColumns := sqlInsertColumns + ', ';
sqlInsertValues := sqlInsertValues + ', ';
end;
if Cell.NewIsNull then
Val := 'NULL'
else case Datatype(i).Category of
dtcInteger, dtcReal: Val := Cell.NewText;
dtcBinary: Val := '_binary ' + Cell.NewText;
else Val := Connection.EscapeString(Cell.NewText);
end;
sqlUpdate := sqlUpdate + Connection.QuoteIdent(FColumnOrgNames[i]) + '=' + Val;
sqlInsertColumns := sqlInsertColumns + Connection.QuoteIdent(FColumnOrgNames[i]);
sqlInsertValues := sqlInsertValues + Val;
end;
// Post query and fetch just inserted auto-increment id if applicable
if RowModified then try
if Row.Inserted then begin
Connection.Query('INSERT INTO '+QuotedDbAndTableName+' ('+sqlInsertColumns+') VALUES ('+sqlInsertValues+')');
for i:=0 to ColumnCount-1 do begin
ColAttr := ColAttributes(i);
if Assigned(ColAttr) and (ColAttr.DefaultType = cdtAutoInc) then begin
Row[i].NewText := UnformatNumber(Row[i].NewText);
if Row[i].NewText = '0' then
Row[i].NewText := Connection.GetVar('SELECT LAST_INSERT_ID()');
Row[i].NewIsNull := False;
break;
end;
end;
end else
Connection.Query('UPDATE '+QuotedDbAndTableName+' SET '+sqlUpdate+' WHERE '+GetWhereClause);
// Reset modification flags
for i:=0 to ColumnCount-1 do begin
Cell := Row[i];
Cell.OldText := Cell.NewText;
Cell.OldIsNull := Cell.NewIsNull;
Cell.Modified := False;
end;
Row.Inserted := False;
// TODO: Reload real row data from server if keys allow that???
except
on E:EDatabaseError do begin
Result := False;
MessageDlg(E.Message, mtError, [mbOK], 0);
end;
end;
end;
end;
procedure TMySQLQuery.DiscardModifications;
var
x: Integer;
c: TCellData;
begin
if FEditingPrepared and Assigned(FCurrentUpdateRow) then begin
if FCurrentUpdateRow.Inserted then
FUpdateData.Remove(FCurrentUpdateRow)
else for x:=0 to FCurrentUpdateRow.Count-1 do begin
c := FCurrentUpdateRow[x];
c.NewText := c.OldText;
c.NewIsNull := c.OldIsNull;
c.Modified := False;
end;
end;
end;
function TMySQLQuery.Modified(Column: Integer): Boolean;
begin
Result := False;
if FEditingPrepared and Assigned(FCurrentUpdateRow) then try
Result := FCurrentUpdateRow[Column].Modified;
except
connection.Log(lcdebug, inttostr(column));
raise;
end;
end;
function TMySQLQuery.Modified: Boolean;
var
x, y: Integer;
begin
Result := False;
if FEditingPrepared then for y:=0 to FUpdateData.Count-1 do begin
for x:=0 to FUpdateData[y].Count-1 do begin
Result := FUpdateData[y][x].Modified;
if Result then
break;
end;
if Result then
break;
end;
end;
function TMySQLQuery.Inserted: Boolean;
begin
// Check if current row was inserted and not yet posted to the server
Result := False;
if FEditingPrepared and Assigned(FCurrentUpdateRow) then
Result := FCurrentUpdateRow.Inserted;
end;
function TMySQLQuery.DatabaseName: String;
var
Field: PMYSQL_FIELD;
i: Integer;
begin
// Return first available Field.db property, or just the current database as fallback
for i:=0 to ColumnCount-1 do begin
Field := mysql_fetch_field_direct(FCurrentResults, i);
if Field.db <> '' then begin
Result := Connection.DecodeAPIString(Field.db);
break;
end;
end;
if Result = '' then
Result := Connection.Database;
end;
function TMySQLQuery.TableName: String;
var
Field: PMYSQL_FIELD;
i: Integer;
tbl, db: AnsiString;
begin
for i:=0 to ColumnCount-1 do begin
Field := mysql_fetch_field_direct(FCurrentResults, i);
if (Field.org_table <> '') and (tbl <> '') and ((tbl <> Field.org_table) or (db <> Field.db)) then
raise EDatabaseError.Create('More than one table involved.');
if Field.org_table <> '' then begin
tbl := Field.org_table;
db := Field.db;
end;
end;
if tbl = '' then
raise EDatabaseError.Create('Could not determine name of table.')
else
Result := Connection.DecodeAPIString(tbl)
end;
function TMySQLQuery.QuotedDbAndTableName: String;
var
db: String;
begin
// Return `db`.`table` if necessairy, otherwise `table`
db := DatabaseName;
if Connection.Database <> db then
Result := Connection.QuoteIdent(db)+'.';
Result := Result + Connection.QuoteIdent(TableName);
end;
function TMySQLQuery.GetKeyColumns: TStringList;
var
NeededCols: TStringList;
i: Integer;
begin
// Return key column names, or all column names if no good key present
PrepareEditing;
NeededCols := Connection.GetKeyColumns(FColumns, FKeys);
if NeededCols.Count = 0 then begin
// No good key found. Just expect all columns to be present.
for i:=0 to FColumns.Count-1 do
NeededCols.Add(FColumns[i].Name);
end;
Result := TStringList.Create;
for i:=0 to NeededCols.Count-1 do begin
if FColumnOrgNames.IndexOf(NeededCols[i]) > -1 then
Result.Add(NeededCols[i]);
end;
end;
procedure TMySQLQuery.CheckEditable;
var
i: Integer;
begin
if GetKeyColumns.Count = 0 then
raise EDatabaseError.Create(MSG_NOGRIDEDITING);
// All column names must be present in order to send valid INSERT/UPDATE/DELETE queries
for i:=0 to FColumnOrgNames.Count-1 do begin
if FColumnOrgNames[i] = '' then
raise EDatabaseError.Create('Column #'+IntToStr(i)+' has an undefined origin: '+ColumnNames[i]);
end;
end;
function TMySQLQuery.GetWhereClause: String;
var
i, j: Integer;
NeededCols: TStringList;
ColVal: String;
ColIsNull: Boolean;
begin
// Compose WHERE clause including values from best key for editing
NeededCols := GetKeyColumns;
for i:=0 to NeededCols.Count-1 do begin
j := FColumnOrgNames.IndexOf(NeededCols[i]);
if j = -1 then
raise EDatabaseError.Create('Cannot compose WHERE clause - column missing: '+NeededCols[i]);
if Result <> '' then
Result := Result + ' AND';
Result := Result + ' ' + Connection.QuoteIdent(FColumnOrgNames[j]);
if Modified(j) then begin
ColVal := FCurrentUpdateRow[j].OldText;
ColIsNull := FCurrentUpdateRow[j].OldIsNull;
end else begin
ColVal := Col(j);
ColIsNull := IsNull(j);
end;
if ColIsNull then
Result := Result + ' IS NULL'
else begin
case DataType(j).Category of
dtcInteger, dtcReal: Result := Result + '=' + ColVal;
else Result := Result + '=' + Connection.EscapeString(ColVal);
end;
end;
end;
end;
{ TDBObjectComparer }
function TDBObjectComparer.Compare(const Left, Right: TDBObject): Integer;
begin
// Simple sort method for a TDBObjectList
Result := CompareAnyNode(Left.Name, Right.Name);
end;
{ TDBObject }
constructor TDBObject.Create;
begin
NodeType := lntNone;
end;
procedure TDBObject.Assign(Source: TPersistent);
var
s: TDBObject;
begin
if Source is TDBObject then begin
s := Source as TDBObject;
Name := s.Name;
Database := s.Database;
NodeType := s.NodeType;
Created := s.Created;
Updated := s.Updated;
Comment := s.Comment;
Rows := s.Rows;
Size := s.Size;
end else
inherited;
end;
function TDBObject.GetObjType: String;
begin
case NodeType of
lntTable: Result := 'Table';
lntView: Result := 'View';
lntFunction: Result := 'Function';
lntProcedure: Result := 'Procedure';
lntTrigger: Result := 'Trigger';
lntEvent: Result := 'Event';
else Result := 'Unknown, should never appear';
end;
end;
function TDBObject.GetImageIndex: Integer;
begin
// Detect key icon index for specified db object (table, trigger, ...)
case NodeType of
lntTable: Result := ICONINDEX_TABLE;
lntFunction: Result := ICONINDEX_STOREDFUNCTION;
lntProcedure: Result := ICONINDEX_STOREDPROCEDURE;
lntView: Result := ICONINDEX_VIEW;
lntTrigger: Result := ICONINDEX_TRIGGER;
lntEvent: Result := ICONINDEX_EVENT;
else Result := -1;
end;
end;
{ *** TTableColumn }
constructor TTableColumn.Create;
begin
inherited Create;
end;
destructor TTableColumn.Destroy;
begin
inherited Destroy;
end;
procedure TTableColumn.SetStatus(Value: TEditingStatus);
begin
// Set editing flag and enable "Save" button
if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esModified) then
Value := esAddedModified
else if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esDeleted) then
Value := esAddedDeleted;
FStatus := Value;
end;
function TTableColumn.SQLCode: String;
begin
Result := TMySQLConnection.QuoteIdent(Name) + ' ' +DataType.Name;
if LengthSet <> '' then
Result := Result + '(' + LengthSet + ')';
if DataType.HasUnsigned and Unsigned then
Result := Result + ' UNSIGNED';
if not AllowNull then
Result := Result + ' NOT';
Result := Result + ' NULL';
if DefaultType <> cdtNothing then begin
Result := Result + ' ' + GetColumnDefaultClause(DefaultType, DefaultText);
Result := TrimRight(Result); // Remove whitespace for columns without default value
end;
if Comment <> '' then
Result := Result + ' COMMENT '+esc(Comment);
if Collation <> '' then
Result := Result + ' COLLATE '+esc(Collation);
end;
{ *** TTableKey }
constructor TTableKey.Create;
begin
inherited Create;
Columns := TStringList.Create;
SubParts := TStringList.Create;
Columns.OnChange := Modification;
Subparts.OnChange := Modification;
end;
destructor TTableKey.Destroy;
begin
FreeAndNil(Columns);
FreeAndNil(SubParts);
inherited Destroy;
end;
procedure TTableKey.Modification(Sender: TObject);
begin
if not Added then
Modified := True;
end;
function TTableKey.SQLCode: String;
var
i: Integer;
begin
Result := '';
// Supress SQL error trying index creation with 0 column
if Columns.Count = 0 then
Exit;
if IndexType = PKEY then
Result := Result + 'PRIMARY KEY '
else begin
if IndexType <> KEY then
Result := Result + IndexType + ' ';
Result := Result + 'INDEX ' + TMySQLConnection.QuoteIdent(Name) + ' ';
end;
Result := Result + '(';
for i:=0 to Columns.Count-1 do begin
Result := Result + TMySQLConnection.QuoteIdent(Columns[i]);
if SubParts[i] <> '' then
Result := Result + '(' + SubParts[i] + ')';
Result := Result + ', ';
end;
if Columns.Count > 0 then
Delete(Result, Length(Result)-1, 2);
Result := Result + ')';
if Algorithm <> '' then
Result := Result + ' USING ' + Algorithm;
end;
{ *** TForeignKey }
constructor TForeignKey.Create;
begin
inherited Create;
Columns := TStringList.Create;
ForeignColumns := TStringList.Create;
end;
destructor TForeignKey.Destroy;
begin
FreeAndNil(Columns);
FreeAndNil(ForeignColumns);
inherited Destroy;
end;
function TForeignKey.SQLCode: String;
var
i: Integer;
begin
Result := 'CONSTRAINT '+TMySQLConnection.QuoteIdent(KeyName)+' FOREIGN KEY (';
for i:=0 to Columns.Count-1 do
Result := Result + TMySQLConnection.QuoteIdent(Columns[i]) + ', ';
if Columns.Count > 0 then Delete(Result, Length(Result)-1, 2);
Result := Result + ') REFERENCES ' + TMySQLConnection.QuoteIdent(ReferenceTable) + ' (';
for i:=0 to ForeignColumns.Count-1 do
Result := Result + TMySQLConnection.QuoteIdent(ForeignColumns[i]) + ', ';
if ForeignColumns.Count > 0 then Delete(Result, Length(Result)-1, 2);
Result := Result + ')';
if OnUpdate <> '' then
Result := Result + ' ON UPDATE ' + OnUpdate;
if OnDelete <> '' then
Result := Result + ' ON DELETE ' + OnDelete;
end;
end.