using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Npgsql.Internal; using Npgsql.Internal.TypeHandlers; using Npgsql.Internal.TypeHandling; using Npgsql.PostgresTypes; using Npgsql.Replication.PgOutput.Messages; using Npgsql.TypeMapping; using Npgsql.Util; namespace Npgsql.BackendMessages; /// /// A RowDescription message sent from the backend. /// /// /// See https://www.postgresql.org/docs/current/static/protocol-message-formats.html /// sealed class RowDescriptionMessage : IBackendMessage, IReadOnlyList { FieldDescription?[] _fields; readonly Dictionary _nameIndex; Dictionary? _insensitiveIndex; internal RowDescriptionMessage(int numFields = 10) { _fields = new FieldDescription[numFields]; _nameIndex = new Dictionary(); } RowDescriptionMessage(RowDescriptionMessage source) { Count = source.Count; _fields = new FieldDescription?[Count]; for (var i = 0; i < Count; i++) _fields[i] = source._fields[i]!.Clone(); _nameIndex = new Dictionary(source._nameIndex); if (source._insensitiveIndex?.Count > 0) _insensitiveIndex = new Dictionary(source._insensitiveIndex); } internal RowDescriptionMessage Load(NpgsqlReadBuffer buf, ConnectorTypeMapper typeMapper) { _nameIndex.Clear(); _insensitiveIndex?.Clear(); var numFields = Count = buf.ReadInt16(); if (_fields.Length < numFields) { var oldFields = _fields; _fields = new FieldDescription[numFields]; Array.Copy(oldFields, _fields, oldFields.Length); } for (var i = 0; i < numFields; ++i) { var field = _fields[i] ??= new(); field.Populate( typeMapper, name: buf.ReadNullTerminatedString(), tableOID: buf.ReadUInt32(), columnAttributeNumber: buf.ReadInt16(), oid: buf.ReadUInt32(), typeSize: buf.ReadInt16(), typeModifier: buf.ReadInt32(), formatCode: (FormatCode)buf.ReadInt16() ); if (!_nameIndex.ContainsKey(field.Name)) _nameIndex.Add(field.Name, i); } return this; } internal static RowDescriptionMessage CreateForReplication( ConnectorTypeMapper typeMapper, uint tableOID, FormatCode formatCode, IReadOnlyList columns) { var msg = new RowDescriptionMessage(columns.Count); var numFields = msg.Count = columns.Count; for (var i = 0; i < numFields; ++i) { var field = msg._fields[i] = new(); var column = columns[i]; field.Populate( typeMapper, name: column.ColumnName, tableOID: tableOID, columnAttributeNumber: checked((short)i), oid: column.DataTypeId, typeSize: 0, // TODO: Confirm we don't have this in replication typeModifier: column.TypeModifier, formatCode: formatCode ); if (!msg._nameIndex.ContainsKey(field.Name)) msg._nameIndex.Add(field.Name, i); } return msg; } public FieldDescription this[int index] { get { Debug.Assert(index < Count); Debug.Assert(_fields[index] != null); return _fields[index]!; } } public int Count { get; private set; } public IEnumerator GetEnumerator() => new Enumerator(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Given a string name, returns the field's ordinal index in the row. /// internal int GetFieldIndex(string name) => TryGetFieldIndex(name, out var ret) ? ret : throw new IndexOutOfRangeException("Field not found in row: " + name); /// /// Given a string name, returns the field's ordinal index in the row. /// internal bool TryGetFieldIndex(string name, out int fieldIndex) { if (_nameIndex.TryGetValue(name, out fieldIndex)) return true; if (_insensitiveIndex is null || _insensitiveIndex.Count == 0) { if (_insensitiveIndex == null) _insensitiveIndex = new Dictionary(InsensitiveComparer.Instance); foreach (var kv in _nameIndex) if (!_insensitiveIndex.ContainsKey(kv.Key)) _insensitiveIndex[kv.Key] = kv.Value; } return _insensitiveIndex.TryGetValue(name, out fieldIndex); } public BackendMessageCode Code => BackendMessageCode.RowDescription; internal RowDescriptionMessage Clone() => new(this); /// /// Comparer that's case-insensitive and Kana width-insensitive /// sealed class InsensitiveComparer : IEqualityComparer { public static readonly InsensitiveComparer Instance = new(); static readonly CompareInfo CompareInfo = CultureInfo.InvariantCulture.CompareInfo; InsensitiveComparer() {} // We should really have CompareOptions.IgnoreKanaType here, but see // https://github.com/dotnet/corefx/issues/12518#issuecomment-389658716 public bool Equals(string? x, string? y) => CompareInfo.Compare(x, y, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType) == 0; public int GetHashCode(string o) => CompareInfo.GetSortKey(o, CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType).GetHashCode(); } class Enumerator : IEnumerator { readonly RowDescriptionMessage _rowDescription; int _pos = -1; public Enumerator(RowDescriptionMessage rowDescription) => _rowDescription = rowDescription; public FieldDescription Current => _pos >= 0 ? _rowDescription[_pos] : throw new InvalidOperationException(); object IEnumerator.Current => Current; public bool MoveNext() { if (_pos == _rowDescription.Count - 1) return false; _pos++; return true; } public void Reset() => _pos = -1; public void Dispose() {} } } /// /// A descriptive record on a single field received from PostgreSQL. /// See RowDescription in https://www.postgresql.org/docs/current/static/protocol-message-formats.html /// public sealed class FieldDescription { #pragma warning disable CS8618 // Lazy-initialized type internal FieldDescription() {} internal FieldDescription(uint oid) : this("?", 0, 0, oid, 0, 0, FormatCode.Binary) {} internal FieldDescription( string name, uint tableOID, short columnAttributeNumber, uint oid, short typeSize, int typeModifier, FormatCode formatCode) { Name = name; TableOID = tableOID; ColumnAttributeNumber = columnAttributeNumber; TypeOID = oid; TypeSize = typeSize; TypeModifier = typeModifier; FormatCode = formatCode; } #pragma warning restore CS8618 internal FieldDescription(FieldDescription source) { _typeMapper = source._typeMapper; Name = source.Name; TableOID = source.TableOID; ColumnAttributeNumber = source.ColumnAttributeNumber; TypeOID = source.TypeOID; TypeSize = source.TypeSize; TypeModifier = source.TypeModifier; FormatCode = source.FormatCode; Handler = source.Handler; } internal void Populate( ConnectorTypeMapper typeMapper, string name, uint tableOID, short columnAttributeNumber, uint oid, short typeSize, int typeModifier, FormatCode formatCode ) { _typeMapper = typeMapper; Name = name; TableOID = tableOID; ColumnAttributeNumber = columnAttributeNumber; TypeOID = oid; TypeSize = typeSize; TypeModifier = typeModifier; FormatCode = formatCode; ResolveHandler(); } /// /// The field name. /// internal string Name { get; set; } /// /// The object ID of the field's data type. /// internal uint TypeOID { get; set; } /// /// The data type size (see pg_type.typlen). Note that negative values denote variable-width types. /// public short TypeSize { get; set; } /// /// The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. /// public int TypeModifier { get; set; } /// /// If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. /// internal uint TableOID { get; set; } /// /// If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. /// internal short ColumnAttributeNumber { get; set; } /// /// The format code being used for the field. /// Currently will be zero (text) or one (binary). /// In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero. /// internal FormatCode FormatCode { get; set; } internal string TypeDisplayName => PostgresType.GetDisplayNameWithFacets(TypeModifier); /// /// The Npgsql type handler assigned to handle this field. /// Returns for fields with format text. /// internal NpgsqlTypeHandler Handler { get; private set; } internal PostgresType PostgresType => _typeMapper.DatabaseInfo.ByOID.TryGetValue(TypeOID, out var postgresType) ? postgresType : UnknownBackendType.Instance; internal Type FieldType => Handler.GetFieldType(this); internal void ResolveHandler() => Handler = IsBinaryFormat ? _typeMapper.ResolveByOID(TypeOID) : _typeMapper.UnrecognizedTypeHandler; ConnectorTypeMapper _typeMapper; internal bool IsBinaryFormat => FormatCode == FormatCode.Binary; internal bool IsTextFormat => FormatCode == FormatCode.Text; internal FieldDescription Clone() { var field = new FieldDescription(this); field.ResolveHandler(); return field; } /// /// Returns a string that represents the current object. /// public override string ToString() => Name + (Handler == null ? "" : $"({Handler.PgDisplayName})"); }