X Tutup
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq; using System.Reflection; using Npgsql.Logging; using Npgsql.TypeHandlers; using NpgsqlTypes; using System.Diagnostics.Contracts; namespace Npgsql { internal class TypeHandlerRegistry { #region Members internal NpgsqlConnector Connector { get; private set; } internal TypeHandler UnrecognizedTypeHandler { get; private set; } readonly Dictionary _oidIndex; readonly Dictionary _byDbType; readonly Dictionary _byNpgsqlDbType; readonly Dictionary _byType; Dictionary _byEnumTypeAsArray; List _backendTypes; static internal readonly Dictionary HandlerTypes; static readonly Dictionary NpgsqlDbTypeToDbType; static readonly Dictionary DbTypeToNpgsqlDbType; static readonly Dictionary TypeToNpgsqlDbType; static readonly Dictionary TypeToDbType; /// /// Caches, for each connection string, the results of the backend type query in the form of a list of type /// info structs keyed by the PG name. /// Repeated connections to the same connection string reuse the query results and avoid an additional /// roundtrip at open-time. /// static readonly ConcurrentDictionary> BackendTypeCache = new ConcurrentDictionary>(); static ConcurrentDictionary _globalEnumRegistrations; static readonly NpgsqlLogger Log = NpgsqlLogManager.GetCurrentClassLogger(); #endregion #region Initialization and Loading static internal void Setup(NpgsqlConnector connector) { connector.TypeHandlerRegistry = new TypeHandlerRegistry(connector); List types; if (!BackendTypeCache.TryGetValue(connector.ConnectionString, out types)) { types = BackendTypeCache[connector.ConnectionString] = LoadBackendTypes(connector); } connector.TypeHandlerRegistry.RegisterTypes(types); } TypeHandlerRegistry(NpgsqlConnector connector) { Connector = connector; UnrecognizedTypeHandler = new UnrecognizedTypeHandler(); _oidIndex = new Dictionary(); _byDbType = new Dictionary(); _byNpgsqlDbType = new Dictionary(); _byType = new Dictionary(); _byType[typeof(DBNull)] = UnrecognizedTypeHandler; _byNpgsqlDbType[NpgsqlDbType.Unknown] = UnrecognizedTypeHandler; } static List LoadBackendTypes(NpgsqlConnector connector) { var byOID = new Dictionary(); // Select all types (base, array which is also base, enum, range). // Note that arrays are distinguished from primitive types through them having typreceive=array_recv. // Order by primitives first, container later. // For arrays and ranges, join in the element OID and type (to filter out arrays of unhandled // types). var query = @"SELECT a.typname, a.oid, " + @"CASE WHEN a.typreceive::TEXT='array_recv' THEN 'a' ELSE a.typtype END AS type, " + @"CASE " + @"WHEN a.typreceive::TEXT='array_recv' THEN a.typelem " + (connector.SupportsRangeTypes ? @"WHEN a.typtype='r' THEN rngsubtype " : "")+ @"ELSE 0 " + @"END AS elemoid, " + @"CASE WHEN a.typreceive::TEXT='array_recv' OR a.typtype='r' THEN 1 ELSE 0 END AS ord " + @"FROM pg_type AS a " + @"LEFT OUTER JOIN pg_type AS b ON (b.oid = a.typelem) " + (connector.SupportsRangeTypes ? @"LEFT OUTER JOIN pg_range ON (pg_range.rngtypid = a.oid) " : "") + @"WHERE a.typtype IN ('b', 'r', 'e') AND (b.typtype IS NULL OR b.typtype IN ('b', 'r', 'e'))" + @"ORDER BY ord"; var types = new List(); using (var command = new NpgsqlCommand(query, connector.Connection)) { command.AllResultTypesAreUnknown = true; using (var dr = command.ExecuteReader(CommandBehavior.SequentialAccess)) { while (dr.Read()) { var backendType = new BackendType { Name = dr.GetString(0), OID = Convert.ToUInt32(dr[1]) }; Contract.Assume(backendType.Name != null); Contract.Assume(backendType.OID != 0); uint elementOID; var typeChar = dr.GetString(2)[0]; switch (typeChar) { case 'b': // Normal base type backendType.Type = BackendTypeType.Base; break; case 'a': // Array backendType.Type = BackendTypeType.Array; elementOID = Convert.ToUInt32(dr[3]); Contract.Assume(elementOID > 0); if (!byOID.TryGetValue(elementOID, out backendType.Element)) { Log.Error(string.Format("Array type '{0}' refers to unknown element with OID {1}, skipping", backendType.Name, elementOID), connector.Id); continue; } backendType.Element.Array = backendType; break; case 'e': // Enum backendType.Type = BackendTypeType.Enum; break; case 'r': // Range backendType.Type = BackendTypeType.Range; elementOID = Convert.ToUInt32(dr[3]); Contract.Assume(elementOID > 0); if (!byOID.TryGetValue(elementOID, out backendType.Element)) { Log.Error(String.Format("Range type '{0}' refers to unknown subtype with OID {1}, skipping", backendType.Name, elementOID), connector.Id); continue; } break; default: throw new ArgumentOutOfRangeException(String.Format("Unknown typtype for type '{0}' in pg_type: {1}", backendType.Name, typeChar)); } types.Add(backendType); byOID[backendType.OID] = backendType; } } } /*foreach (var notFound in _typeHandlers.Where(t => t.Oid == -1)) { _log.WarnFormat("Could not find type {0} in pg_type", notFound.PgNames[0]); }*/ return types; } void RegisterTypes(List backendTypes) { foreach (var backendType in backendTypes) { switch (backendType.Type) { case BackendTypeType.Base: RegisterBaseType(backendType); continue; case BackendTypeType.Array: RegisterArrayType(backendType); continue; case BackendTypeType.Range: RegisterRangeType(backendType); continue; case BackendTypeType.Enum: TypeHandler handler; if (_globalEnumRegistrations != null && _globalEnumRegistrations.TryGetValue(backendType.Name, out handler)) { ActivateEnumType(handler, backendType); } continue; default: Log.Error("Unknown type of type encountered, skipping: " + backendType, Connector.Id); continue; } } _backendTypes = backendTypes; } void RegisterBaseType(BackendType backendType) { TypeAndMapping typeAndMapping; if (!HandlerTypes.TryGetValue(backendType.Name, out typeAndMapping)) { // Backend type not supported by Npgsql return; } var handlerType = typeAndMapping.HandlerType; var mapping = typeAndMapping.Mapping; // Instantiate the type handler. If it has a constructor that accepts an NpgsqlConnector, use that to allow // the handler to make connector-specific adjustments. Otherwise (the normal case), use the default constructor. var handler = (TypeHandler)( handlerType.GetConstructor(new[] { typeof(TypeHandlerRegistry) }) != null ? Activator.CreateInstance(handlerType, this) : Activator.CreateInstance(handlerType) ); handler.OID = backendType.OID; _oidIndex[backendType.OID] = handler; handler.PgName = backendType.Name; if (mapping.NpgsqlDbType.HasValue) { var npgsqlDbType = mapping.NpgsqlDbType.Value; if (_byNpgsqlDbType.ContainsKey(npgsqlDbType)) throw new Exception(String.Format("Two type handlers registered on same NpgsqlDbType {0}: {1} and {2}", npgsqlDbType, _byNpgsqlDbType[npgsqlDbType].GetType().Name, handlerType.Name)); _byNpgsqlDbType[npgsqlDbType] = handler; handler.NpgsqlDbType = npgsqlDbType; } foreach (var dbType in mapping.DbTypes) { if (_byDbType.ContainsKey(dbType)) throw new Exception(String.Format("Two type handlers registered on same DbType {0}: {1} and {2}", dbType, _byDbType[dbType].GetType().Name, handlerType.Name)); _byDbType[dbType] = handler; } foreach (var type in mapping.Types) { if (_byType.ContainsKey(type)) throw new Exception(String.Format("Two type handlers registered on same .NET type {0}: {1} and {2}", type, _byType[type].GetType().Name, handlerType.Name)); _byType[type] = handler; } } #endregion #region Array void RegisterArrayType(BackendType backendType) { Contract.Requires(backendType.Element != null); TypeHandler elementHandler; if (!_oidIndex.TryGetValue(backendType.Element.OID, out elementHandler)) { // Array type referring to an unhandled element type return; } ArrayHandler arrayHandler; var asBitStringHandler = elementHandler as BitStringHandler; if (asBitStringHandler != null) { // BitString requires a special array handler which returns bool or BitArray arrayHandler = new BitStringArrayHandler(asBitStringHandler); } else if (elementHandler is ITypeHandlerWithPsv) { var arrayHandlerType = typeof(ArrayHandlerWithPsv<,>).MakeGenericType(elementHandler.GetFieldType(), elementHandler.GetProviderSpecificFieldType()); arrayHandler = (ArrayHandler)Activator.CreateInstance(arrayHandlerType, elementHandler); } else { var arrayHandlerType = typeof(ArrayHandler<>).MakeGenericType(elementHandler.GetFieldType()); arrayHandler = (ArrayHandler)Activator.CreateInstance(arrayHandlerType, elementHandler); } arrayHandler.PgName = "array"; arrayHandler.OID = backendType.OID; _oidIndex[backendType.OID] = arrayHandler; if (elementHandler is IEnumHandler) { if (_byEnumTypeAsArray == null) { _byEnumTypeAsArray = new Dictionary(); } var enumType = elementHandler.GetType().GetGenericArguments()[0]; Contract.Assert(enumType.GetTypeInfo().IsEnum); _byEnumTypeAsArray[enumType] = arrayHandler; } else { _byNpgsqlDbType[NpgsqlDbType.Array | elementHandler.NpgsqlDbType] = arrayHandler; } } #endregion #region Range void RegisterRangeType(BackendType backendType) { Contract.Requires(backendType.Element != null); TypeHandler elementHandler; if (!_oidIndex.TryGetValue(backendType.Element.OID, out elementHandler)) { // Range type referring to an unhandled element type return; } var rangeHandlerType = typeof(RangeHandler<>).MakeGenericType(elementHandler.GetFieldType()); var handler = (TypeHandler)Activator.CreateInstance(rangeHandlerType, elementHandler, backendType.Name); handler.PgName = backendType.Name; handler.NpgsqlDbType = NpgsqlDbType.Range | elementHandler.NpgsqlDbType; handler.OID = backendType.OID; _oidIndex[backendType.OID] = handler; _byNpgsqlDbType.Add(handler.NpgsqlDbType, handler); } #endregion #region Enum internal void RegisterEnumType(string pgName) where TEnum : struct { var backendTypeInfo = _backendTypes.FirstOrDefault(t => t.Name == pgName); if (backendTypeInfo == null) { throw new Exception(String.Format("An enum with the name {0} was not found in the database", pgName)); } var handler = new EnumHandler(); ActivateEnumType(handler, backendTypeInfo); } internal static void RegisterEnumTypeGlobally(string pgName) where TEnum : struct { if (_globalEnumRegistrations == null) { _globalEnumRegistrations = new ConcurrentDictionary(); } _globalEnumRegistrations[pgName] = new EnumHandler(); } void ActivateEnumType(TypeHandler handler, BackendType backendType) { handler.PgName = backendType.Name; handler.OID = backendType.OID; handler.NpgsqlDbType = NpgsqlDbType.Enum; _oidIndex[backendType.OID] = handler; _byType[handler.GetFieldType()] = handler; if (backendType.Array != null) { RegisterArrayType(backendType.Array); } } #endregion #region Lookups /// /// Looks up a type handler by its Postgresql type's OID. /// /// A Postgresql type OID /// A type handler that can be used to encode and decode values. internal TypeHandler this[uint oid] { get { TypeHandler result; if (!_oidIndex.TryGetValue(oid, out result)) { result = UnrecognizedTypeHandler; } return result; } set { _oidIndex[oid] = value; } } internal TypeHandler this[NpgsqlDbType npgsqlDbType, Type enumType = null] { get { TypeHandler handler; if (_byNpgsqlDbType.TryGetValue(npgsqlDbType, out handler)) { return handler; } if (npgsqlDbType == NpgsqlDbType.Enum) { if (enumType == null) { throw new InvalidCastException("Either specify EnumType along with NpgsqlDbType.Enum, or leave both empty to infer from Value"); } if (!_byType.TryGetValue(enumType, out handler)) { throw new NotSupportedException("This enum type is not supported (have you registered it in Npsql and set the EnumType property of NpgsqlParameter?)"); } return handler; } if (npgsqlDbType == (NpgsqlDbType.Enum | NpgsqlDbType.Array)) { if (enumType == null) { throw new InvalidCastException("Either specify EnumType along with NpgsqlDbType.Enum, or leave both empty to infer from Value"); } if (_byEnumTypeAsArray != null && _byEnumTypeAsArray.TryGetValue(enumType, out handler)) { return handler; } throw new NotSupportedException("This enum array type is not supported (have you registered it in Npsql and set the EnumType property of NpgsqlParameter?)"); } throw new NotSupportedException("This NpgsqlDbType isn't supported in Npgsql yet: " + npgsqlDbType); } } internal TypeHandler this[DbType dbType] { get { Contract.Ensures(Contract.Result() != null); TypeHandler handler; if (!_byDbType.TryGetValue(dbType, out handler)) { throw new NotSupportedException("This DbType is not supported in Npgsql: " + dbType); } return handler; } } internal TypeHandler this[object value] { get { Contract.Requires(value != null); Contract.Ensures(Contract.Result() != null); if (value is DateTime) { return ((DateTime) value).Kind == DateTimeKind.Utc ? this[NpgsqlDbType.TimestampTZ] : this[NpgsqlDbType.Timestamp]; } if (value is NpgsqlDateTime) { return ((NpgsqlDateTime)value).Kind == DateTimeKind.Utc ? this[NpgsqlDbType.TimestampTZ] : this[NpgsqlDbType.Timestamp]; } return this[value.GetType()]; } } internal TypeHandler this[Type type] { get { Contract.Ensures(Contract.Result() != null); TypeHandler handler; if (_byType.TryGetValue(type, out handler)) { return handler; } if (type.IsArray) { var elementType = type.GetElementType(); if (elementType.GetTypeInfo().IsEnum) { if (_byEnumTypeAsArray != null && _byEnumTypeAsArray.TryGetValue(elementType, out handler)) { return handler; } throw new Exception("Enums must be registered with Npgsql via Connection.RegisterEnumType or RegisterEnumTypeGlobally"); } if (!_byType.TryGetValue(elementType, out handler)) { throw new NotSupportedException("This .NET type is not supported in Npgsql or your PostgreSQL: " + type); } return this[NpgsqlDbType.Array | handler.NpgsqlDbType]; } var typeInfo = type.GetTypeInfo(); if (typeof(IList).IsAssignableFrom(type)) { if (typeInfo.IsGenericType) { if (!_byType.TryGetValue(type.GetGenericArguments()[0], out handler)) { throw new NotSupportedException("This .NET type is not supported in Npgsql or your PostgreSQL: " + type); } return this[NpgsqlDbType.Array | handler.NpgsqlDbType]; } throw new NotSupportedException("Non-generic IList is a supported parameter, but the NpgsqlDbType parameter must be set on the parameter"); } if (typeInfo.IsEnum) { throw new Exception("Enums must be registered with Npgsql via Connection.RegisterEnumType or RegisterEnumTypeGlobally"); } if (typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>)) { if (!_byType.TryGetValue(type.GetGenericArguments()[0], out handler)) { throw new NotSupportedException("This .NET range type is not supported in your PostgreSQL: " + type); } return this[NpgsqlDbType.Range | handler.NpgsqlDbType]; } throw new NotSupportedException("This .NET type is not supported in Npgsql or your PostgreSQL: " + type); } } internal static NpgsqlDbType ToNpgsqlDbType(DbType dbType) { return DbTypeToNpgsqlDbType[dbType]; } internal static NpgsqlDbType ToNpgsqlDbType(Type type) { NpgsqlDbType npgsqlDbType; if (TypeToNpgsqlDbType.TryGetValue(type, out npgsqlDbType)) { return npgsqlDbType; } if (type.IsArray) { if (type == typeof(byte[])) { return NpgsqlDbType.Bytea; } return NpgsqlDbType.Array | ToNpgsqlDbType(type.GetElementType()); } var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) { return NpgsqlDbType.Enum; } if (typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>)) { return NpgsqlDbType.Range | ToNpgsqlDbType(type.GetGenericArguments()[0]); } if (type == typeof(DBNull)) { return NpgsqlDbType.Unknown; } throw new NotSupportedException("Can't infer NpgsqlDbType for type " + type); } internal static DbType ToDbType(Type type) { DbType dbType; return TypeToDbType.TryGetValue(type, out dbType) ? dbType : DbType.Object; } internal static DbType ToDbType(NpgsqlDbType npgsqlDbType) { DbType dbType; return NpgsqlDbTypeToDbType.TryGetValue(npgsqlDbType, out dbType) ? dbType : DbType.Object; } #endregion #region Type Handler Discovery static TypeHandlerRegistry() { HandlerTypes = new Dictionary(); NpgsqlDbTypeToDbType = new Dictionary(); DbTypeToNpgsqlDbType = new Dictionary(); TypeToNpgsqlDbType = new Dictionary(); TypeToDbType = new Dictionary(); foreach (var t in Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(TypeHandler)))) { var mappings = t.GetCustomAttributes(typeof(TypeMappingAttribute), false); if (!mappings.Any()) continue; foreach (TypeMappingAttribute m in mappings) { if (HandlerTypes.ContainsKey(m.PgName)) { throw new Exception("Two type handlers registered on same PostgreSQL type name: " + m.PgName); } HandlerTypes[m.PgName] = new TypeAndMapping { HandlerType=t, Mapping=m }; if (!m.NpgsqlDbType.HasValue) { continue; } var npgsqlDbType = m.NpgsqlDbType.Value; var inferredDbType = m.InferredDbType; if (inferredDbType != null) { NpgsqlDbTypeToDbType[npgsqlDbType] = inferredDbType.Value; } foreach (var dbType in m.DbTypes) { DbTypeToNpgsqlDbType[dbType] = npgsqlDbType; } foreach (var type in m.Types) { TypeToNpgsqlDbType[type] = npgsqlDbType; if (inferredDbType != null) { TypeToDbType[type] = inferredDbType.Value; } } } } } #endregion #region Misc static internal void ClearBackendTypeCache() { BackendTypeCache.Clear(); } #endregion #region Debugging / Testing #if DEBUG internal Dictionary OIDIndex { get { return _oidIndex; } } #endif #endregion } class BackendType { internal string Name; internal uint OID; internal BackendTypeType Type; internal BackendType Element; internal BackendType Array; } struct TypeAndMapping { internal Type HandlerType; internal TypeMappingAttribute Mapping; } /// /// Specifies the type of a type, as represented in the PostgreSQL typtype column of the pg_type table. /// See http://www.postgresql.org/docs/current/static/catalog-pg-type.html /// enum BackendTypeType { Base, Array, Range, Enum, Pseudo } }
X Tutup