Tipos personalizados de PostgreSQL y Npgsql

A raíz de una pregunta en la lista de discusión en español de PostgreSQL en el que hacían la siguiente pregunta:

Hola, tengo una función definida en mi BD postgresql que recive un array de un elementos de un tipo definido en la BD. (este tipo es un bigint y un character varying). Ahora mi duda es si el conector npgsql (Conector para .NET) soporta la posibilidad de llamar a una funcion con estas características.

Estos tipos compuestos en PostgreSQL se crean con la sentencia CREATE TYPE:

code:
test=> CREATE TYPE complex AS (
test(>     r       double precision,
test(>     i       double precision
test(> );
CREATE TYPE
test=> create table foo (item complex);
CREATE TABLE
test=> insert into foo values(ROW(5.2,1.6));
INSERT 0 1
test=> insert into foo values(ROW(2,-1));
INSERT 0 1
test=> select * from foo;
   item
-----------
 (5.2,1.6)
 (2,-1)
(2 filas)

Npgsql trae soporte sólo para los tipos de datos nativos que ofrece PostgreSQL, y trata a estos tipos compuestos como una cadena de caracteres. Si bien es cierto que se podría trabajar solamente con cadenas tanto para insertar como para recuperar este tipo de datos, esto probablemente causaría algunos problemas en la etapa de desarrollo, porque estos valores no serían comprobados en tiempo de compilación (más detalles); puesto que disponemos del código de Npgsql, es posible extenderlo sin mayores problemas para que soporte los tipos de datos que usemos en una aplicación X.

Lo primero, es definir una clase que represente el tipo que hayamos definido en la base de datos:

csharp:
public struct NpgsqlComplex
{
        private double _r;
        private double _i;

        public double I
        {
                get { return _i; }
                set { _i = value; }
        }

        public double R
        {
                get { return _r; }
                set { _r = value; }
        }

        public NpgsqlComplex(double r, double i)
        {
                _r = r;
                _i = i;
        }       
}

Luego se tiene que registrar esta clase en el método VerifyDefaultTypesMap de la clase NpgsqlTypesHelper:

csharp:
NativeTypeMapping.AddType("complex", NpgsqlDbType.Complex, DbType.Object, true,
        new ConvertNativeToBackendHandler(ExtendedNativeToBackendTypeConverter.ToComplex));

NativeTypeMapping.AddTypeAlias("complex", typeof(NpgsqlComplex));

y agregar un nuevo elemento a la variable TypeInfoList ubicado en el método CreateAndLoadInitialTypesMapping de la misma clase:

csharp:
new NpgsqlBackendTypeInfo(0, "complex", NpgsqlDbType.Complex, DbType.Object, typeof(NpgsqlComplex),
                        new ConvertBackendToNativeHandler(ExtendedBackendToNativeTypeConverter.ToComplex)),

Los métodos ExtendedNativeToBackendTypeConverter.ToComplex y ExtendedBackendToNativeTypeConverter.ToComplex se usan para convertir estos tipos compuestos desde .NET a PostgreSQL y desde PostgreSQL a .NET respectivamente

csharp:
// clase ExtendedNativeToBackendTypeConverter
private static readonly Regex complexRegex = new Regex(@"\(([-+]?\b[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?\b),([-+]?\b[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?\b)\)", RegexOptions.Compiled);
internal static Object ToComplex(NpgsqlBackendTypeInfo TypeInfo, String BackendData, Int16 TypeSize, Int32 TypeModifier)
{
        Match m = complexRegex.Match(BackendData);

        return new NpgsqlComplex(
                        Double.Parse(m.Groups[1].ToString(), NumberStyles.Any,
                                                 CultureInfo.InvariantCulture.NumberFormat),
                        Double.Parse(m.Groups[2].ToString(), NumberStyles.Any,
                                                 CultureInfo.InvariantCulture.NumberFormat));
}


// clase ExtendedBackendToNativeTypeConverter
internal static String ToComplex(NpgsqlNativeTypeInfo TypeInfo, Object NativeData)
{
        if (NativeData is NpgsqlComplex)
        {
                NpgsqlComplex complex = (NpgsqlComplex)NativeData;
                return String.Format(CultureInfo.InvariantCulture, "({0},{1})", complex.R, complex.I);
        }
        else
        {
                throw new InvalidCastException("Unable to cast data to NpgsqlComplex type");
        }
}

Finalmente, la clase de prueba para ver si todo funciona como debería:

csharp:
using System;
using System.Collections.Generic;
using System.Text;
using Npgsql;
using NpgsqlTypes;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (NpgsqlCommand command = new NpgsqlCommand("SELECT item FROM foo LIMIT 1", new NpgsqlConnection("server=192.168.1.20;uid=alex;database=test")))
            {
                command.Connection.Open();

                NpgsqlDataReader reader = command.ExecuteReader();
                while (reader.Read())
                {
                    NpgsqlComplex num = (NpgsqlComplex)reader["item"];
                    Console.WriteLine("{0} + {1}i", num.R, num.I);
                }
                reader.Close();

                command.CommandText = "INSERT INTO foo VALUES (@item)";
                command.Parameters.Add("@item", NpgsqlDbType.Complex);
                command.Parameters["@item"].Value = new NpgsqlComplex(15.5, -5);

                command.ExecuteNonQuery();
            }
        }
    }
}

Aunque el ejemplo es bastante trivial y tiene poco o nulo valor en aplicaciones reales, este ejemplo intenta mostrar uno de los beneficios del uso de Software Libre: el hecho de poder acomodar a nuestras necesidades las cosas que usemos.

Nota: Si alguien le interesa el ejemplo mostrado, puede descargar una versión modificada de Npgsql (para Visual Studio 2005) más el proyecto de prueba.

One Reply to “Tipos personalizados de PostgreSQL y Npgsql”

Comments are closed.