From 14f5468af21530689802c4a3f73d9d669ffd10c7 Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Mon, 16 Feb 2026 20:45:23 +0100 Subject: [PATCH] refactor: outsource queries from GetRowCount into TSqlProvider This required to introduce a third version of TSqlProvider.GetSql which works with named parameters packed into a TStringMap --- source/dbconnection.pas | 93 ++++++++++++------------------ source/dbstructures.mssql.pas | 5 ++ source/dbstructures.mysql.pas | 5 ++ source/dbstructures.pas | 24 +++++++- source/dbstructures.postgresql.pas | 5 ++ 5 files changed, 72 insertions(+), 60 deletions(-) diff --git a/source/dbconnection.pas b/source/dbconnection.pas index 72731202..fa456df1 100644 --- a/source/dbconnection.pas +++ b/source/dbconnection.pas @@ -166,6 +166,7 @@ type FCreateCodeLoaded: Boolean; FWasSelected: Boolean; FConnection: TDBConnection; + FMap: TStringMap; function GetObjType: String; function GetImageIndex: Integer; function GetOverlayImageIndex: Integer; @@ -185,6 +186,7 @@ type NodeType, GroupType: TListNodeType; constructor Create(OwnerConnection: TDBConnection); + destructor Destroy; procedure Assign(Source: TPersistent); override; procedure UnloadDetails; procedure Drop; @@ -197,6 +199,7 @@ type function RowCount(Reload: Boolean; ForceExact: Boolean=False): Int64; function GetCreateCode: String; overload; function GetCreateCode(RemoveAutoInc, RemoveDefiner: Boolean): String; overload; + function AsStringMap: TStringMap; property ObjType: String read GetObjType; property ImageIndex: Integer read GetImageIndex; property OverlayImageIndex: Integer read GetOverlayImageIndex; @@ -417,9 +420,9 @@ type 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, frKillQuery, - frLockedTables, frShowCreateTrigger, frShowWarnings, - frShowCharset, frIntegerDisplayWidth, frShowFunctionStatus, frShowProcedureStatus, + TFeatureOrRequirement = (frSrid, frTimezoneVar, frTemporalTypesFraction, + frShowCreateTrigger, frShowWarnings, + frIntegerDisplayWidth, frShowFunctionStatus, frShowProcedureStatus, frShowTriggers, frShowEvents, frColumnDefaultParentheses, frHelpKeyword, frEditVariables, frCreateView, frCreateProcedure, frCreateFunction, frCreateTrigger, frCreateEvent, frInvisibleColumns, frCompressedColumns); @@ -644,7 +647,6 @@ type function GetAllDatabases: TStringList; override; function GetTableEngines: TStringList; override; function GetCreateViewCode(Database, Name: String): String; - function GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; override; procedure FetchDbObjects(db: String; var Cache: TDBObjectList); override; public constructor Create(AOwner: TComponent); override; @@ -674,7 +676,6 @@ type function GetLastErrorCode: Cardinal; override; function GetLastErrorMsg: String; override; function GetAllDatabases: TStringList; override; - function GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; override; procedure FetchDbObjects(db: String; var Cache: TDBObjectList); override; public constructor Create(AOwner: TComponent); override; @@ -714,7 +715,6 @@ type function Ping(Reconnect: Boolean): Boolean; override; function GetCreateCode(Obj: TDBObject): String; override; function ConnectionInfo: TStringList; override; - function GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; override; property LastRawResults: TPGRawResults read FLastRawResults; property RegClasses: TOidStringPairs read FRegClasses; function GetTableKeys(Table: TDBObject): TTableKeyList; override; @@ -6621,11 +6621,8 @@ begin frTimezoneVar: Result := ServerVersionInt >= 40103; frTemporalTypesFraction: Result := (FParameters.IsMariaDB and (ServerVersionInt >= 50300)) or (FParameters.IsMySQL(True) and (ServerVersionInt >= 50604)); - frKillQuery: Result := (not FParameters.IsMySQLonRDS) and (ServerVersionInt >= 50000); - frLockedTables: Result := (not FParameters.IsProxySQLAdmin) and (ServerVersionInt >= 50124); frShowCreateTrigger: Result := ServerVersionInt >= 50121; frShowWarnings: Result := ServerVersionInt >= 40100; - frShowCharset: Result := ServerVersionInt >= 40100; frIntegerDisplayWidth: Result := (FParameters.IsMySQL(True) and (ServerVersionInt < 80017)) or (not FParameters.IsMySQL(True)); frShowFunctionStatus: Result := (not Parameters.IsProxySQLAdmin) and (ServerVersionInt >= 50000); @@ -6651,59 +6648,20 @@ end; function TDBConnection.GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; var - Rows: String; + Rows, QueryApprox, QueryExact: String; + RowsColumn: Integer; begin // Get row number from a table - Rows := GetVar('SELECT COUNT(*) FROM '+QuotedDbAndTableName(Obj.Database, Obj.Name), 0); - Result := MakeInt(Rows); -end; - - -function TMySQLConnection.GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; -var - Rows: String; -begin - // Get row number from a mysql table - if Parameters.IsProxySQLAdmin or ForceExact then begin - Result := inherited + QueryApprox := FSqlProvider.GetSql(qGetRowCountApprox, Obj.AsStringMap); + if QueryApprox.IsEmpty or ForceExact then begin + QueryExact := FSqlProvider.GetSql(qGetRowCountExact, Obj.AsStringMap); + Rows := GetVar(QueryExact); end else begin - Rows := GetVar('SHOW TABLE STATUS LIKE '+EscapeString(Obj.Name), 'Rows'); - Result := MakeInt(Rows); + // This is ugly: in MySQL 4.x we only have SHOW TABLE STATUS, which cannot be limited to the "Rows" column + RowsColumn := IfThen(QueryApprox.StartsWith('SHOW ', True), 4, 0); + Rows := GetVar(QueryApprox, RowsColumn); end; -end; - - -function TAdoDBConnection.GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; -var - Rows: String; -begin - // Get row number from a mssql table - if (ServerVersionInt < 900) or ForceExact then begin - Result := inherited - end - else begin - Rows := GetVar('SELECT SUM('+QuoteIdent('rows')+') FROM '+QuoteIdent('sys')+'.'+QuoteIdent('partitions')+ - ' WHERE '+QuoteIdent('index_id')+' IN (0, 1)'+ - ' AND '+QuoteIdent('object_id')+' = object_id('+EscapeString(Obj.Database+'.'+Obj.Schema+'.'+Obj.Name)+')' - ); - Result := MakeInt(Rows); - end; -end; - - -function TPgConnection.GetRowCount(Obj: TDBObject; ForceExact: Boolean=False): Int64; -var - Rows: String; -begin - // Get row number from a postgres table - Rows := GetVar('SELECT '+QuoteIdent('reltuples')+'::bigint FROM '+QuoteIdent('pg_class')+ - ' LEFT JOIN '+QuoteIdent('pg_namespace')+ - ' ON ('+QuoteIdent('pg_namespace')+'.'+QuoteIdent('oid')+' = '+QuoteIdent('pg_class')+'.'+QuoteIdent('relnamespace')+')'+ - ' WHERE '+QuoteIdent('pg_class')+'.'+QuoteIdent('relkind')+'='+EscapeString('r')+ - ' AND '+QuoteIdent('pg_namespace')+'.'+QuoteIdent('nspname')+'='+EscapeString(Obj.Database)+ - ' AND '+QuoteIdent('pg_class')+'.'+QuoteIdent('relname')+'='+EscapeString(Obj.Name) - ); Result := MakeInt(Rows); end; @@ -10183,6 +10141,13 @@ begin FCreateCodeLoaded := False; FWasSelected := False; FConnection := OwnerConnection; + FMap := TStringMap.Create; +end; + +destructor TDBObject.Destroy; +begin + FMap.Free; + inherited; end; @@ -10532,6 +10497,20 @@ begin Result.Assign(CheckConstraintsInCache); end; +function TDBObject.AsStringMap: TStringMap; +begin + FMap.Clear; + FMap.Add('EscapedName', FConnection.EscapeString(Name)); + FMap.Add('EscapedSchema', FConnection.EscapeString(Schema)); + FMap.Add('EscapedDatabase', FConnection.EscapeString(Database)); + FMap.Add('EscapedDbSchemaName', FConnection.EscapeString(Database+'.'+Schema+'.'+Name)); + FMap.Add('QuotedDatabase', QuotedDatabase); + FMap.Add('QuotedName', QuotedName); + FMap.Add('QuotedDbAndTableName', QuotedDbAndTableName); + Result := FMap; +end; + + { *** TTableColumn } diff --git a/source/dbstructures.mssql.pas b/source/dbstructures.mssql.pas index 370cc9fb..41d033eb 100644 --- a/source/dbstructures.mssql.pas +++ b/source/dbstructures.mssql.pas @@ -476,6 +476,11 @@ begin ''''' AS "Default", '''' AS "Compiled", '+ '1 AS "Sortlen"'; qGetCharsets: Result := 'SELECT name AS Charset, description AS Description FROM master.sys.syscharsets'; + qGetRowCountApprox: Result := IfThen( + FServerVersion >= 900, + 'SELECT SUM("rows") FROM "sys"."partitions" WHERE "index_id" IN (0, 1) AND "object_id" = object_id(:EscapedDbSchemaName)', + '' + ); else Result := inherited; end; end; diff --git a/source/dbstructures.mysql.pas b/source/dbstructures.mysql.pas index c7f0eef2..4158097b 100644 --- a/source/dbstructures.mysql.pas +++ b/source/dbstructures.mysql.pas @@ -3316,6 +3316,11 @@ begin 'SHOW CHARSET', '' ); + qGetRowCountApprox: Result := IfThen( + FNetType <> ntMySQL_ProxySQLAdmin, + 'SHOW TABLE STATUS LIKE :EscapedName', + '' + ); else Result := inherited; end; end; diff --git a/source/dbstructures.pas b/source/dbstructures.pas index 0bb59292..2c5630bc 100644 --- a/source/dbstructures.pas +++ b/source/dbstructures.pas @@ -36,6 +36,7 @@ type TNetTypeLibs = TDictionary; // SQL query ids and provider + TStringMap = TDictionary; TQueryId = (qDatabaseTable, qDatabaseTableId, qDatabaseDrop, qDbObjectsTable, qDbObjectsCreateCol, qDbObjectsUpdateCol, qDbObjectsTypeCol, qEmptyTable, qRenameTable, qRenameView, qCurrentUserHost, qLikeCompare, @@ -45,7 +46,7 @@ type qUSEQuery, qKillQuery, qKillProcess, qFuncLength, qFuncCeil, qFuncLeft, qFuncNow, qFuncLastAutoIncNumber, qLockedTables, qDisableForeignKeyChecks, qEnableForeignKeyChecks, - qOrderAsc, qOrderDesc, + qOrderAsc, qOrderDesc, qGetRowCountExact, qGetRowCountApprox, qForeignKeyDrop, qGetTableColumns, qGetCollations, qGetCollationsExtended, qGetCharsets); TSqlProvider = class strict protected @@ -54,8 +55,12 @@ type public constructor Create(ANetType: TNetType); function Has(AId: TQueryId): Boolean; + // Base version, just returns the original SQL string function GetSql(AId: TQueryId): string; overload; virtual; + // Version for simple strings passed to Format() function GetSql(AId: TQueryId; const Args: array of const): string; overload; + // Version for named parameters + function GetSql(AId: TQueryId; NamedParameters: TStringMap): string; overload; property ServerVersion: Integer read FServerVersion write FServerVersion; end; @@ -203,6 +208,7 @@ begin qForeignKeyEventAction: Result := 'RESTRICT,CASCADE,SET NULL,NO ACTION'; qOrderAsc: Result := 'ASC'; qOrderDesc: Result := 'DESC'; + qGetRowCountExact: Result := 'SELECT COUNT(*) FROM :QuotedDbAndTableName'; else Result := ''; end; end; @@ -210,10 +216,22 @@ end; function TSqlProvider.GetSql(AId: TQueryId; const Args: array of const): string; begin Result := GetSql(AId); - if not Result.IsEmpty then - Result := Format(Result, Args); + if Result.IsEmpty then + Exit; + Result := Format(Result, Args); end; +function TSqlProvider.GetSql(AId: TQueryId; NamedParameters: TStringMap): string; +var + Key: String; +begin + Result := GetSql(AId); + if Result.IsEmpty then + Exit; + for Key in NamedParameters.Keys do begin + Result := StringReplace(Result, ':'+Key, NamedParameters[Key], [rfReplaceAll]); + end; +end; diff --git a/source/dbstructures.postgresql.pas b/source/dbstructures.postgresql.pas index 31cc1abe..cb71fca6 100644 --- a/source/dbstructures.postgresql.pas +++ b/source/dbstructures.postgresql.pas @@ -709,6 +709,11 @@ begin '(SELECT conforencoding AS enc FROM pg_catalog.pg_conversion '+ ' UNION '+ ' SELECT contoencoding AS enc FROM pg_catalog.pg_conversion) AS x'; + qGetRowCountApprox: Result := 'SELECT reltuples::bigint FROM pg_class'+ + ' LEFT JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace'+ + ' WHERE pg_class.relkind=''r'''+ + ' AND pg_namespace.nspname=:EscapedDatabase'+ + ' AND pg_class.relname=:EscapedName'; else Result := inherited; end; end;