diff --git a/.github/workflows/lazarus.yaml b/.github/workflows/lazarus.yaml index 3460aa1e6..c37018592 100644 --- a/.github/workflows/lazarus.yaml +++ b/.github/workflows/lazarus.yaml @@ -163,12 +163,18 @@ jobs: - name: Install FPM run: sudo gem install --no-document fpm + - name: Install rpm tooling for fpm + run: sudo apt-get update && sudo apt-get install -y rpm + - name: Create release archives (Linux GTK) run: make tar-gtk2 - name: Create debian package run: make deb-package + - name: Build rpm package + run: make rpm-package + - name: Create release archives (Linux QT5) run: make tar-qt5 diff --git a/Makefile b/Makefile index 9c0b75c38..9675bce03 100644 --- a/Makefile +++ b/Makefile @@ -120,6 +120,38 @@ deb-package: ./deb/=/ rm control.txt +rpm-package: + @echo "=== Creating rpm package" + rm -vrf rpm + cp -R package-skeleton rpm + find rpm -iname ".gitkeep" -exec rm -v {} + + cp -vR extra/locale/*.mo rpm/usr/share/heidisql/locale + cp -v extra/ini/*.ini rpm/usr/share/heidisql + cp -v res/deb-package-icon.png rpm/usr/share/pixmaps/heidisql.png + cp -v $(BINQT6) rpm/usr/share/heidisql/heidisql + chmod +x rpm/usr/share/heidisql/heidisql + cp -v README.md LICENSE rpm/usr/share/doc/heidisql + mkdir -p dist + rm -vf dist/*.rpm + + fpm -s dir -t rpm -n heidisql -v $(VERSION) \ + -p dist \ + --verbose \ + --rpm-os linux \ + --description "HeidiSQL SQL client (Qt6)" \ + --url "https://www.heidisql.com" \ + --license "GPL-2.0-or-later" \ + --depends "libQt6Widgets.so.6" \ + --depends "libQt6Gui.so.6" \ + --depends "libQt6Core.so.6" \ + --depends "libQt6Pas.so.6" \ + --depends "libssl.so.3" \ + --depends "libmariadb.so.3" \ + --depends "libpq.so.5" \ + --depends "libsqlite3.so.0" \ + --depends "libsybdb.so.5" \ + ./rpm/=/ + tar-gtk2: @echo "=== Creating GTK2 archive" rm -vrf tar diff --git a/extra/locale/heidisql.po b/extra/locale/heidisql.po index 1db89328b..d9595c297 100644 --- a/extra/locale/heidisql.po +++ b/extra/locale/heidisql.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: HeidiSQL\n" "POT-Creation-Date: 2012-11-05 21:40\n" -"PO-Revision-Date: 2026-02-28 15:20+0100\n" +"PO-Revision-Date: 2026-03-24 15:09+0100\n" "Last-Translator: Ansgar Becker \n" "Language-Team: English (http://www.transifex.com/projects/p/heidisql/language/en/)\n" "Language: en\n" @@ -2605,6 +2605,9 @@ msgstr "Explore folder" msgid "Clear query history ..." msgstr "Clear query history ..." +msgid "Delete this query from history" +msgstr "Delete this query from history" + #. MainForm..popupQueryHelpers..menuQueryHelpersGenerateInsert..Caption #: main.dfm:9218 msgid "Generate %s ..." @@ -4928,6 +4931,9 @@ msgstr "Could not find node to focus." msgid "Could not load full row data." msgstr "Could not load full row data." +msgid "Column %s is defined as generated per \"%s\". You cannot edit its content." +msgstr "Column %s is defined as generated per \"%s\". You cannot edit its content." + #: main.pas:8462 msgid "Not available on %s" msgstr "Not available on %s" @@ -5436,8 +5442,8 @@ msgid "Unhandled privilege object: %s" msgstr "Unhandled privilege object: %s" #: usermanager.pas:1297 -msgid "Delete user %s@%s?" -msgstr "Delete user %s@%s?" +msgid "Delete user or role %s?" +msgstr "Delete user or role %s?" #. dbconnection.pas msgid "Yes" @@ -6800,3 +6806,33 @@ msgstr "Reverse" msgid "Show reverse foreign keys" msgstr "Show reverse foreign keys" + +msgid "Login cancelled" +msgstr "Login cancelled" + +msgid "Folder in path does not exist: %s" +msgstr "Folder in path does not exist: %s" + +msgid "File does not yet exist, will be created now: %s" +msgstr "File does not yet exist, will be created now: %s" + +msgid "Create role" +msgstr "Create role" + +msgid "Role name" +msgstr "Role name" + +msgid "Roles" +msgstr "Roles" + +msgid "Assigned" +msgstr "Assigned" + +msgid "Yes, with admin option" +msgstr "Yes, with admin option" + +msgid "No password hash column available" +msgstr "No password hash column available" + +msgid "Default role:" +msgstr "Default role:" diff --git a/heidisql.lpi b/heidisql.lpi index 7bfd3bff4..6a11785d9 100644 --- a/heidisql.lpi +++ b/heidisql.lpi @@ -25,7 +25,7 @@ - + diff --git a/source/apphelpers.pas b/source/apphelpers.pas index e2a728aaa..53e2813ea 100644 --- a/source/apphelpers.pas +++ b/source/apphelpers.pas @@ -74,12 +74,13 @@ TSQLBatch = class(TObjectList) private FSQL: String; FQuotes: THashedStringList; + FEscape: Char; procedure SetSQL(Value: String); function GetSize: Integer; function GetSQLWithoutComments: String; overload; public constructor Create(NetTypeGroup: TNetTypeGroup); - destructor Destroy; overload; + destructor Destroy; override; class function GetSQLWithoutComments(FullSQL: String): String; overload; property Size: Integer read GetSize; property SQL: String read FSQL write SetSQL; @@ -713,10 +714,13 @@ function EncodeURLParam(const Value: String): String; } procedure StreamWrite(S: TStream; Text: String = ''); var - utf8: AnsiString; + utf8: UTF8String; + L: Integer; begin utf8 := Utf8Encode(Text); - S.Write(utf8[1], Length(utf8)); + L := Length(utf8); + if L > 0 then + S.WriteBuffer(utf8[1], L); end; @@ -1475,12 +1479,8 @@ procedure FixVT(VT: TVirtualStringTree; IsResultGrid: Boolean=False); else MultiLineCount := 1; // Issue #2344: TBaseVirtualTree.UpdateVerticalRange crashes with ERangeError on ArchLinux - // due to FRangeY/Cardinal getting a negative value. - // Happening when DefaultNodeHeight is set and then with 0 nodes in data grid - // Once the form files are back at 96 PPI this should no longer crash. - {$IFNDEF LINUX} + // Fixed through form files back at 96 PPI VT.DefaultNodeHeight := SingleLineHeight * MultiLineCount; - {$ENDIF} if MultiLineCount > 1 then begin VT.BeginUpdate; Node := VT.GetFirstInitialized; @@ -2366,8 +2366,6 @@ function MessageDialog(const Title, Msg: string; DlgType: TMsgDlgType; Buttons: end; if Title <> Dialog.Caption then Dialog.Title := Title; - if Assigned(MainForm) and (MainForm.ActiveConnection <> nil) then - Dialog.Caption := MainForm.ActiveConnection.Parameters.SessionName + ': ' + Dialog.Caption; rx := TRegExpr.Create; rx.Expression := 'https?://[^\s"]+'; if ThemeIsDark then @@ -3219,19 +3217,21 @@ function TSQLSentence.GetSQLWithoutComments: String; constructor TSQLBatch.Create(NetTypeGroup: TNetTypeGroup); begin - inherited; + inherited Create; FQuotes := THashedStringList.Create; FQuotes.CaseSensitive := True; FQuotes.Sorted := True; FQuotes.Add('"'); FQuotes.Add(''''); + FEscape := '\'; case NetTypeGroup of ngMySQL: FQuotes.Add('`'); // MySQL/MariaDB only ngPgSQL: FQuotes.Add('$$'); // PostgreSQL only ($abc$ unsupported) + ngSQLite: FEscape := ''''; end; end; -destructor TSQLBatch.Destroy; overload; +destructor TSQLBatch.Destroy; begin FQuotes.Free; inherited; @@ -3318,7 +3318,7 @@ procedure TSQLBatch.SetSQL(Value: String); end; end; if not InEscape then - InEscape := c = '\' + InEscape := c = FEscape else InEscape := False; @@ -3535,7 +3535,6 @@ constructor TAppSettings.Create; var rx: TRegExpr; i: Integer; - DefaultSnippetsDirectory: String; PortableLockFile: String; NewFileHandle: THandle; begin @@ -3784,10 +3783,7 @@ constructor TAppSettings.Create; InitSetting(asDisplayReverseForeignKeys, 'DisplayReverseForeignKeys', 0, False); InitSetting(asGenerateDataNumRows, 'GenerateDataNumRows', 1000); InitSetting(asGenerateDataNullAmount, 'GenerateDataNullAmount', 10); - - // Default folder for snippets - DefaultSnippetsDirectory := DirnameUserDocuments + 'Snippets' + DirectorySeparator; - InitSetting(asCustomSnippetsDirectory, 'CustomSnippetsDirectory', 0, False, DefaultSnippetsDirectory); + InitSetting(asCustomSnippetsDirectory, 'CustomSnippetsDirectory', 0, False, DirnameUserDocuments + 'Snippets' + DirectorySeparator); InitSetting(asPromptSaveFileOnTabClose, 'PromptSaveFileOnTabClose', 0, True); // Restore tabs feature crashes often on old XP systems, see https://www.heidisql.com/forum.php?t=34044 InitSetting(asRestoreTabs, 'RestoreTabs', 0, True); diff --git a/source/connections.pas b/source/connections.pas index d02177da2..fbe7af18a 100644 --- a/source/connections.pas +++ b/source/connections.pas @@ -311,6 +311,7 @@ procedure Tconnform.FormCreate(Sender: TObject); editLogFilePath.Hint := FilenameHint; // Populate dropdown with supported SSH executables + {$IFDEF WINDOWS} ExeFiles := FindAllFiles(GetAppDir, '*.exe', False); for ExePath in ExeFiles do begin ExeFile := ExtractFileName(ExePath); @@ -320,6 +321,10 @@ procedure Tconnform.FormCreate(Sender: TObject); end; ExeFiles.Free; comboSSHExe.Items.Add('ssh.exe'); + {$ELSE} + // Linux and macOS go here, no executable shipped + comboSSHExe.Items.Add('ssh'); + {$ENDIF} // Translate combo items for i:=0 to comboSSLVerification.Items.Count-1 do begin @@ -1321,7 +1326,7 @@ procedure Tconnform.editDatabasesRightButtonClick(Sender: TObject); Item.Caption := DB; Item.OnClick := MenuDatabasesClick; Item.AutoCheck := True; - Item.RadioItem := Params.NetTypeGroup = ngPgSQL; + Item.RadioItem := Params.IsAnyPostgreSQL; FPopupDatabases.Items.Add(Item); end; Databases.Free; diff --git a/source/const.inc b/source/const.inc index aa72a55e3..57d0796b1 100644 --- a/source/const.inc +++ b/source/const.inc @@ -49,6 +49,8 @@ const ICONINDEX_EVENT = 80; ICONINDEX_KEYWORD = 25; ICONINDEX_FOLDER = 174; + ICONINDEX_USER = 43; + ICONINDEX_ROLE = 95; // Size of byte units {Kibibyte} SIZE_KB = Int64(1024); @@ -99,7 +101,8 @@ const MsgUnhandledNetType: String = 'Unhandled connection type (%d)'; MsgUnhandledControl: String = 'Unhandled control in %s'; MsgDisconnect: String = 'Connection to %s closed at %s'; - TextInvalidColumn: String = '?'; + // This must be an empty string, otherwise TTableColumn's get GenerationExpression=XYZ on old servers + TextInvalidColumn: String = ''; FILEFILTER_SQLITEDB = '*.sqlite3;*.sqlite;*.db;*.s3db'; FILEEXT_SQLITEDB = 'sqlite3'; PROPOSAL_ITEM_HEIGHT = 18; diff --git a/source/copytable.pas b/source/copytable.pas index 898661c32..5ebe2c1a6 100644 --- a/source/copytable.pas +++ b/source/copytable.pas @@ -419,7 +419,7 @@ procedure TCopyTableForm.btnOKClick(Sender: TObject); for Column in SelectedColumns do begin AutoIncGetsKey := False; AutoIncRemoved := False; - AutoIncName := Column.AutoIncName; + AutoIncName := FConnection.SqlProvider.GetSql(qAutoInc); if Column.DefaultType = cdtAutoInc then begin for Key in SelectedKeys do begin // Don't check index type, MySQL allows auto-increment columns on nearly all indexes diff --git a/source/dbconnection.pas b/source/dbconnection.pas index 88fdccbbd..0f772d582 100644 --- a/source/dbconnection.pas +++ b/source/dbconnection.pas @@ -64,7 +64,6 @@ TTableColumn = class(TPersistent) function CastAsText: String; property Status: TEditingStatus read FStatus write SetStatus; property Connection: TDBConnection read FConnection; - function AutoIncName: String; function FullDataType: String; end; PTableColumn = ^TTableColumn; @@ -91,7 +90,7 @@ TTableKey = class(TPersistent) Name, OldName: String; IndexType, OldIndexType, Algorithm, Comment: String; Columns, SubParts, Collations: TStringList; - Modified, Added: Boolean; + Modified, Added, Visible: Boolean; constructor Create(AOwner: TDBConnection); destructor Destroy; override; procedure Assign(Source: TPersistent); override; @@ -188,7 +187,7 @@ TDBObject = class(TPersistent) NodeType, GroupType: TListNodeType; constructor Create(OwnerConnection: TDBConnection); - destructor Destroy; + destructor Destroy; override; procedure Assign(Source: TPersistent); override; procedure UnloadDetails; procedure Drop; @@ -412,12 +411,20 @@ TDBLogItem = class(TObject) TDBLogEvent = procedure(Msg: String; Category: TDBLogCategory=lcInfo; Connection: TDBConnection=nil) of object; TDBEvent = procedure(Connection: TDBConnection; Database: String) of object; TDBDataTypeArray = Array of TDBDataType; - TFeatureOrRequirement = (frSrid, frTimezoneVar, frTemporalTypesFraction, - frShowCreateTrigger, frShowWarnings, - frIntegerDisplayWidth, frShowFunctionStatus, frShowProcedureStatus, - frShowTriggers, frShowEvents, frColumnDefaultParentheses, - frHelpKeyword, frEditVariables, frCreateView, frCreateProcedure, frCreateFunction, - frCreateTrigger, frCreateEvent, frInvisibleColumns, frCompressedColumns); + TFeatureOrRequirement = ( + frSrid, + frTemporalTypesFraction, + frIntegerDisplayWidth, + frColumnDefaultParentheses, + frEditVariables, + frCreateView, + frCreateProcedure, + frCreateFunction, + frCreateTrigger, + frCreateEvent, + frInvisibleColumns, + frCompressedColumns + ); TDBConnection = class(TComponent) private @@ -426,6 +433,7 @@ TDBConnection = class(TComponent) FServerUptime: Integer; FServerDateTimeOnStartup: String; FParameters: TConnectionParameters; + FOwnsParameters: Boolean; FSecureShellCmd: TSecureShellCmd; FDatabase: String; FAllDatabases: TStringList; @@ -473,6 +481,7 @@ TDBConnection = class(TComponent) FMaxRowsPerInsert: Int64; FCaseSensitivity: Integer; FSQLFunctions: TSQLFunctionList; + FNamedEnums: TStringList; procedure SetActive(Value: Boolean); virtual; abstract; procedure DoBeforeConnect; virtual; procedure StartSSHTunnel(var FinalHost: String; var FinalPort: Integer); @@ -509,7 +518,9 @@ TDBConnection = class(TComponent) public constructor Create(AOwner: TComponent); override; destructor Destroy; override; - procedure Query(SQL: String; DoStoreResult: Boolean=False; LogCategory: TDBLogCategory=lcSQL); virtual; + procedure Query(SQL: String; DoStoreResult: Boolean=False; LogCategory: TDBLogCategory=lcSQL); overload; virtual; + procedure Query(QueryId: TQueryId); overload; + procedure Query(QueryId: TQueryId; const Args: array of const); overload; procedure Log(Category: TDBLogCategory; Msg: String); function EscapeString(Text: String; ProcessJokerChars: Boolean=False; DoQuote: Boolean=True): String; overload; function EscapeString(Text: String; Datatype: TDBDatatype): String; overload; @@ -552,6 +563,7 @@ TDBConnection = class(TComponent) function ApplyLimitClause(QueryType, QueryBody: String; Limit, Offset: Int64): String; function LikeClauseTail: String; property Parameters: TConnectionParameters read FParameters write FParameters; + property OwnsParameters: Boolean read FOwnsParameters write FOwnsParameters; property ThreadId: Int64 read GetThreadId; property ConnectionUptime: Int64 read GetConnectionUptime; property ServerUptime: Int64 read GetServerUptime; @@ -565,6 +577,7 @@ TDBConnection = class(TComponent) property KeyCache: TKeyCache read FKeyCache; property ForeignKeyCache: TForeignKeyCache read FForeignKeyCache; property CheckConstraintCache: TCheckConstraintCache read FCheckConstraintCache; + property StringQuoteChar: Char read FStringQuoteChar; property QuoteChar: Char read FQuoteChar; property QuoteChars: String read FQuoteChars; function ServerVersionStr: String; @@ -606,6 +619,7 @@ TDBConnection = class(TComponent) function IsHex(Text: String): Boolean; function Has(Item: TFeatureOrRequirement): Boolean; property SqlProvider: TSqlProvider read FSqlProvider; + property NamedEnums: TStringList read FNamedEnums; published property Active: Boolean read FActive write SetActive default False; property Database: String read FDatabase write SetDatabase; @@ -2017,6 +2031,7 @@ constructor TDBConnection.Create(AOwner: TComponent); begin inherited; FParameters := TConnectionParameters.Create; + FOwnsParameters := True; FRowsFound := 0; FRowsAffected := 0; FWarningCount := 0; @@ -2050,6 +2065,9 @@ constructor TDBConnection.Create(AOwner: TComponent); FStringQuoteChar := ''''; FCollationTable := nil; FCharsetTable := nil; + FQuoteChar := '"'; + FQuoteChars := '"[]'; + FNamedEnums := TStringList.Create; end; @@ -2074,8 +2092,6 @@ constructor TSqlSrvConnection.Create(AOwner: TComponent); i: Integer; begin inherited; - FQuoteChar := '"'; - FQuoteChars := '"[]'; SetLength(FDatatypes, Length(MSSQLDatatypes)); for i:=0 to High(MSSQLDatatypes) do FDatatypes[i] := MSSQLDatatypes[i]; @@ -2090,7 +2106,6 @@ constructor TPgConnection.Create(AOwner: TComponent); i: Integer; begin inherited; - FQuoteChar := '"'; FQuoteChars := '"'; SetLength(FDatatypes, Length(PostGreSQLDatatypes)); for i:=0 to High(PostGreSQLDatatypes) do @@ -2107,8 +2122,6 @@ constructor TSQLiteConnection.Create(AOwner: TComponent); i: Integer; begin inherited; - FQuoteChar := '"'; - FQuoteChars := '"[]'; SetLength(FDatatypes, Length(SQLiteDatatypes)); for i:=0 to High(SQLiteDatatypes) do FDatatypes[i] := SQLiteDatatypes[i]; @@ -2122,8 +2135,6 @@ constructor TSQLiteConnection.Create(AOwner: TComponent); i: Integer; begin inherited; - FQuoteChar := '"'; - FQuoteChars := '"[]'; SetLength(FDatatypes, Length(InterbaseDatatypes)); for i:=0 to High(InterbaseDatatypes) do FDatatypes[i] := InterbaseDatatypes[i]; @@ -2138,7 +2149,9 @@ destructor TDBConnection.Destroy; FKeepAliveTimer.Free; FFavorites.Free; FInformationSchemaObjects.Free; - FParameters.Free; + FNamedEnums.Free; + if FOwnsParameters then + FParameters.Free; inherited; end; @@ -2213,7 +2226,7 @@ function TDBConnection.GetDatatypeByName(var DataType: String; DeleteFromSource: TypesSorted.Free; end; - rx.Expression := '^('+Types+')\b(\[\])?'; + rx.Expression := '\b('+Types+')\b(\[\])?'; Match := rx.Exec(DataType); // Prefer a later match which is longer than the one found before. // See http://www.heidisql.com/forum.php?t=17061 @@ -2254,27 +2267,18 @@ function TDBConnection.GetDatatypeByNativeType(NativeType: Integer; Identifier: i: Integer; rx: TRegExpr; TypeFound: Boolean; - TypeOid: String; begin Result := Default(TDBDatatype); rx := TRegExpr.Create; TypeFound := False; + for i:=0 to High(Datatypes) do begin - if Datatypes[i].NativeTypes = '?' then begin - // PG oid is set to be populated via '?' - Datatypes[i].NativeTypes := ''; - TypeOid := GetVar('SELECT oid FROM '+QuoteIdent('pg_type')+' WHERE '+QuoteIdent('typname')+' = '+EscapeString(Datatypes[i].Name.ToLower)); - if IsNumeric(TypeOid) then begin - Datatypes[i].NativeTypes := TypeOid; - Log(lcInfo, 'Found oid/NativeTypes of '+Datatypes[i].Name+' data type: '+Datatypes[i].NativeTypes); - end - else begin - Log(lcInfo, 'No support for '+Datatypes[i].Name+' data type on this server.'); - end; - end; - // Skip if native ids / oid's are (still) empty + // Skip if native ids / oid's are (yet) empty if Datatypes[i].NativeTypes.IsEmpty then Continue; + // Skip ? and e which have a special meaning + if Datatypes[i].NativeTypes.Length = 1 then + Continue; rx.Expression := '\b('+Datatypes[i].NativeTypes+')\b'; if rx.Exec(IntToStr(NativeType)) then begin Result := Datatypes[i]; @@ -2725,18 +2729,13 @@ procedure TSqlSrvConnection.SetActive(Value: Boolean); procedure TPgConnection.SetActive(Value: Boolean); var - dbname, ConnectionString, OptionValue, Error: String; + ConnectionString, OptionValue, Error: String; ConnectOptions: TStringList; FinalHost, ErrorHint: String; FinalPort, i: Integer; begin if Value then begin DoBeforeConnect; - // Simon Riggs: - // "You should connect as "postgres" database by default, with an option to change. Don't use template1" - dbname := FParameters.AllDatabasesStr; - if dbname = '' then - dbname := 'postgres'; // Prepare special stuff for SSH tunnel FinalHost := FParameters.Hostname; @@ -2752,9 +2751,10 @@ procedure TPgConnection.SetActive(Value: Boolean); .AddPair('port', IntToStr(FinalPort)) .AddPair('user', FParameters.Username) .AddPair('password', FParameters.Password) - .AddPair('dbname', dbname) .AddPair('application_name', APPNAME) .AddPair('sslmode', 'disable'); + if not FParameters.AllDatabasesStr.IsEmpty then + ConnectOptions.AddPair('dbname', FParameters.AllDatabasesStr); if FParameters.WantSSL then begin // Be aware .AddPair would add duplicates case FParameters.SSLVerification of @@ -2846,6 +2846,7 @@ procedure TSQLiteConnection.SetActive(Value: Boolean); ErrorHint: String; FileNames, EncryptionParams: TStringList; MainFile, DbAlias, Param, ParamName: String; + MainFileDir: String; i, SplitPos, ParamValue: Integer; CipherIndex, ConfigResult: Integer; ParamWasSet: Boolean; @@ -2856,8 +2857,12 @@ procedure TSQLiteConnection.SetActive(Value: Boolean); if Value then begin // Fixes "out of memory" crash in sqlite3_open, see issue #1367 + MainFileDir := ExtractFilePath(MainFile); + MainFileDir := IncludeTrailingPathDelimiter(MainFileDir); + if not DirectoryExists(MainFileDir) then + raise EDbError.Create(f_('Folder in path does not exist: %s', [MainFile])); if not FileExists(MainFile) then - raise EDbError.Create(f_('File does not exist: %s', [MainFile])); + Log(lcInfo, f_('File does not yet exist, will be created now: %s', [MainFile])); DoBeforeConnect; @@ -3112,10 +3117,15 @@ procedure TDBConnection.DoBeforeConnect; Dialog.lblPrompt.Caption := f_('Login to %s:', [FParameters.Hostname]); Dialog.editUsername.Text := FParameters.Username; Dialog.editPassword.Text := FParameters.Password; - Dialog.ShowModal; - FParameters.Username := Dialog.editUsername.Text; - FParameters.Password := Dialog.editPassword.Text; - Dialog.Free; + if Dialog.ShowModal = mrOk then begin + FParameters.Username := Dialog.editUsername.Text; + FParameters.Password := Dialog.editPassword.Text; + Dialog.Free; + end + else begin + Dialog.Free; + raise EDbError.Create(_('Login cancelled')); + end; end; // Prepare connection @@ -3245,13 +3255,59 @@ procedure TDBConnection.EndSSHTunnel; procedure TDBConnection.DoAfterConnect; var + i: Integer; + TypeOid: String; + AllEnums: TDBQuery; + AllEnumsList: TStringList; SQLFunctionsFileOrder: String; MajorMinorVer, MajorVer: String; StartupScript: String; StartupBatch: TSQLBatch; SqlQuery: TSQLSentence; + Minutes, Hours: Integer; + Offset: String; begin FSqlProvider.ServerVersion := ServerVersionInt; + + for i:=0 to High(Datatypes) do begin + + if Datatypes[i].NativeTypes = '?' then begin + // PG oid is set to be populated via '?' + TypeOid := GetVar('SELECT oid FROM '+QuoteIdent('pg_type')+' WHERE '+QuoteIdent('typname')+' = '+EscapeString(Datatypes[i].Name.ToLower)); + if IsNumeric(TypeOid) then begin + Datatypes[i].NativeTypes := TypeOid; + Log(lcInfo, 'Found oid/NativeTypes of '+Datatypes[i].Name+' data type: '+Datatypes[i].NativeTypes); + end + else begin + Log(lcInfo, 'No support for '+Datatypes[i].Name+' data type on this server.'); + end; + end + + else if (Datatypes[i].NativeTypes = 'e') and FSqlProvider.Has(qGetEnumTypes) then begin + // PG ENUM types populated via 'e' + AllEnums := GetResults(FSqlProvider.GetSql(qGetEnumTypes)); + AllEnumsList := TStringList.Create; + while not AllEnums.Eof do begin + AllEnumsList.Add(AllEnums.Col('enum_name')); + AllEnumsList.Add(AllEnums.Col('enum_schema') + '.' + AllEnums.Col('enum_name')); + FNamedEnums.AddPair( + AllEnums.Col('enum_name'), + AllEnums.Col('enum_labels') + ); + FNamedEnums.AddPair( + AllEnums.Col('enum_schema') + '.' + AllEnums.Col('enum_name'), + AllEnums.Col('enum_labels') + ); + AllEnums.Next; + end; + AllEnums.Free; + Datatypes[i].Names := Implode('|', AllEnumsList); + AllEnumsList.Free; + end; + + end; + + AppSettings.SessionPath := FParameters.SessionPath; AppSettings.WriteString(asServerVersionFull, FServerVersionUntouched); FParameters.ServerVersion := FServerVersionUntouched; @@ -3284,6 +3340,19 @@ procedure TDBConnection.DoAfterConnect; SQLFunctionsFileOrder := ''; FSQLFunctions := TSQLFunctionList.Create(Self, SQLFunctionsFileOrder); + // Set timezone offset to UTC + if FSqlProvider.Has(qSetTimezone) and Parameters.LocalTimeZone then begin + Minutes := GetLocalTimeOffset; + Hours := Minutes div 60; + Minutes := Minutes mod 60; + if Hours < 0 then + Offset := '+' + else + Offset := '-'; + Offset := Offset + Format('%.2d:%.2d', [Abs(Hours), Abs(Minutes)]); + Query(qSetTimezone, [EscapeString(Offset)]); + end; + // Process startup script StartupScript := Trim(FParameters.StartupScriptFilename); if StartupScript <> '' then begin @@ -3306,25 +3375,11 @@ procedure TDBConnection.DoAfterConnect; procedure TMySQLConnection.DoAfterConnect; var - Minutes, Hours, i: Integer; - Offset: String; ObjNames: TStringList; + i: Integer; begin inherited; - // Set timezone offset to UTC - if Has(frTimezoneVar) and Parameters.LocalTimeZone then begin - Minutes := GetLocalTimeOffset; - Hours := Minutes div 60; - Minutes := Minutes mod 60; - if Hours < 0 then - Offset := '+' - else - Offset := '-'; - Offset := Offset + Format('%.2d:%.2d', [Abs(Hours), Abs(Minutes)]); - Query('SET time_zone='+EscapeString(Offset)); - end; - // Support microseconds in some temporal datatypes of MariaDB 5.3+ and MySQL 5.6 if Has(frTemporalTypesFraction) then begin for i:=Low(FDatatypes) to High(FDatatypes) do begin @@ -3531,6 +3586,17 @@ procedure TDBConnection.Query(SQL: String; DoStoreResult: Boolean=False; LogCate end; +procedure TDBConnection.Query(QueryId: TQueryId); +begin + Query(FSqlProvider.GetSql(QueryId)); +end; + +procedure TDBConnection.Query(QueryId: TQueryId; const Args: array of const); +begin + Query(FSqlProvider.GetSql(QueryId, Args)); +end; + + procedure TMySQLConnection.Query(SQL: String; DoStoreResult: Boolean=False; LogCategory: TDBLogCategory=lcSQL); var QueryStatus: Integer; @@ -4191,7 +4257,7 @@ procedure TDBConnection.PrefetchCreateCode(Objects: TDBObjectList); // SHOW CREATE TRIGGER was introduced in MySQL 5.1.21 // See #111 if Obj.NodeType = lntTrigger then - UseIt := UseIt and Has(frShowCreateTrigger); + UseIt := UseIt and FSqlProvider.Has(qShowCreateTrigger); if UseIt then Queries.Add('SHOW CREATE '+UpperCase(Obj.ObjType)+' '+QuoteIdent(Obj.Database)+'.'+QuoteIdent(Obj.Name)); end; @@ -4223,7 +4289,6 @@ procedure TDBConnection.PrefetchCreateCode(Objects: TDBObjectList); procedure TDBConnection.SetDatabase(Value: String); var s: String; - UseQuery: String; begin Log(lcDebug, 'SetDatabase('+Value+'), FDatabase: '+FDatabase); if Value <> FDatabase then begin @@ -4243,9 +4308,8 @@ procedure TDBConnection.SetDatabase(Value: String); s := s + ', ' + EscapeString('public'); end else s := QuoteIdent(Value); - UseQuery := FSqlProvider.GetSql(qUSEQuery); - if not UseQuery.IsEmpty then begin - Query(FSqlProvider.GetSql(qUSEQuery, [s]), False); + if FSqlProvider.Has(qUSEQuery) then begin + Query(qUSEQuery, [s]); end; FDatabase := DeQuoteIdent(Value); if Assigned(FOnDatabaseChanged) then @@ -4610,8 +4674,8 @@ procedure TMySQLConnection.ShowWarnings; begin // Log warnings // SHOW WARNINGS is implemented as of MySQL 4.1.0 - if (WarningCount > 0) and Has(frShowWarnings) then begin - Warnings := GetResults('SHOW WARNINGS'); + if (WarningCount > 0) and FSqlProvider.Has(qShowWarnings) then begin + Warnings := GetResults(FSqlProvider.GetSql(qShowWarnings)); while not Warnings.Eof do begin Log(lcError, _(Warnings.Col('Level')) + ': ('+Warnings.Col('Code')+') ' + Warnings.Col('Message')); Warnings.Next; @@ -5664,7 +5728,7 @@ function TDBConnection.GetTableColumns(Table: TDBObject): TTableColumnList; else if ExecRegExpr('\bauto_increment\b', ExtraText.ToLowerInvariant) then begin // MySQL auto increment Col.DefaultType := cdtAutoInc; - Col.DefaultText := Col.AutoIncName; + Col.DefaultText := FSqlProvider.GetSql(qAutoInc); end else if DefText.ToLowerInvariant = 'null' then begin Col.DefaultType := cdtNull; @@ -5755,7 +5819,7 @@ function TMySQLConnection.GetTableColumns(Table: TDBObject): TTableColumnList; Col.OnUpdateType := cdtNothing; if ExecRegExpr('^auto_increment$', ExtraText.ToLowerInvariant) then begin Col.DefaultType := cdtAutoInc; - Col.DefaultText := Col.AutoIncName; + Col.DefaultText := FSqlProvider.GetSql(qAutoInc); end else if ColQuery.IsNull('Default') then begin Col.DefaultType := cdtNothing; end else if IsTextDefault(DefText, Col.DataType) then begin @@ -6000,6 +6064,10 @@ function TMySQLConnection.GetTableKeys(Table: TDBObject): TTableKeyList; if ExecRegExpr('(BTREE|HASH)', KeyQuery.Col('Index_type')) then NewKey.Algorithm := KeyQuery.Col('Index_type'); NewKey.Comment := KeyQuery.Col('Index_comment', True); + if KeyQuery.ColumnExists('Visible') then // mysql 8 + NewKey.Visible := SameText(KeyQuery.Col('Visible'), 'yes') + else if KeyQuery.ColumnExists('Ignored') then // mariadb 10.6 + NewKey.Visible := SameText(KeyQuery.Col('Ignored'), 'NO'); end; if KeyQuery.ColumnExists('Expression') and (not KeyQuery.IsNull('Expression')) then begin // Functional key part: enclose expression within parentheses to distinguish them from columns (issue #1777) @@ -6495,19 +6563,11 @@ function TDBConnection.Has(Item: TFeatureOrRequirement): Boolean; ngMySQL: case Item of frSrid: Result := FParameters.IsMySQL(True) and (ServerVersionInt >= 80000); - frTimezoneVar: Result := ServerVersionInt >= 40103; frTemporalTypesFraction: Result := (FParameters.IsMariaDB and (ServerVersionInt >= 50300)) or (FParameters.IsMySQL(True) and (ServerVersionInt >= 50604)); - frShowCreateTrigger: Result := ServerVersionInt >= 50121; - frShowWarnings: Result := ServerVersionInt >= 40100; frIntegerDisplayWidth: Result := (FParameters.IsMySQL(True) and (ServerVersionInt < 80017)) or (not FParameters.IsMySQL(True)); - frShowFunctionStatus: Result := (not Parameters.IsProxySQLAdmin) and (ServerVersionInt >= 50000); - frShowProcedureStatus: Result := (not FParameters.IsProxySQLAdmin) and (ServerVersionInt >= 50000); - frShowTriggers: Result := (not FParameters.IsProxySQLAdmin) and (ServerVersionInt >= 50010); - frShowEvents: Result := (not Parameters.IsProxySQLAdmin) and (ServerVersionInt >= 50100); frColumnDefaultParentheses: Result := FParameters.IsMySQL(True) and (ServerVersionInt >= 80013); - frHelpKeyword: Result := (not FParameters.IsProxySQLAdmin) and (ServerVersionInt >= 40100); frEditVariables: Result := ServerVersionInt >= 40003; frCreateView: Result := ServerVersionInt >= 50001; frCreateProcedure: Result := ServerVersionInt >= 50003; @@ -6969,8 +7029,8 @@ procedure TMySQLConnection.FetchDbObjects(db: String; var Cache: TDBObjectList); end; // Stored functions - if Has(frShowFunctionStatus) then try - Results := GetResults('SHOW FUNCTION STATUS WHERE '+QuoteIdent('Db')+'='+EscapeString(db)); + if FSqlProvider.Has(qShowFunctionStatus) then try + Results := GetResults(FSqlProvider.GetSql(qShowFunctionStatus, [EscapeString(db)])); except on E:EDbError do; end; @@ -6990,8 +7050,8 @@ procedure TMySQLConnection.FetchDbObjects(db: String; var Cache: TDBObjectList); end; // Stored procedures - if Has(frShowProcedureStatus) then try - Results := GetResults('SHOW PROCEDURE STATUS WHERE '+QuoteIdent('Db')+'='+EscapeString(db)); + if FSqlProvider.Has(qShowProcedureStatus) then try + Results := GetResults(FSqlProvider.GetSql(qShowProcedureStatus, [EscapeString(db)])); except on E:EDbError do; end; @@ -7011,8 +7071,8 @@ procedure TMySQLConnection.FetchDbObjects(db: String; var Cache: TDBObjectList); end; // Triggers - if Has(frShowTriggers) then try - Results := GetResults('SHOW TRIGGERS FROM '+QuoteIdent(db)); + if FSqlProvider.Has(qShowTriggers) then try + Results := GetResults(FSqlProvider.GetSql(qShowTriggers, [QuoteIdent(db)])); except on E:EDbError do; end; @@ -7031,9 +7091,8 @@ procedure TMySQLConnection.FetchDbObjects(db: String; var Cache: TDBObjectList); end; // Events - if Has(frShowEvents) then try - Results := GetResults('SELECT *, EVENT_SCHEMA AS '+QuoteIdent('Db')+', EVENT_NAME AS '+QuoteIdent('Name')+ - ' FROM '+InfSch+'.'+QuoteIdent('EVENTS')+' WHERE '+QuoteIdent('EVENT_SCHEMA')+'='+EscapeString(db)) + if FSqlProvider.Has(qShowEvents) then try + Results := GetResults(FSqlProvider.GetSql(qShowEvents, [EscapeString(db)])); except on E:EDbError do begin try @@ -8846,8 +8905,8 @@ function TPGQuery.Col(Column: Integer; IgnoreErrors: Boolean=False): String; SetString(AnsiStr, FConnection.Lib.PQgetvalue(FCurrentResults, FRecNoLocal, Column), FColumnLengths[Column]); if Datatype(Column).Category in [dtcBinary, dtcSpatial] then Result := String(AnsiStr) - else if Datatype(Column).Index = dbdtBool then - if AnsiStr='t' then Result := 'true' else Result := 'false' + else if (Datatype(Column).Index = dbdtBool) and (Length(AnsiStr) > 0) then + Result := IfThen(AnsiStr='t', 'true', 'false') else Result := Connection.DecodeAPIString(AnsiStr); end; @@ -8964,19 +9023,33 @@ function TDBQuery.ValueList(Column: Integer): TStringList; i: Integer; begin Result := TStringList.Create; - Result.QuoteChar := ''''; - Result.Delimiter := ','; ColAttr := ColAttributes(Column); if Assigned(ColAttr) then case ColAttr.DataType.Index of + dbdtEnum, dbdtSet: begin - Result.DelimitedText := ColAttr.LengthSet; - // Take care for escaped ENUM definitions, see issue #799 + // Lool up PostgreSQL enum labels in prefetched list + i := FConnection.NamedEnums.IndexOfName(ColAttr.LengthSet); + if i > -1 then begin + Result.Delimiter := '|'; + Result.DelimitedText := FConnection.NamedEnums.ValueFromIndex[i]; + end + else begin + // .. or in MySQL Length/Set + Result.QuoteChar := ''''; + Result.Delimiter := ','; + Result.DelimitedText := ColAttr.LengthSet; + end; + // In any case, take care for escaped ENUM definitions, see issue #799 for i:=0 to Result.Count-1 do begin Result[i] := FConnection.UnescapeString(Result[i]); end; end; - dbdtBool: + + dbdtBool: begin + Result.Delimiter := ','; Result.DelimitedText := 'true,false'; + end; + end; end; @@ -10422,6 +10495,7 @@ function TDBObject.AsStringMap: TStringMap; FMap.Add('QuotedDatabase', QuotedDatabase); FMap.Add('QuotedName', QuotedName); FMap.Add('QuotedDbAndTableName', QuotedDbAndTableName); + FMap.Add('ObjType', UpperCase(ObjType)); Result := FMap; end; @@ -10594,25 +10668,29 @@ function TTableColumn.SQLCode(OverrideCollation: String=''; Parts: TColumnParts= end; if InParts(cpType) then begin - case FConnection.Parameters.NetTypeGroup of - ngPgSQL: begin - if DefaultType = cdtAutoInc then - Result := Result + 'SERIAL' - else - Result := Result + DataType.Name; + + if FConnection.Parameters.IsAnyPostgreSQL and (DefaultType = cdtAutoInc) then begin + Result := Result + 'SERIAL'; + end + else begin + + if (DataType.Index = dbdtEnum) and (FConnection.NamedEnums.IndexOfName(LengthSet) > -1) then begin + Result := Result + LengthSet; + end + else begin + Result := Result + DataType.Name; + if (LengthSet <> '') and DataType.HasLength then + Result := Result + '(' + LengthSet + ')'; end; - else Result := Result + DataType.Name; - end; - if (LengthSet <> '') and DataType.HasLength then - Result := Result + '(' + LengthSet + ')'; - if (DataType.Category in [dtcInteger, dtcReal]) and Unsigned then - Result := Result + ' UNSIGNED'; - if (DataType.Category in [dtcInteger, dtcReal]) and ZeroFill then - Result := Result + ' ZEROFILL'; - if Compressed and FConnection.Parameters.IsMariaDB then - Result := Result + ' /*!100301 COMPRESSED*/'; - Result := Result + ' '; // Add space after each part + if (DataType.Category in [dtcInteger, dtcReal]) and Unsigned then + Result := Result + ' UNSIGNED'; + if (DataType.Category in [dtcInteger, dtcReal]) and ZeroFill then + Result := Result + ' ZEROFILL'; + if Compressed and FConnection.Parameters.IsMariaDB then + Result := Result + ' /*!100301 COMPRESSED*/'; + Result := Result + ' '; // Add space after each part + end; end; if InParts(cpAllowNull) and (not IsVirtual) and (not FConnection.Parameters.IsAnyMSSQL) then begin @@ -10637,7 +10715,7 @@ function TTableColumn.SQLCode(OverrideCollation: String=''; Parts: TColumnParts= cdtAutoInc: begin case FConnection.Parameters.NetTypeGroup of ngPgSQL:; - else Result := Result + AutoIncName; + else Result := Result + FConnection.SqlProvider.GetSql(qAutoInc); end; end; cdtExpression: begin @@ -10703,7 +10781,9 @@ procedure TTableColumn.ParseDatatype(Source: String); var InLiteral: Boolean; ParenthLeft, i: Integer; + OrgSource: String; begin + OrgSource := Source; DataType := Connection.GetDatatypeByName(Source, True); // Length / Set // Various datatypes, e.g. BLOBs, don't have any length property @@ -10722,6 +10802,11 @@ procedure TTableColumn.ParseDatatype(Source: String); LengthSet := ''; end else begin LengthSet := ''; + if DataType.Index = dbdtEnum then begin + // Assign PostgreSQL enum type to LengthSet, so we can provide it in table editor + // Some enum types are wrapped in double quotes + LengthSet := OrgSource.Trim([FConnection.QuoteChar]); + end; end; Unsigned := ExecRegExpr('\bunsigned\b', Source.ToLowerInvariant); ZeroFill := ExecRegExpr('\bzerofill\b', Source.ToLowerInvariant); @@ -10752,20 +10837,15 @@ function TTableColumn.CastAsText: String; end; -function TTableColumn.AutoIncName: String; -begin - case FConnection.Parameters.NetTypeGroup of - ngPgSQL: Result := 'SERIAL'; - else Result := 'AUTO_INCREMENT'; - end; -end; - - function TTableColumn.FullDataType: String; begin Result := DataType.Name; - if not LengthSet.IsEmpty then - Result := Result + '(' + LengthSet + ')'; + if not LengthSet.IsEmpty then begin + if (DataType.Index = dbdtEnum) and (FConnection.NamedEnums.IndexOfName(LengthSet) > -1) then + Result := LengthSet + else + Result := Result + '(' + LengthSet + ')'; + end; end; @@ -10808,6 +10888,7 @@ constructor TTableKey.Create(AOwner: TDBConnection); Columns.OnChange := Modification; Subparts.OnChange := Modification; Collations.OnChange := Modification; + Visible := True; end; destructor TTableKey.Destroy; @@ -10830,6 +10911,7 @@ procedure TTableKey.Assign(Source: TPersistent); OldIndexType := s.OldIndexType; Algorithm := s.Algorithm; Comment := s.Comment; + Visible := s.Visible; Columns.Assign(s.Columns); SubParts.Assign(s.SubParts); Collations.Assign(s.Collations); @@ -10947,6 +11029,7 @@ function TTableKey.SQLCode(TableName: String=''): String; if not Comment.IsEmpty then Result := Result + ' COMMENT ' + FConnection.EscapeString(Comment); + end else begin // SQLite syntax: diff --git a/source/dbstructures.interbase.pas b/source/dbstructures.interbase.pas index 461f1f883..52ba541fa 100644 --- a/source/dbstructures.interbase.pas +++ b/source/dbstructures.interbase.pas @@ -186,7 +186,7 @@ function TInterbaseProvider.GetSql(AId: TQueryId): string; begin case AId of qDatabaseDrop: Result := 'DROP DATABASE %s'; - qEmptyTable: Result := 'TRUNCATE '; + qEmptyTable: Result := 'TRUNCATE %s'; qRenameTable: Result := 'RENAME TABLE %s TO %s'; qRenameView: Result := 'RENAME TABLE %s TO %s'; qCurrentUserHost: Result := IfThen( @@ -236,8 +236,9 @@ function TInterbaseProvider.GetSql(AId: TQueryId): string; ' RDB$CHARACTER_SET_ID'+ ' FROM RDB$COLLATIONS'; qGetCharsets: Result := 'SELECT RDB$CHARACTER_SET_NAME AS "Charset", RDB$CHARACTER_SET_NAME AS "Description" FROM RDB$CHARACTER_SETS'; + else Result := inherited; end; end; -end. \ No newline at end of file +end. diff --git a/source/dbstructures.mssql.pas b/source/dbstructures.mssql.pas index 1851ebbf8..a94c55f7f 100644 --- a/source/dbstructures.mssql.pas +++ b/source/dbstructures.mssql.pas @@ -450,7 +450,6 @@ function TMsSqlProvider.GetSql(AId: TQueryId): string; 'xtype', 'type' ); - qEmptyTable: Result := 'DELETE FROM '; qRenameTable: Result := 'EXEC sp_rename %s, %s'; qRenameView: Result := 'EXEC sp_rename %s, %s'; qCurrentUserHost: Result := 'SELECT SYSTEM_USER'; diff --git a/source/dbstructures.mysql.pas b/source/dbstructures.mysql.pas index 5a0bcd62c..48c6a9fff 100644 --- a/source/dbstructures.mysql.pas +++ b/source/dbstructures.mysql.pas @@ -3247,7 +3247,7 @@ function TMySqlProvider.GetSql(AId: TQueryId): string; begin case AId of qDatabaseDrop: Result := 'DROP DATABASE %s'; - qEmptyTable: Result := 'TRUNCATE '; + qEmptyTable: Result := 'TRUNCATE %s'; qRenameTable: Result := 'RENAME TABLE %s TO %s'; qRenameView: Result := 'RENAME TABLE %s TO %s'; qCurrentUserHost: Result := 'SELECT CURRENT_USER()'; @@ -3326,7 +3326,7 @@ function TMySqlProvider.GetSql(AId: TQueryId): string; ', ID AS `Id`'+ ', IS_DEFAULT AS `Default`'+ ', 0 AS `Sortlen`'+ - ' FROM information_schema.COLLATION_CHARACTER_SET_APPLICABILITY'+ + ' FROM INFORMATION_SCHEMA.COLLATION_CHARACTER_SET_APPLICABILITY'+ ' ORDER BY `Collation`', '' ); @@ -3346,6 +3346,80 @@ function TMySqlProvider.GetSql(AId: TQueryId): string; ' WHERE'+ ' REFERENCED_TABLE_SCHEMA = :EscapedDatabase AND'+ ' REFERENCED_TABLE_NAME = :EscapedName'; + qExplain: Result := IfThen( + (FServerVersion >= 80400) and (FServerVersion < 100000), // Not MariaDB + 'EXPLAIN FORMAT=TRADITIONAL %s', + 'EXPLAIN %s' + ); + qSetTimezone: Result := IfThen( + FServerVersion >= 40103, + 'SET time_zone=%s', + '' + ); + qShowFunctionStatus: Result := IfThen( + (FServerVersion >= 50000) and (FNetType <> ntMySQL_ProxySQLAdmin), + 'SHOW FUNCTION STATUS WHERE Db = %s', + '' + ); + qShowProcedureStatus: Result := IfThen( + (FServerVersion >= 50000) and (FNetType <> ntMySQL_ProxySQLAdmin), + 'SHOW PROCEDURE STATUS WHERE Db = %s', + '' + ); + qShowTriggers: Result := IfThen( + (FServerVersion >= 50010) and (FNetType <> ntMySQL_ProxySQLAdmin), + 'SHOW TRIGGERS FROM %s', + '' + ); + qShowEvents: Result := IfThen( + (FServerVersion >= 50010) and (FNetType <> ntMySQL_ProxySQLAdmin), + 'SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM INFORMATION_SCHEMA.`EVENTS` WHERE EVENT_SCHEMA=%s', + '' + ); + qHelpKeyword: Result := IfThen( + (FServerVersion >= 40100) and (FNetType <> ntMySQL_ProxySQLAdmin), + 'HELP %s', + '' + ); + qShowCreateTrigger: Result := IfThen( + FServerVersion >= 50121, + 'SHOW CREATE TRIGGER :QuotedDatabase.:QuotedName', + '' + ); + qShowWarnings: Result := IfThen( + FServerVersion >= 40100, + 'SHOW WARNINGS', + '' + ); + qDropUser: Result := IfThen( + FServerVersion < 40101, + 'DELETE FROM mysql.user WHERE User=%s AND Host=%s', + 'DROP USER %s@%s' + ); + qCreateRole: Result := 'CREATE ROLE %s'; + qDropRole: Result := 'DROP ROLE %s'; + qReloadPrivileges: Result := 'FLUSH PRIVILEGES'; + qGrantRole: Result := 'GRANT %s TO %s%s'; + qRevokeRole: Result := 'REVOKE %s FROM %s'; + qSetDefaultRole: Result := 'SET DEFAULT ROLE %s FOR %s'; + qIndexVisible: Result := IfThen( + FServerVersion >= 100600, // mariadb + 'NOT IGNORED', + IfThen( + FServerVersion >= 80000, // mysql + 'VISIBLE', + '' + ) + ); + qIndexInvisible: Result := IfThen( + FServerVersion >= 100600, // mariadb + 'IGNORED', + IfThen( + FServerVersion >= 80000, // mysql + 'INVISIBLE', + '' + ) + ); else Result := inherited; end; end; diff --git a/source/dbstructures.pas b/source/dbstructures.pas index 6a62375f8..b3a20d3cf 100644 --- a/source/dbstructures.pas +++ b/source/dbstructures.pas @@ -46,7 +46,11 @@ interface qLockedTables, qDisableForeignKeyChecks, qEnableForeignKeyChecks, qOrderAsc, qOrderDesc, qGetRowCountExact, qGetRowCountApprox, qForeignKeyDrop, qGetTableColumns, qGetCollations, qGetCollationsExtended, qGetCharsets, - qGetReverseForeignKeys); + qGetReverseForeignKeys, qExplain, qSetTimezone, + qShowFunctionStatus, qShowProcedureStatus, qShowTriggers, qShowEvents, qShowCreateTrigger, + qHelpKeyword, qShowWarnings, qGetEnumTypes, + qDropUser, qCreateRole, qDropRole, qReloadPrivileges, qGrantRole, qRevokeRole, qSetDefaultRole, + qAutoInc, qIndexVisible, qIndexInvisible); TSqlProvider = class strict protected FNetType: TNetType; @@ -202,10 +206,12 @@ function TSqlProvider.GetSql(AId: TQueryId): string; begin // Basic default SQL snippets compatible to all or most servers case AId of + qEmptyTable: Result := 'DELETE FROM %s'; qForeignKeyEventAction: Result := 'RESTRICT,CASCADE,SET NULL,NO ACTION'; qOrderAsc: Result := 'ASC'; qOrderDesc: Result := 'DESC'; qGetRowCountExact: Result := 'SELECT COUNT(*) FROM :QuotedDbAndTableName'; + qAutoInc: Result := 'AUTO_INCREMENT'; else Result := ''; end; end; diff --git a/source/dbstructures.postgresql.pas b/source/dbstructures.postgresql.pas index a01de8762..e0c45003b 100644 --- a/source/dbstructures.postgresql.pas +++ b/source/dbstructures.postgresql.pas @@ -46,7 +46,7 @@ TPostgreSQLProvider = class(TSqlProvider) const InvalidOid: POid = 0; var - PostgreSQLDatatypes: Array[0..38] of TDBDatatype = + PostgreSQLDatatypes: Array[0..39] of TDBDatatype = ( ( Index: dbdtUnknown; @@ -511,6 +511,19 @@ TPostgreSQLProvider = class(TSqlProvider) LoadPart: False; Category: dtcOther; ), + ( + Index: dbdtEnum; + NativeTypes: 'e'; + Name: 'ENUM'; + Names: ''; + Description: 'A list of quoted labels, each of which must be less than NAMEDATALEN bytes long (64 bytes in a standard PostgreSQL build)'; + HasLength: True; // Enables the Length/set field in table editor + RequiresLength: True; + HasBinary: False; + HasDefault: True; + LoadPart: False; + Category: dtcOther; + ), ( Index: dbdtJson; NativeTypes: '114'; @@ -586,7 +599,6 @@ function TPostgreSQLProvider.GetSql(AId: TQueryId): string; begin case AId of qDatabaseDrop: Result := 'DROP SCHEMA %s'; - qEmptyTable: Result := 'DELETE FROM '; qRenameTable: Result := 'ALTER TABLE %s RENAME TO %s'; qRenameView: Result := 'ALTER VIEW %s RENAME TO %s'; qCurrentUserHost: Result := 'SELECT CURRENT_USER'; @@ -716,6 +728,23 @@ function TPostgreSQLProvider.GetSql(AId: TQueryId): string; ' WHERE pg_class.relkind=''r'''+ ' AND pg_namespace.nspname=:EscapedDatabase'+ ' AND pg_class.relname=:EscapedName'; + qGetEnumTypes: Result := IfThen( + FServerVersion >= 90000, + 'SELECT ' + + ' n.nspname AS enum_schema, ' + + ' t.typname AS enum_name, ' + + ' string_agg(e.enumlabel, ''|'' ORDER BY e.enumsortorder) AS enum_labels ' + + 'FROM pg_type AS t ' + + 'JOIN pg_enum AS e ' + + ' ON t.oid = e.enumtypid ' + + 'JOIN pg_namespace AS n ' + + ' ON n.oid = t.typnamespace ' + + 'WHERE t.typtype = ''e'' ' + + 'GROUP BY n.nspname, t.typname ' + + 'ORDER BY UPPER(t.typname)', + '' // ServerVersion < 9 + ); + qAutoInc: Result := 'SERIAL'; else Result := inherited; end; end; diff --git a/source/dbstructures.sqlite.pas b/source/dbstructures.sqlite.pas index 13c6efc48..e5f1eb90b 100644 --- a/source/dbstructures.sqlite.pas +++ b/source/dbstructures.sqlite.pas @@ -401,7 +401,6 @@ function TSQLiteProvider.GetSql(AId: TQueryId): string; begin case AId of qDatabaseDrop: Result := 'DROP DATABASE %s'; - qEmptyTable: Result := 'DELETE FROM '; qRenameTable: Result := 'ALTER TABLE %s RENAME TO %s'; qRenameView: Result := 'ALTER TABLE %s RENAME TO %s'; qCurrentUserHost: Result := ''; // unsupported diff --git a/source/exportgrid.pas b/source/exportgrid.pas index 40ffc24aa..1105a5ef8 100644 --- a/source/exportgrid.pas +++ b/source/exportgrid.pas @@ -724,15 +724,15 @@ procedure TfrmExportGrid.btnOKClick(Sender: TObject); CodeIndent(2) + '' + TableName + '' + sLineBreak + CodeIndent(2) + '' + sLineBreak + CodeIndent(2) + '' + sLineBreak + + CodeIndent(2) + '' + sLineBreak + CodeIndent(2) + '