Cómo pasar un tipo de tabla con un campo de matriz a una función en postgresql

8

tengo una mesa llamada libro

CREATE TABLE book
(
  id smallint NOT NULL DEFAULT 0,       
  bname text,       
  btype text,
  bprices numeric(11,2)[],
  CONSTRAINT key PRIMARY KEY (id )
)

y una función save_book

CREATE OR REPLACE FUNCTION save_book(thebook book)
  RETURNS text AS
$BODY$
DECLARE 
myoutput text :='Nothing has occured';
BEGIN

    update book set 
    bname=thebook.bname,
    btype=thebook.btype,bprices=thebook.bprices  WHERE id=thebook.id;

    IF FOUND THEN
        myoutput:= 'Record with PK[' || thebook.id || '] successfully updated';
        RETURN myoutput;
    END IF;

    BEGIN
        INSERT INTO book values(thebook.id,thebook.bname,thebook.btype,
        thebook.bprices);
        myoutput:= 'Record successfully added';           
    END;
 RETURN myoutput;

    END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;

ahora cuando llamo a la función

SELECT save_book('(179,the art of war,fiction,{190,220})'::book);

me sale el error

ERROR: malformed array literal: "{190"
SQL state: 22P02
Character: 18

No entiendo porque no veo ningún error en el formato de la matriz, ¿alguna ayuda?

indago
fuente
Su función se ve un poco complicada para mí, pero de todos modos. Un literal de Array debe estar entre comillas simples, a fin de tratar el siguiente: ave_book((179,the art of war,fiction,'{190,220}')::book. La fila construida no necesita las comillas.
dezso
cuando ejecuto eso me sale el error ERROR: syntax error at or near "art"
indago
1
Los literales de cadena individuales aún deben estar entre comillas.
Andriy M
Lo siento, la correcta es ave_book((179, 'the art of war', 'fiction', '{190,220}')::book, como dijo Andriy.
dezso
@dezso: Parece una respuesta para mí. :)
Andriy M

Respuestas:

7

Este tipo de cosas se complica. Estoy trabajando en algunos proyectos relacionados en este momento. El ajuste básico es que PostgreSQL usa un formato que usa comillas dobles internamente en representación de tuplas para representar valores literales, por lo tanto:

SELECT save_book('(179,the art of war,fiction,"{190,220}")'::book);

Deberia trabajar. En esencia, un buen truco es crear un csv y encerrarlo en identificadores de tuplas o matrices. El gran problema es que tiene que lidiar con el escape (duplicando las cotizaciones en todos los niveles según sea necesario). Entonces lo siguiente es exactamente equivalente:

SELECT save_book('(179,"the art of war","fiction","{""190"",""220""}")'::book);

El segundo enfoque es usar un constructor de filas:

SELECT save_book(row(179,'the art of war','fiction', array[190,220])::book);

La primera solución tiene la ventaja obvia de poder aprovechar los marcos de programación existentes para la generación y el escape de CSV. El segundo es más limpio en SQL. Se pueden mezclar y combinar.

Chris Travers
fuente
+1 por usar un constructor de matrices, escapar puede ser una pesadilla
Jack dice que intente topanswers.xyz
@ Chris, ¡La primera solución parece buena y para mí creo que es la mejor!
indago
@JackDouglas, una cosa que estamos viendo es ir a la primera ruta en LSMB específicamente porque podemos usar marcos CSV que ya están funcionando.
Chris Travers
6

Si alguna vez se pregunta acerca de la sintaxis correcta para un tipo de fila, pregunte a Postgres. Debe saber:

SELECT b FROM book b LIMIT 1;  -- or: WHERE id = 179;

Lo que devolverá una representación de texto de su fila en formato válido:

(179,"the art of war",fiction,"{190,220}")
  • Los valores de las columnas se representan como una lista sin comillas, separada por comas, encerrada entre paréntesis.

  • Las comillas dobles se usan alrededor de los valores, si puede haber ambigüedad, incluido el texto con espacios en blanco. Si bien en este caso particular las comillas dobles "the art of war"son opcionales, las comillas dobles "{190,220}"son necesarias para una matriz.

Encierre la cadena entre comillas simples, modifique y pruebe:

SELECT '(333,the art of war,fiction,"{191,220,235}")'::book

Función revisada

Considere lo que discutimos en la pregunta anterior relacionada:
Problema con el tipo compuesto en una función UPSERT

Un bloque separado ( BEGIN .. END;) solo es útil si desea capturar EXCEPTIONun INSERTaumento de poder. Dado que un bloque con excepción conlleva cierta sobrecarga, tiene sentido tener un bloque separado que nunca podría ingresarse:

CREATE OR REPLACE FUNCTION save_book(thebook book)
  RETURNS text AS
$BODY$
BEGIN
   UPDATE book
   SET    bname =   thebook.bname
         ,btype =   thebook.btype
         ,bprices = thebook.bprices
   WHERE  id = thebook.id;

   IF FOUND THEN
      RETURN format('Record with PK[%s] successfully updated', thebook.id);
   END IF;

   BEGIN
      INSERT INTO book SELECT (thebook).*;
      RETURN format('Record with PK[%s] successfully inserted', thebook.id);

   EXCEPTION WHEN unique_violation THEN
      UPDATE book
      SET    bname =   thebook.bname
            ,btype =   thebook.btype
            ,bprices = thebook.bprices
      WHERE  id = thebook.id;
   END;

   RETURN format('Record with PK[%s] successfully updated', thebook.id);
END
$BODY$  LANGUAGE plpgsql

De lo contrario, simplifique :

CREATE OR REPLACE FUNCTION save_book(thebook book)
  RETURNS text AS
$BODY$
BEGIN
   UPDATE book
   SET    bname =   thebook.bname
         ,btype =   thebook.btype
         ,bprices = thebook.bprices
   WHERE  id = thebook.id;

   IF FOUND THEN
      RETURN format('Record with PK[%s] successfully updated', thebook.id);
   END IF;

   INSERT INTO book SELECT (thebook).*;
   RETURN format('Record with PK[%s] successfully inserted', thebook.id);
END
$BODY$  LANGUAGE plpgsql

También simplifiqué tu INSERTdeclaración. Es seguro omitir la lista de columnas de INSERT en las circunstancias dadas.

Erwin Brandstetter
fuente
3

Si bien no veo la ventaja real de su solución, me refiero a pasar una fila a la función en lugar de pasar los valores individuales como en

CREATE OR REPLACE FUNCTION save_book2(
      integer
    , text
    , text
    , integer[]
)
RETURNS text AS
...

De todos modos, su solución también funciona si llama a la función correctamente:

SELECT ave_book((179, 'the art of war', 'fiction', '{190,220}')::book);

Es decir, la expresión de registro no necesita comillas, mientras que los valores de texto y la matriz literal sí.

dezso
fuente
Hice esa función para manejar todas las inserciones y actualizaciones protegiendo a la aplicación de tratar directamente con las tablas, ¿cree que tener esa función no agrega ninguna ventaja?
indago
Mis $ 0.02 en la decisión de diseño. Creo que tiene una gran ventaja siempre que tenga un código de aplicación para buscar la estructura del tipo involucrado y construir el argumento por usted. Esto le brinda una API reconocible que es muy útil. Sin eso, sin embargo, significa que si la estructura de su tabla cambia, tiene lugares adicionales para hacer un cambio que definitivamente no es bueno. Digo que este es uno que está moviendo mi desarrollo en la dirección que usted está yendo, y en general creo que es una buena idea. Sin embargo, tiene peligros.
Chris Travers