Nombre de la tabla como parámetro de función de PostgreSQL

85

Quiero pasar un nombre de tabla como parámetro en una función de Postgres. Probé este código:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

Y tengo esto:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

Y aquí está el error que obtuve cuando lo cambié a esto select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Probablemente, quote_ident($1)funciona, porque sin la where quote_ident($1).id=1parte que obtengo 1, lo que significa que se selecciona algo. ¿Por qué el primero puede quote_ident($1)funcionar y el segundo no al mismo tiempo? ¿Y cómo podría solucionarse esto?

John Doe
fuente
Sé que esta pregunta es algo antigua, pero la encontré mientras buscaba la respuesta a otro problema. ¿No podría su función simplemente consultar el esquema_informativo? Quiero decir, para eso es de alguna manera: para permitirle consultar y ver qué objetos existen en la base de datos. Solo una idea.
David S
@DavidS Gracias por un comentario, lo intentaré.
John Doe

Respuestas:

125

Esto se puede simplificar y mejorar aún más:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Llame con un nombre calificado por esquema (consulte a continuación):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

O:

SELECT some_f('"my very uncommon table name"');

Puntos principales

  • Utilice un OUTparámetro para simplificar la función. Puede seleccionar directamente el resultado del SQL dinámico en él y listo. No se necesitan variables ni código adicionales.

  • EXISTShace exactamente lo que quiere. Obtienes truesi la fila existe o falseno. Hay varias formas de hacer esto, EXISTSnormalmente es la más eficiente.

  • Parece que quieres recuperar un número entero , así que lanzo el booleanresultado de EXISTSa integer, que da exactamente lo que tenías. En su lugar, devolvería boolean .

  • Utilizo el tipo de identificador de objeto regclasscomo tipo de entrada para _tbl. Eso hace todo quote_ident(_tbl)o format('%I', _tbl)lo haría, pero mejor, porque:

  • .. también evita la inyección de SQL .

  • .. falla inmediatamente y con más gracia si el nombre de la tabla no es válido / no existe / es invisible para el usuario actual. (Un regclassparámetro solo es aplicable a tablas existentes ).

  • .. funciona con nombres de tabla calificados por esquema, donde un simple quote_ident(_tbl)o format(%I)fallaría porque no pueden resolver la ambigüedad. Tendría que pasar y escapar los nombres de esquema y tabla por separado.

  • Todavía lo uso format(), porque simplifica la sintaxis (y para demostrar cómo se usa), pero con en %slugar de %I. Normalmente, las consultas son más complejas, por lo que format()ayudan más. Para el ejemplo simple, también podríamos concatenar:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • No es necesario calificar la idcolumna como tabla mientras solo haya una tabla en la FROMlista. No es posible ambigüedad en este ejemplo. Los comandos SQL (dinámicos) internos EXECUTEtienen un alcance separado , las variables de función o los parámetros no son visibles allí, a diferencia de los comandos SQL simples en el cuerpo de la función.

He aquí por qué siempre escapa la entrada del usuario para SQL dinámico correctamente:

db <> violín aquí demostrando la inyección SQL
Antiguo sqlfiddle

Erwin Brandstetter
fuente
2
@suhprano: Claro. Pruébelo:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter
¿Por qué% s y no% L?
Lotus
3
@Lotus: La explicación está en la respuesta. regclasslos valores se escapan automáticamente cuando se imprimen como texto. %Lestaría mal en este caso.
Erwin Brandstetter
CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; crear una función de recuento de filas de tabla,select table_rows('nf_part1');
l mingzhi
¿cómo podemos obtener todas las columnas?
Ashish
12

Si es posible, no hagas esto.

Esa es la respuesta: es un anti-patrón. Si el cliente conoce la tabla de la que quiere datos, entonces SELECT FROM ThatTable. Si una base de datos está diseñada de manera que esto sea necesario, parece estar diseñada de manera subóptima. Si una capa de acceso a datos necesita saber si existe un valor en una tabla, es fácil componer SQL en ese código y no es bueno insertar este código en la base de datos.

Para mí, esto parece como instalar un dispositivo dentro de un ascensor donde se puede ingresar el número del piso deseado. Después de presionar el botón Ir, mueve una mano mecánica al botón correcto para el piso deseado y lo presiona. Esto introduce muchos problemas potenciales.

Tenga en cuenta: no hay intención de burla, aquí. Mi tonto ejemplo de ascensor fue * el mejor dispositivo que pude imaginar * para señalar de manera sucinta problemas con esta técnica. Agrega una capa inútil de indirección, moviendo la elección del nombre de la tabla desde un espacio de llamada (usando un DSL, SQL robusto y bien entendido) a un híbrido usando código SQL del lado del servidor oscuro / extraño.

Tal división de responsabilidades mediante el movimiento de la lógica de construcción de consultas en SQL dinámico hace que el código sea más difícil de entender. Viola una convención estándar y confiable (cómo una consulta SQL elige qué seleccionar) en el nombre del código personalizado plagado de posibles errores.

A continuación, se detallan algunos de los problemas potenciales con este enfoque:

  • El SQL dinámico ofrece la posibilidad de una inyección de SQL que es difícil de reconocer en el código del front-end o solo en el código del back-end (se deben inspeccionar juntos para ver esto).

  • Los procedimientos y funciones almacenados pueden acceder a los recursos a los que el propietario de la función / SP tiene derechos, pero el que llama no. Por lo que tengo entendido, sin un cuidado especial, entonces, de forma predeterminada, cuando usa código que produce SQL dinámico y lo ejecuta, la base de datos ejecuta el SQL dinámico bajo los derechos de la persona que llama. Esto significa que no podrá utilizar objetos con privilegios en absoluto o tendrá que abrirlos a todos los clientes, lo que aumentará la superficie de posible ataque a los datos privilegiados. Configurar la función SP / en el momento de la creación para que siempre se ejecute como un usuario en particular (en SQL Server EXECUTE AS) puede resolver ese problema, pero complica las cosas. Esto agrava el riesgo de inyección de SQL mencionado en el punto anterior, al hacer del SQL dinámico un vector de ataque muy atractivo.

  • Cuando un desarrollador debe comprender qué está haciendo el código de la aplicación para modificarlo o corregir un error, le resultará muy difícil conseguir que se ejecute la consulta SQL exacta. Se puede usar el generador de perfiles SQL, pero esto requiere privilegios especiales y puede tener efectos negativos en el rendimiento de los sistemas de producción. El SP puede registrar la consulta ejecutada, pero esto aumenta la complejidad para un beneficio cuestionable (que requiere acomodar nuevas tablas, depurar datos antiguos, etc.) y no es bastante obvio. De hecho, algunas aplicaciones están diseñadas de tal manera que el desarrollador no tiene credenciales de base de datos, por lo que le resulta casi imposible ver la consulta que se envía.

  • Cuando se produce un error, como cuando intenta seleccionar una tabla que no existe, obtendrá un mensaje en la línea de "nombre de objeto no válido" de la base de datos. Eso sucederá exactamente igual si está componiendo el SQL en el back-end o en la base de datos, pero la diferencia es que algún desarrollador pobre que está tratando de solucionar problemas del sistema tiene que escribir un nivel más profundo en otra cueva debajo de la que está existe un problema, profundizar en el procedimiento maravilloso que lo hace todo para tratar de averiguar cuál es el problema. Los registros no mostrarán "Error en GetWidget", mostrará "Error en OneProcedureToRuleThemAllRunner". Esta abstracción por lo general hacer un sistema peor .

Un ejemplo en pseudo-C # de cambiar nombres de tablas según un parámetro:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Si bien esto no elimina todos los problemas posibles imaginables, los defectos que describí con la otra técnica están ausentes en este ejemplo.

ErikE
fuente
4
No estoy completamente de acuerdo con eso. Digamos, presionas este botón "Ir" y luego algunos mecanismos verifican, si el piso existe. Las funciones pueden usarse en disparadores, que a su vez pueden verificar algunas condiciones. Esta decisión puede no ser la más hermosa, pero si el sistema ya es lo suficientemente grande y necesita hacer algunas correcciones en su lógica, bueno, esta elección no es tan dramática, supongo.
John Doe
1
Pero considere que la acción de intentar presionar un botón que no existe simplemente generará una excepción sin importar cómo lo maneje. En realidad, no puede presionar un botón inexistente, por lo que no hay ningún beneficio en agregar, además de presionar un botón, una capa para verificar si hay números inexistentes, ¡ya que dicha entrada de número no existía antes de crear dicha capa! La abstracción es, en mi opinión, la herramienta más poderosa en programación. Sin embargo, agregar una capa que simplemente duplica de manera deficiente una abstracción existente es incorrecto . La base de datos en sí ya es una capa de abstracción que asigna nombres a conjuntos de datos.
ErikE
3
Correcto. El objetivo de SQL es expresar el conjunto de datos que desea extraer. Lo único que hace esta función es encapsular una declaración SQL "enlatada". Dado que el identificador también está codificado, todo tiene mal olor.
Nick Hristov
1
@three Hasta que alguien esté en la fase de dominio (ver el modelo de Dreyfus de adquisición de habilidades ) de una habilidad, simplemente debe obedecer absolutamente reglas como "NO pasar nombres de tablas a un procedimiento para ser utilizado en SQL dinámico". Incluso insinuar que no siempre es malo es en sí mismo un mal consejo . ¡Sabiendo esto, el principiante se sentirá tentado a usarlo! Eso es malo. Solo los expertos en un tema deberían romper las reglas, ya que son los únicos con la experiencia para saber, en un caso particular, si tal violación realmente tiene sentido.
ErikE
1
@ Three-Cup actualicé con muchos más detalles sobre por qué es una mala idea.
ErikE
10

Dentro del código plpgsql, la instrucción EXECUTE debe usarse para consultas en las que los nombres de tablas o columnas provienen de variables. Además, la IF EXISTS (<query>)construcción no está permitida cuando queryse genera dinámicamente.

Aquí está su función con ambos problemas solucionados:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
Daniel Vérité
fuente
Gracias, estaba haciendo lo mismo hace un par de minutos cuando leí tu respuesta. La única diferencia es que tuve que eliminarlo quote_ident()porque agregó comillas adicionales, lo que me sorprendió un poco, bueno, porque se usa en la mayoría de los ejemplos.
John Doe
Esas citas adicionales serán necesarias si / cuando el nombre de la tabla contiene caracteres fuera de [az], o si / cuando choca con un identificador reservado (ejemplo: "grupo" como nombre de la tabla)
Daniel Vérité
Y, por cierto, ¿podría proporcionar un enlace que demuestre que la IF EXISTS <query>construcción no existe? Estoy bastante seguro de que vi algo así como una muestra de código de trabajo.
John Doe
1
@JohnDoe: IF EXISTS (<query>) THEN ...es una construcción perfectamente válida en plpgsql. Simplemente no con SQL dinámico para <query>. Lo uso mucho. Además, esta función se puede mejorar bastante. Publiqué una respuesta.
Erwin Brandstetter
1
Lo siento, tienes razón if exists(<query>), es válido en el caso general. Solo verifiqué y modifiqué la respuesta en consecuencia.
Daniel Vérité
4

El primero en realidad no "funciona" en el sentido que usted quiere decir, solo funciona en la medida en que no genera un error.

Inténtelo SELECT * FROM quote_ident('table_that_does_not_exist');y verá por qué su función devuelve 1: la selección devuelve una tabla con una columna (nombrada quote_ident) con una fila (la variable $1o en este caso particular table_that_does_not_exist).

Lo que desee hacer requerirá SQL dinámico, que en realidad es el lugar donde quote_*se deben usar las funciones.

Mate
fuente
Muchas gracias, Matt, table_that_does_not_existdio el mismo resultado, tienes razón.
John Doe
2

Si la pregunta era probar si la tabla está vacía o no (id = 1), aquí hay una versión simplificada del proceso almacenado de Erwin:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;
Julien Feniou
fuente
1

Sé que este es un hilo antiguo, pero lo encontré recientemente cuando intentaba resolver el mismo problema, en mi caso, para algunos scripts bastante complejos.

Convertir todo el script en SQL dinámico no es ideal. Es un trabajo tedioso y propenso a errores, y pierde la capacidad de parametrizar: los parámetros deben interpolarse en constantes en SQL, con malas consecuencias para el rendimiento y la seguridad.

Aquí hay un truco simple que le permite mantener el SQL intacto si solo necesita seleccionar de su tabla: use SQL dinámico para crear una vista temporal:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;
Nathan Meyers
fuente
0

Si desea que el nombre de la tabla, el nombre de la columna y el valor se pasen dinámicamente para funcionar como parámetro

usa este código

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value
Sandip Debnath
fuente
-2

Tengo la versión 9.4 de PostgreSQL y siempre uso este código:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

Y entonces:

SELECT add_new_table('my_table_name');

Funciona bien para mi.

¡Atención! El ejemplo anterior es uno de los que muestra "Cómo no hacerlo si queremos mantener la seguridad durante la consulta de la base de datos": P

dm3
fuente
1
Crear una newtabla es diferente a operar con el nombre de una tabla existente. De cualquier manera, debe escapar de los parámetros de texto ejecutados como código o está abierto a la inyección SQL.
Erwin Brandstetter
Oh, sí, mi error. El tema me engañó y además no lo leí hasta el final. Normalmente en mi caso. : P ¿Por qué el código con un parámetro de texto está expuesto a inyección?
dm3
Vaya, es realmente peligroso. ¡Gracias por la respuesta!
dm3