using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Transactions; using Npgsql.BackendMessages; using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandlers.CompositeHandlers; using Npgsql.Util; namespace Npgsql.Schema; class DbColumnSchemaGenerator { readonly RowDescriptionMessage _rowDescription; readonly NpgsqlConnection _connection; readonly bool _fetchAdditionalInfo; internal DbColumnSchemaGenerator(NpgsqlConnection connection, RowDescriptionMessage rowDescription, bool fetchAdditionalInfo) { _connection = connection; _rowDescription = rowDescription; _fetchAdditionalInfo = fetchAdditionalInfo; } #region Columns queries static string GenerateColumnsQuery(Version pgVersion, string columnFieldFilter) => $@"SELECT typ.oid AS typoid, nspname, relname, attname, attrelid, attnum, attnotnull, {(pgVersion.IsGreaterOrEqual(10) ? "attidentity != ''" : "FALSE")} AS isidentity, CASE WHEN typ.typtype = 'd' THEN typ.typtypmod ELSE atttypmod END AS typmod, CASE WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHERE adrelid = cls.oid AND adnum = attr.attnum) ELSE NULL END AS default, CASE WHEN col.is_updatable = 'YES' THEN true ELSE false END AS is_updatable, EXISTS ( SELECT * FROM pg_index WHERE pg_index.indrelid = cls.oid AND pg_index.indisprimary AND attnum = ANY (indkey) ) AS isprimarykey, EXISTS ( SELECT * FROM pg_index WHERE pg_index.indrelid = cls.oid AND pg_index.indisunique AND pg_index.{(pgVersion.IsGreaterOrEqual(11) ? "indnkeyatts" : "indnatts")} = 1 AND attnum = pg_index.indkey[0] ) AS isunique FROM pg_attribute AS attr JOIN pg_type AS typ ON attr.atttypid = typ.oid JOIN pg_class AS cls ON cls.oid = attr.attrelid JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace LEFT OUTER JOIN information_schema.columns AS col ON col.table_schema = nspname AND col.table_name = relname AND col.column_name = attname WHERE atttypid <> 0 AND relkind IN ('r', 'v', 'm') AND NOT attisdropped AND nspname NOT IN ('pg_catalog', 'information_schema') AND attnum > 0 AND ({columnFieldFilter}) ORDER BY attnum"; /// /// Stripped-down version of , mainly to support Amazon Redshift. /// static string GenerateOldColumnsQuery(string columnFieldFilter) => $@"SELECT typ.oid AS typoid, nspname, relname, attname, attrelid, attnum, attnotnull, CASE WHEN typ.typtype = 'd' THEN typ.typtypmod ELSE atttypmod END AS typmod, CASE WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHERE adrelid = cls.oid AND adnum = attr.attnum) ELSE NULL END AS default, TRUE AS is_updatable, /* Supported only since PG 8.2 */ FALSE AS isprimarykey, /* Can't do ANY() on pg_index.indkey which is int2vector */ FALSE AS isunique /* Can't do ANY() on pg_index.indkey which is int2vector */ FROM pg_attribute AS attr JOIN pg_type AS typ ON attr.atttypid = typ.oid JOIN pg_class AS cls ON cls.oid = attr.attrelid JOIN pg_namespace AS ns ON ns.oid = cls.relnamespace LEFT OUTER JOIN information_schema.columns AS col ON col.table_schema = nspname AND col.table_name = relname AND col.column_name = attname WHERE atttypid <> 0 AND relkind IN ('r', 'v', 'm') AND NOT attisdropped AND nspname NOT IN ('pg_catalog', 'information_schema') AND attnum > 0 AND ({columnFieldFilter}) ORDER BY attnum"; #endregion Column queries internal async Task> GetColumnSchema(bool async, CancellationToken cancellationToken = default) { // This is mainly for Amazon Redshift var oldQueryMode = _connection.PostgreSqlVersion < new Version(8, 2); var numFields = _rowDescription.Count; var result = new List(numFields); for (var i = 0; i < numFields; i++) result.Add(null); var populatedColumns = 0; if (_fetchAdditionalInfo) { // We have two types of fields - those which correspond to actual database columns // and those that don't (e.g. SELECT 8). For the former we load lots of info from // the backend (if fetchAdditionalInfo is true), for the latter we only have the RowDescription var columnFieldFilter = _rowDescription .Where(f => f.TableOID != 0) // Only column fields .Select(c => $"(attr.attrelid={c.TableOID} AND attr.attnum={c.ColumnAttributeNumber})") .Join(" OR "); if (columnFieldFilter != string.Empty) { var query = oldQueryMode ? GenerateOldColumnsQuery(columnFieldFilter) : GenerateColumnsQuery(_connection.PostgreSqlVersion, columnFieldFilter); using var scope = new TransactionScope( TransactionScopeOption.Suppress, async ? TransactionScopeAsyncFlowOption.Enabled : TransactionScopeAsyncFlowOption.Suppress); using var connection = (NpgsqlConnection)((ICloneable)_connection).Clone(); await connection.Open(async, cancellationToken); using var cmd = new NpgsqlCommand(query, connection); var reader = await cmd.ExecuteReader(CommandBehavior.Default, async, cancellationToken); try { while (async ? await reader.ReadAsync(cancellationToken) : reader.Read()) { var column = LoadColumnDefinition(reader, _connection.Connector!.TypeMapper.DatabaseInfo, oldQueryMode); for (var ordinal = 0; ordinal < numFields; ordinal++) { var field = _rowDescription[ordinal]; if (field.TableOID == column.TableOID && field.ColumnAttributeNumber == column.ColumnAttributeNumber) { populatedColumns++; if (column.ColumnOrdinal.HasValue) column = column.Clone(); // The column's ordinal is with respect to the resultset, not its table column.ColumnOrdinal = ordinal; result[ordinal] = column; } } } } finally { if (async) await reader.DisposeAsync(); else reader.Dispose(); } } } // We had some fields which don't correspond to regular table columns (or fetchAdditionalInfo is false). // Fill in whatever info we have from the RowDescription itself for (var i = 0; i < numFields; i++) { var column = result[i]; var field = _rowDescription[i]; if (column is null) { column = SetUpNonColumnField(field); column.ColumnOrdinal = i; result[i] = column; populatedColumns++; } column.ColumnName = field.Name; column.IsAliased = column.BaseColumnName is null ? default(bool?) : (column.BaseColumnName != column.ColumnName); } if (populatedColumns != numFields) throw new NpgsqlException("Could not load all columns for the resultset"); return result.AsReadOnly()!; } NpgsqlDbColumn LoadColumnDefinition(NpgsqlDataReader reader, NpgsqlDatabaseInfo databaseInfo, bool oldQueryMode) { // We don't set ColumnName here. It should always contain the column alias rather than // the table column name (i.e. in case of "SELECT foo AS foo_alias"). It will be set later. var column = new NpgsqlDbColumn { AllowDBNull = !reader.GetBoolean(reader.GetOrdinal("attnotnull")), BaseCatalogName = _connection.Database!, BaseSchemaName = reader.GetString(reader.GetOrdinal("nspname")), BaseServerName = _connection.Host!, BaseTableName = reader.GetString(reader.GetOrdinal("relname")), BaseColumnName = reader.GetString(reader.GetOrdinal("attname")), ColumnAttributeNumber = reader.GetInt16(reader.GetOrdinal("attnum")), IsKey = reader.GetBoolean(reader.GetOrdinal("isprimarykey")), IsReadOnly = !reader.GetBoolean(reader.GetOrdinal("is_updatable")), IsUnique = reader.GetBoolean(reader.GetOrdinal("isunique")), TableOID = reader.GetFieldValue(reader.GetOrdinal("attrelid")), TypeOID = reader.GetFieldValue(reader.GetOrdinal("typoid")) }; column.PostgresType = databaseInfo.ByOID[column.TypeOID]; column.DataTypeName = column.PostgresType.DisplayName; // Facets do not get included var defaultValueOrdinal = reader.GetOrdinal("default"); column.DefaultValue = reader.IsDBNull(defaultValueOrdinal) ? null : reader.GetString(defaultValueOrdinal); column.IsAutoIncrement = !oldQueryMode && reader.GetBoolean(reader.GetOrdinal("isidentity")) || column.DefaultValue != null && column.DefaultValue.StartsWith("nextval(", StringComparison.Ordinal); ColumnPostConfig(column, reader.GetInt32(reader.GetOrdinal("typmod"))); return column; } NpgsqlDbColumn SetUpNonColumnField(FieldDescription field) { // ColumnName and BaseColumnName will be set later var column = new NpgsqlDbColumn { BaseCatalogName = _connection.Database!, BaseServerName = _connection.Host!, IsReadOnly = true, DataTypeName = field.PostgresType.DisplayName, TypeOID = field.TypeOID, TableOID = field.TableOID, ColumnAttributeNumber = field.ColumnAttributeNumber, PostgresType = field.PostgresType }; ColumnPostConfig(column, field.TypeModifier); return column; } /// /// Performs some post-setup configuration that's common to both table columns and non-columns. /// void ColumnPostConfig(NpgsqlDbColumn column, int typeModifier) { var typeMapper = _connection.Connector!.TypeMapper; column.NpgsqlDbType = typeMapper.GetTypeInfoByOid(column.TypeOID).npgsqlDbType; column.DataType = typeMapper.TryResolveByOID(column.TypeOID, out var handler) ? handler.GetFieldType() : null; if (column.DataType != null) { column.IsLong = handler is ByteaHandler; if (handler is ICompositeHandler) column.UdtAssemblyQualifiedName = column.DataType.AssemblyQualifiedName; } var facets = column.PostgresType.GetFacets(typeModifier); if (facets.Size != null) column.ColumnSize = facets.Size; if (facets.Precision != null) column.NumericPrecision = facets.Precision; if (facets.Scale != null) column.NumericScale = facets.Scale; } }