using System.Collections.Concurrent; using Npgsql; namespace NpgsqlRest; /// /// Metadata for a composite type including field names, types, and nested composite info. /// public class CompositeTypeMetadata { /// /// Fully qualified type name, e.g., "public.my_type" /// public required string TypeName { get; init; } /// /// Field names in order, e.g., ["id", "name", "nested_val"] /// public required string[] FieldNames { get; init; } /// /// Field type names in order, e.g., ["integer", "text", "public.inner_type"] /// public required string[] FieldTypeNames { get; init; } /// /// Converted field names using the name converter /// public string[]? ConvertedFieldNames { get; set; } /// /// Type descriptors for each field /// public TypeDescriptor[]? FieldDescriptors { get; set; } /// /// For each field, if it's a composite type, contains its metadata; null otherwise /// public CompositeTypeMetadata?[]? NestedFieldTypes { get; set; } /// /// For each field, true if the field is an array type /// public required bool[] IsArrayField { get; init; } /// /// For each field that is an array of composites, contains the element type metadata /// public CompositeTypeMetadata?[]? ArrayElementTypes { get; set; } } /// /// Thread-safe cache for composite type metadata. /// Used to resolve nested composite types for deep JSON serialization. /// public static class CompositeTypeCache { private static readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private static readonly object _initLock = new(); private static volatile bool _initialized = false; private static Func? _nameConverter; /// /// SQL query to fetch all composite types and their fields. /// private const string TypeQuery = """ select (quote_ident(n.nspname) || '.' || quote_ident(t.typname))::regtype::text as type_name, a.attnum as field_pos, a.attname as field_name, pg_catalog.format_type(a.atttypid, a.atttypmod) as field_type, ft.typrelid <> 0 as is_field_composite, a.atttypid = any(array(select oid from pg_type where typelem <> 0)) as is_array, case when ft.typelem <> 0 then (select eft.typrelid <> 0 from pg_catalog.pg_type eft where eft.oid = ft.typelem) else false end as is_array_of_composite from pg_catalog.pg_type t join pg_catalog.pg_namespace n on n.oid = t.typnamespace join pg_catalog.pg_class c on t.typrelid = c.oid and c.relkind in ('r', 'c') join pg_catalog.pg_attribute a on t.typrelid = a.attrelid and a.attisdropped is false and a.attnum > 0 left join pg_catalog.pg_type ft on a.atttypid = ft.oid where n.nspname not like 'pg_%' and n.nspname <> 'information_schema' and has_schema_privilege(current_user, n.nspname, 'USAGE') order by type_name, field_pos """; /// /// Initialize the composite type cache from the database. /// Thread-safe and idempotent - only loads once. /// /// Open database connection /// Name converter function for field names public static void Initialize(NpgsqlConnection connection, Func? nameConverter = null) { if (_initialized) return; lock (_initLock) { if (_initialized) return; _nameConverter = nameConverter; LoadAllCompositeTypes(connection); ResolveNestedTypes(); _initialized = true; } } /// /// Clear the cache. Useful for testing or when schema changes. /// public static void Clear() { lock (_initLock) { _cache.Clear(); _initialized = false; _nameConverter = null; } } /// /// Check if the cache is initialized. /// public static bool IsInitialized => _initialized; /// /// Get metadata for a composite type by name. /// /// Type name (e.g., "public.my_type" or "my_type") /// Composite type metadata, or null if not a composite type public static CompositeTypeMetadata? GetType(string? typeName) { if (string.IsNullOrEmpty(typeName)) return null; return FindInCache(typeName); } /// /// Get metadata for the element type of an array of composites. /// /// Array type name (e.g., "public.my_type[]") /// Element composite type metadata, or null if not an array of composites public static CompositeTypeMetadata? GetArrayElementType(string? arrayTypeName) { if (string.IsNullOrEmpty(arrayTypeName)) return null; // Strip array suffix(es) - handles both [] and [][] etc. var elementTypeName = arrayTypeName; while (elementTypeName.EndsWith("[]", StringComparison.Ordinal)) { elementTypeName = elementTypeName[..^2]; } return GetType(elementTypeName); } /// /// Resolve composite type metadata for a TypeDescriptor. /// Sets CompositeFieldNames/CompositeFieldDescriptors if the type is a composite, /// or ArrayCompositeFieldNames/ArrayCompositeFieldDescriptors if it's an array of composites. /// Can be called from plugins since TypeDescriptor setters are internal. /// /// The TypeDescriptor to resolve /// True if the descriptor was resolved as a composite or array of composites public static bool ResolveTypeDescriptor(TypeDescriptor descriptor) { var compositeMetadata = GetType(descriptor.OriginalType); if (compositeMetadata != null) { descriptor.CompositeFieldNames = compositeMetadata.ConvertedFieldNames; descriptor.CompositeFieldDescriptors = compositeMetadata.FieldDescriptors; return true; } if (descriptor.IsArray) { var elementMetadata = GetArrayElementType(descriptor.OriginalType); if (elementMetadata != null) { descriptor.ArrayCompositeFieldNames = elementMetadata.ConvertedFieldNames; descriptor.ArrayCompositeFieldDescriptors = elementMetadata.FieldDescriptors; return true; } } return false; } /// /// Normalize type name by removing quotes and handling schema prefixes. /// private static string NormalizeTypeName(string typeName) { // Remove surrounding quotes if present var result = typeName.Trim(); if (result.StartsWith('"') && result.EndsWith('"')) { result = result[1..^1]; } // Handle quoted identifiers within the name result = result.Replace("\"", ""); return result; } /// /// Try to find a type in the cache, also trying without schema prefix. /// PostgreSQL regtype::text omits the public schema, but other sources may include it. /// private static CompositeTypeMetadata? FindInCache(string typeName) { var normalized = NormalizeTypeName(typeName); if (_cache.TryGetValue(normalized, out var result)) { return result; } // Try stripping schema prefix (e.g., "public.my_type" → "my_type") int dotIndex = normalized.IndexOf('.'); if (dotIndex >= 0) { var withoutSchema = normalized[(dotIndex + 1)..]; if (_cache.TryGetValue(withoutSchema, out result)) { return result; } } return null; } /// /// Load all composite types from the database into the cache. /// private static void LoadAllCompositeTypes(NpgsqlConnection connection) { using var command = connection.CreateCommand(); command.CommandText = TypeQuery; command.LogCommand(nameof(CompositeTypeCache)); using var reader = command.ExecuteReader(); // Group rows by type name to build metadata string? currentTypeName = null; var fieldNames = new List(); var fieldTypes = new List(); var isArrayField = new List(); var isFieldComposite = new List(); var isArrayOfComposite = new List(); while (reader.Read()) { var typeName = reader.GetString(0); // If we've moved to a new type, save the previous one if (currentTypeName != null && typeName != currentTypeName) { SaveTypeMetadata(currentTypeName, fieldNames, fieldTypes, isArrayField, isFieldComposite, isArrayOfComposite); fieldNames.Clear(); fieldTypes.Clear(); isArrayField.Clear(); isFieldComposite.Clear(); isArrayOfComposite.Clear(); } currentTypeName = typeName; fieldNames.Add(reader.GetString(2)); fieldTypes.Add(reader.GetString(3)); isArrayField.Add(reader.GetBoolean(5)); isFieldComposite.Add(reader.GetBoolean(4)); isArrayOfComposite.Add(reader.GetBoolean(6)); } // Don't forget the last type if (currentTypeName != null) { SaveTypeMetadata(currentTypeName, fieldNames, fieldTypes, isArrayField, isFieldComposite, isArrayOfComposite); } } /// /// Save a single type's metadata to the cache. /// private static void SaveTypeMetadata( string typeName, List fieldNames, List fieldTypes, List isArrayField, List isFieldComposite, List isArrayOfComposite) { var fieldNamesArray = fieldNames.ToArray(); var fieldTypesArray = fieldTypes.ToArray(); var count = fieldNamesArray.Length; // Apply name converter var convertedNames = new string[count]; for (var i = 0; i < count; i++) { convertedNames[i] = _nameConverter?.Invoke(fieldNamesArray[i]) ?? fieldNamesArray[i]; } // Create type descriptors for each field var descriptors = new TypeDescriptor[count]; for (var i = 0; i < count; i++) { descriptors[i] = new TypeDescriptor(fieldTypesArray[i]); } var metadata = new CompositeTypeMetadata { TypeName = typeName, FieldNames = fieldNamesArray, FieldTypeNames = fieldTypesArray, ConvertedFieldNames = convertedNames, FieldDescriptors = descriptors, IsArrayField = isArrayField.ToArray(), // These will be populated in ResolveNestedTypes() NestedFieldTypes = new CompositeTypeMetadata?[count], ArrayElementTypes = new CompositeTypeMetadata?[count] }; var normalizedName = NormalizeTypeName(typeName); _cache[normalizedName] = metadata; } /// /// After all types are loaded, resolve nested type references. /// Uses cycle detection to handle self-referencing types. /// private static void ResolveNestedTypes() { foreach (var metadata in _cache.Values) { var visited = new HashSet(StringComparer.Ordinal); ResolveNestedTypesForType(metadata, visited); } } /// /// Recursively resolve nested types for a single type. /// private static void ResolveNestedTypesForType(CompositeTypeMetadata metadata, HashSet visited) { // Cycle detection var normalizedName = NormalizeTypeName(metadata.TypeName); if (!visited.Add(normalizedName)) { return; // Already processing this type - circular reference } try { for (var i = 0; i < metadata.FieldTypeNames.Length; i++) { var fieldType = metadata.FieldTypeNames[i]; // Check if this field is a composite type var nestedType = GetType(fieldType); if (nestedType != null) { metadata.NestedFieldTypes![i] = nestedType; // Also set the nested info on the TypeDescriptor if (metadata.FieldDescriptors != null) { metadata.FieldDescriptors[i].CompositeFieldNames = nestedType.ConvertedFieldNames; metadata.FieldDescriptors[i].CompositeFieldDescriptors = nestedType.FieldDescriptors; } // Recursively resolve (with cycle detection) ResolveNestedTypesForType(nestedType, visited); } // Check if this field is an array of composites if (metadata.IsArrayField[i]) { var elementType = GetArrayElementType(fieldType); if (elementType != null) { metadata.ArrayElementTypes![i] = elementType; // Also set the array composite info on the TypeDescriptor if (metadata.FieldDescriptors != null) { metadata.FieldDescriptors[i].ArrayCompositeFieldNames = elementType.ConvertedFieldNames; metadata.FieldDescriptors[i].ArrayCompositeFieldDescriptors = elementType.FieldDescriptors; } // Recursively resolve the element type ResolveNestedTypesForType(elementType, visited); } } } } finally { visited.Remove(normalizedName); } } }