refactor: outsource 3 large queries from GetTableColumns into TSqlProvider

Refs #1880
This commit is contained in:
Ansgar Becker
2026-02-15 18:41:37 +01:00
parent c450f516b7
commit fecd011d86
5 changed files with 130 additions and 117 deletions

View File

@@ -5803,106 +5803,13 @@ var
Col: TTableColumn;
dt, DefText, ExtraText, MaxLen, ColSQL: String;
begin
// Generic: query table columns from IS.COLUMNS
// Generic: query table columns from IS.COLUMNS or query from provider
Log(lcDebug, 'Getting fresh columns for '+Table.QuotedDbAndTableName);
Result := TTableColumnList.Create(True);
if (FParameters.IsAnyPostgreSQL) and (ServerVersionInt >= 120000) then begin
// This uses pg_attribute.attgenerated, which only exists starting in PostgreSQL 12
// Todo: outsource such bigger SQL chunks into dbstructures units, together with the FSQLSpecifities array
ColSQL :=
'SELECT ' +
' n.nspname AS table_schema, ' +
' c.relname AS table_name, ' +
' a.attname AS column_name, ' +
' a.attnum AS ordinal_position, ' +
' pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, ' +
// YES/NO like information_schema.is_nullable
' CASE ' +
' WHEN a.attnotnull THEN ''NO'' ' +
' ELSE ''YES'' ' +
' END AS is_nullable, ' +
// Character maximum length (in characters)
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' AND a.atttypmod <> -1 ' +
' THEN a.atttypmod - 4 ' +
' ELSE NULL ' +
' END AS character_maximum_length, ' +
// Numeric precision / scale (NULL for non-numeric)
' CASE ' +
' WHEN (bt.typcategory IN (''N'',''F'')) OR (bt.oid IS NULL AND t.typcategory IN (''N'',''F'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.atttypmod = -1 THEN NULL ' +
' ELSE ((a.atttypmod - 4) >> 16)::integer ' +
' END ' +
' END AS numeric_precision, ' +
' CASE ' +
' WHEN (bt.typcategory IN (''N'',''F'')) OR (bt.oid IS NULL AND t.typcategory IN (''N'',''F'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.atttypmod = -1 THEN NULL ' +
' ELSE ((a.atttypmod - 4) & 65535)::integer ' +
' END ' +
' END AS numeric_scale, ' +
// Datetime precision (for time/timestamp/interval)
' CASE ' +
' WHEN (bt.typcategory = ''D'' OR (bt.oid IS NULL AND t.typcategory = ''D'')) ' +
' AND a.atttypmod <> -1 ' +
' THEN a.atttypmod ' +
' ELSE NULL ' +
' END AS datetime_precision, ' +
// Character set name: PostgreSQL has one per DB; mimic information_schema
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' THEN current_database() ' +
' ELSE NULL ' +
' END AS character_set_name, ' +
// Collation name for collatable columns
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.attcollation <> t.typcollation ' +
' THEN coll.collname ' +
' ELSE NULL ' +
' END ' +
' ELSE NULL ' +
' END AS collation_name, ' +
// Default expression for non-generated columns
' CASE ' +
' WHEN a.attgenerated = '''' AND a.atthasdef ' +
' THEN pg_get_expr(ad.adbin, ad.adrelid) ' +
' ELSE NULL ' +
' END AS column_default, ' +
// Generation expression for generated columns
' CASE ' +
' WHEN a.attgenerated <> '''' AND a.atthasdef ' +
' THEN pg_get_expr(ad.adbin, ad.adrelid) ' +
' ELSE NULL ' +
' END AS generation_expression, ' +
' d.description AS column_comment ' +
'FROM pg_catalog.pg_class AS c ' +
'JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace ' +
'JOIN pg_catalog.pg_attribute AS a ON a.attrelid = c.oid ' +
'JOIN pg_catalog.pg_type AS t ON t.oid = a.atttypid ' +
'LEFT JOIN pg_catalog.pg_type AS bt ON bt.oid = t.typbasetype ' +
'LEFT JOIN pg_catalog.pg_attrdef AS ad ' +
' ON ad.adrelid = a.attrelid ' +
' AND ad.adnum = a.attnum ' +
'LEFT JOIN pg_catalog.pg_description AS d ' +
' ON d.objoid = a.attrelid ' +
' AND d.objsubid = a.attnum ' +
'LEFT JOIN pg_catalog.pg_collation AS coll ' +
' ON coll.oid = a.attcollation ' +
'WHERE n.nspname = ' + EscapeString(Table.Schema) + ' ' +
' AND a.attnum > 0 ' +
' AND NOT a.attisdropped ' +
' AND c.relname = ' + EscapeString(Table.Name) + ' ' +
'ORDER BY ordinal_position';
if FSqlProvider.Has(qGetTableColumns) then begin
ColSQL := FSqlProvider.GetSql(qGetTableColumns, [EscapeString(Table.Schema), EscapeString(Table.Name)]);
end
else begin
TableIdx := InformationSchemaObjects.IndexOf('columns');
if TableIdx = -1 then begin
@@ -6152,7 +6059,7 @@ begin
// Todo: include database name
// Todo: default values
Result := TTableColumnList.Create(True);
ColQuery := GetResults('SELECT * FROM pragma_table_xinfo('+EscapeString(Table.Name)+', '+EscapeString(Table.Database)+')');
ColQuery := GetResults(FSqlProvider.GetSql(qGetTableColumns, [EscapeString(Table.Name), EscapeString(Table.Database)]));
while not ColQuery.Eof do begin
Col := TTableColumn.Create(Self);
Result.Add(Col);
@@ -6182,24 +6089,7 @@ var
begin
// Todo
Result := TTableColumnList.Create(True);
ColQuery := GetResults('SELECT r.RDB$FIELD_NAME AS field_name,'+
' r.RDB$DESCRIPTION AS field_description,'+
' r.RDB$DEFAULT_VALUE AS field_default_value,'+
' r.RDB$NULL_FLAG AS null_flag,'+
' f.RDB$FIELD_LENGTH AS field_length,'+
' f.RDB$FIELD_PRECISION AS field_precision,'+
' f.RDB$FIELD_SCALE AS field_scale,'+
' f.RDB$FIELD_TYPE AS field_type,'+
' f.RDB$FIELD_SUB_TYPE AS field_subtype,'+
' coll.RDB$COLLATION_NAME AS field_collation,'+
' cset.RDB$CHARACTER_SET_NAME AS field_charset'+
' FROM RDB$RELATION_FIELDS r'+
' LEFT JOIN RDB$FIELDS f ON r.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME'+
' LEFT JOIN RDB$CHARACTER_SETS cset ON f.RDB$CHARACTER_SET_ID = cset.RDB$CHARACTER_SET_ID'+
' LEFT JOIN RDB$COLLATIONS coll ON f.RDB$COLLATION_ID = coll.RDB$COLLATION_ID'+
' AND F.RDB$CHARACTER_SET_ID = COLL.RDB$CHARACTER_SET_ID'+
' WHERE r.RDB$RELATION_NAME='+EscapeString(Table.Name)+
' ORDER BY r.RDB$FIELD_POSITION');
ColQuery := GetResults(FSqlProvider.GetSql(qGetTableColumns, [EscapeString(Table.Name)]));
while not ColQuery.Eof do begin
Col := TTableColumn.Create(Self);
Result.Add(Col);

View File

@@ -211,6 +211,24 @@ begin
qDisableForeignKeyChecks: Result := '';
qEnableForeignKeyChecks: Result := '';
qForeignKeyDrop: Result := 'DROP FOREIGN KEY %s';
qGetTableColumns: Result := 'SELECT r.RDB$FIELD_NAME AS field_name,'+
' r.RDB$DESCRIPTION AS field_description,'+
' r.RDB$DEFAULT_VALUE AS field_default_value,'+
' r.RDB$NULL_FLAG AS null_flag,'+
' f.RDB$FIELD_LENGTH AS field_length,'+
' f.RDB$FIELD_PRECISION AS field_precision,'+
' f.RDB$FIELD_SCALE AS field_scale,'+
' f.RDB$FIELD_TYPE AS field_type,'+
' f.RDB$FIELD_SUB_TYPE AS field_subtype,'+
' coll.RDB$COLLATION_NAME AS field_collation,'+
' cset.RDB$CHARACTER_SET_NAME AS field_charset'+
' FROM RDB$RELATION_FIELDS r'+
' LEFT JOIN RDB$FIELDS f ON r.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME'+
' LEFT JOIN RDB$CHARACTER_SETS cset ON f.RDB$CHARACTER_SET_ID = cset.RDB$CHARACTER_SET_ID'+
' LEFT JOIN RDB$COLLATIONS coll ON f.RDB$COLLATION_ID = coll.RDB$COLLATION_ID'+
' AND F.RDB$CHARACTER_SET_ID = COLL.RDB$CHARACTER_SET_ID'+
' WHERE r.RDB$RELATION_NAME=%s'+
' ORDER BY r.RDB$FIELD_POSITION';
end;
end;

View File

@@ -46,13 +46,14 @@ type
qFuncLength, qFuncCeil, qFuncLeft, qFuncNow, qFuncLastAutoIncNumber,
qLockedTables, qDisableForeignKeyChecks, qEnableForeignKeyChecks,
qOrderAsc, qOrderDesc,
qForeignKeyDrop);
qForeignKeyDrop, qGetTableColumns);
TSqlProvider = class
strict protected
FNetType: TNetType;
FServerVersion: Integer;
public
constructor Create(ANetType: TNetType);
function Has(AId: TQueryId): Boolean;
function GetSql(AId: TQueryId): string; overload; virtual;
function GetSql(AId: TQueryId; const Args: array of const): string; overload;
property ServerVersion: Integer read FServerVersion write FServerVersion;
@@ -190,6 +191,11 @@ begin
FServerVersion := 0;
end;
function TSqlProvider.Has(AId: TQueryId): Boolean;
begin
Result := not GetSql(AId).IsEmpty;
end;
function TSqlProvider.GetSql(AId: TQueryId): string;
begin
// This provides default values for queries, basically MySQL syntax
@@ -257,6 +263,7 @@ begin
qOrderAsc: Result := 'ASC';
qOrderDesc: Result := 'DESC';
qForeignKeyDrop: Result := 'DROP FOREIGN KEY %s';
qGetTableColumns: Result := '';
else Result := '';
end;
end;

View File

@@ -3,7 +3,7 @@ unit dbstructures.postgresql;
interface
uses
dbstructures;
dbstructures, StrUtils;
type
// PostgreSQL structures
@@ -608,6 +608,103 @@ begin
qDisableForeignKeyChecks: Result := '';
qEnableForeignKeyChecks: Result := '';
qForeignKeyDrop: Result := 'DROP CONSTRAINT %s';
// This uses pg_attribute.attgenerated, which only exists starting in PostgreSQL 12
qGetTableColumns: Result := IfThen(
FServerVersion >= 120000,
'SELECT ' +
' n.nspname AS table_schema, ' +
' c.relname AS table_name, ' +
' a.attname AS column_name, ' +
' a.attnum AS ordinal_position, ' +
' pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type, ' +
// YES/NO like information_schema.is_nullable
' CASE ' +
' WHEN a.attnotnull THEN ''NO'' ' +
' ELSE ''YES'' ' +
' END AS is_nullable, ' +
// Character maximum length (in characters)
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' AND a.atttypmod <> -1 ' +
' THEN a.atttypmod - 4 ' +
' ELSE NULL ' +
' END AS character_maximum_length, ' +
// Numeric precision / scale (NULL for non-numeric)
' CASE ' +
' WHEN (bt.typcategory IN (''N'',''F'')) OR (bt.oid IS NULL AND t.typcategory IN (''N'',''F'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.atttypmod = -1 THEN NULL ' +
' ELSE ((a.atttypmod - 4) >> 16)::integer ' +
' END ' +
' END AS numeric_precision, ' +
' CASE ' +
' WHEN (bt.typcategory IN (''N'',''F'')) OR (bt.oid IS NULL AND t.typcategory IN (''N'',''F'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.atttypmod = -1 THEN NULL ' +
' ELSE ((a.atttypmod - 4) & 65535)::integer ' +
' END ' +
' END AS numeric_scale, ' +
// Datetime precision (for time/timestamp/interval)
' CASE ' +
' WHEN (bt.typcategory = ''D'' OR (bt.oid IS NULL AND t.typcategory = ''D'')) ' +
' AND a.atttypmod <> -1 ' +
' THEN a.atttypmod ' +
' ELSE NULL ' +
' END AS datetime_precision, ' +
// Character set name: PostgreSQL has one per DB; mimic information_schema
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' THEN current_database() ' +
' ELSE NULL ' +
' END AS character_set_name, ' +
// Collation name for collatable columns
' CASE ' +
' WHEN (bt.typcategory = ''S'' OR (bt.oid IS NULL AND t.typcategory = ''S'')) ' +
' THEN ' +
' CASE ' +
' WHEN a.attcollation <> t.typcollation ' +
' THEN coll.collname ' +
' ELSE NULL ' +
' END ' +
' ELSE NULL ' +
' END AS collation_name, ' +
// Default expression for non-generated columns
' CASE ' +
' WHEN a.attgenerated = '''' AND a.atthasdef ' +
' THEN pg_get_expr(ad.adbin, ad.adrelid) ' +
' ELSE NULL ' +
' END AS column_default, ' +
// Generation expression for generated columns
' CASE ' +
' WHEN a.attgenerated <> '''' AND a.atthasdef ' +
' THEN pg_get_expr(ad.adbin, ad.adrelid) ' +
' ELSE NULL ' +
' END AS generation_expression, ' +
' d.description AS column_comment ' +
'FROM pg_catalog.pg_class AS c ' +
'JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace ' +
'JOIN pg_catalog.pg_attribute AS a ON a.attrelid = c.oid ' +
'JOIN pg_catalog.pg_type AS t ON t.oid = a.atttypid ' +
'LEFT JOIN pg_catalog.pg_type AS bt ON bt.oid = t.typbasetype ' +
'LEFT JOIN pg_catalog.pg_attrdef AS ad ' +
' ON ad.adrelid = a.attrelid ' +
' AND ad.adnum = a.attnum ' +
'LEFT JOIN pg_catalog.pg_description AS d ' +
' ON d.objoid = a.attrelid ' +
' AND d.objsubid = a.attnum ' +
'LEFT JOIN pg_catalog.pg_collation AS coll ' +
' ON coll.oid = a.attcollation ' +
'WHERE n.nspname = %s ' +
' AND a.attnum > 0 ' +
' AND NOT a.attisdropped ' +
' AND c.relname = %s ' +
'ORDER BY ordinal_position',
'' // ServerVersion < 12
);
else Result := inherited;
end;
end;

View File

@@ -422,6 +422,7 @@ begin
qDisableForeignKeyChecks: Result := '';
qEnableForeignKeyChecks: Result := '';
qForeignKeyDrop: Result := 'DROP FOREIGN KEY %s';
qGetTableColumns: Result := 'SELECT * FROM pragma_table_xinfo(%s, %s)';
else Result := inherited;
end;
end;