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

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 obtuve 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 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 el where quote_ident($1).id=1 parte que entiendo 1, lo que significa que algo está seleccionado. ¿Por qué la primera quote_ident($1) trabajo y el segundo no al mismo tiempo? ¿Y cómo se podría solucionar esto?

preguntado el 22 de mayo de 12 a las 16:05

Sé que esta pregunta es un poco 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 en cierto modo: para permitirle consultar y ver qué objetos existen en la base de datos. Solo una idea. -

@DavidS Gracias por un comentario, lo intentaré. -

8 Respuestas

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$;

Llamada con nombre calificado de esquema (ver a continuación):

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

o:

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

Puntos principales

Usa una OUT parámetro para simplificar la función. Puede seleccionar directamente el resultado del SQL dinámico y listo. No hay necesidad de variables y código adicionales.

EXISTS hace exactamente lo que quieres. Usted obtiene true si la fila existe o false de lo contrario. Hay varias maneras de hacer esto, EXISTS suele ser más eficiente.

Parece que quieres un entero atrás, así que eché el boolean el resultado de EXISTS a integer, que produce exactamente lo que tenía. volvería booleano preferiblemente.

Yo uso el tipo de identificador de objeto regclass como tipo de entrada para _tbl. eso lo hace todo quote_ident(_tbl) or format('%I', _tbl) haría, pero mejor, porque:

  • .. previene inyección SQL igual de bien.

  • .. 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. (A regclass parámetro sólo es aplicable para existente mesas.)

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

Solo funciona para existente mesas, obviamente.

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

EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'

No es necesario calificar en la tabla el id columna mientras que sólo hay una tabla en el FROM lista. No hay ambigüedad posible en este ejemplo. Comandos SQL (dinámicos) dentro EXECUTE tiene un alcance separado, las variables de función o los parámetros no están visibles allí, a diferencia de los comandos SQL simples en el cuerpo de la función.

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

db <> violín aquí demostración de inyección SQL
Viejo sqlfiddle

Respondido el 11 de Septiembre de 21 a las 22:09

@suhprano: Claro. Intentalo: DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$; - Erwin Brandstetter

¿Por qué %s y no %L? - Loto

@Lotus: La explicación está en la respuesta. regclass los valores se escapan automáticamente cuando se envían como texto. %L serí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 conteo de filas en la tabla, select table_rows('nf_part1'); - Ferris

¿Cómo podemos obtener todas las columnas? - Ashish

Si es posible, no hagas esto.

Esa es la respuesta: es un antipatró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 es 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 uno puede escribir el número del piso deseado. Después de presionar el botón Ir, mueve una mano mecánica hacia el 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 ejemplo tonto de ascensor fue *el mejor dispositivo que pude imaginar* para señalar de manera sucinta los problemas con esta técnica. Agrega una capa inútil de direccionamiento indirecto, 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 un código SQL oscuro/extraño del lado del servidor.

Tal división de responsabilidades a través del 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 nombre de un código personalizado lleno de posibilidades de error.

Aquí hay puntos detallados sobre algunos de los problemas potenciales con este enfoque:

  • Dynamic SQL ofrece la posibilidad de inyección de SQL que es difícil de reconocer en el código de front-end o en el código de back-end solo (uno debe inspeccionarlos 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 la persona que llama no. Según tengo entendido, sin especial cuidado, 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á usar objetos privilegiados en absoluto, o tendrá que abrirlos a todos los clientes, lo que aumenta el área de superficie de posible ataque a los datos privilegiados. Configurar el SP/función 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 hace las cosas más complicadas. Esto exacerba el riesgo de inyección de SQL mencionado en el punto anterior, al convertir el SQL dinámico en un vector de ataque muy atractivo.

  • Cuando un desarrollador debe entender qué está haciendo el código de la aplicación para modificarlo o corregir un error, le resultará muy difícil ejecutar la consulta SQL exacta. Se puede usar el generador de perfiles de SQL, pero 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 por un beneficio cuestionable (requiere acomodar nuevas tablas, purgar datos antiguos, etc.) y no es muy obvio. De hecho, algunas aplicaciones están diseñadas de tal manera que el desarrollador no tiene las credenciales de la base de datos, por lo que le resulta casi imposible ver la consulta que se envía.

  • Cuando ocurre un error, como cuando intenta seleccionar una tabla que no existe, recibirá un mensaje como "nombre de objeto no válido" de la base de datos. Eso sucederá exactamente igual ya sea que 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 los problemas del sistema tiene que profundizar un nivel más en otra cueva debajo de aquella donde el problema existe, profundizar en el procedimiento maravilloso que lo hace todo para tratar de descubrir cuál es el problema. Los registros no mostrarán "Error en GetWidget", mostrarán "Error en OneProcedureToRuleThemAllRunner". Esta abstracción generalmente hará un sistema peor.

Un ejemplo en pseudo-C# de cambio de nombres de tablas en función de un parámetro:

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

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

respondido 24 mar '20, 16:03

No estoy completamente de acuerdo con eso. Digamos que presiona este botón "Ir" y luego algún mecanismo verifica si existe el piso. 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 necesitas hacer algunas correcciones en su lógica, bueno, esta elección no es tan dramática, supongo. - Fulano de Tal

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 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 duplique pobremente una abstracción existente es Mal. La base de datos en sí es ya haya utilizado una capa de abstracción que asigna nombres a conjuntos de datos. - ErikE

Correcto. El objetivo de SQL es expresar el conjunto de datos que desea extraer. Lo único que hace esta función es encapsular una instrucción SQL "enlatada". Dado el hecho de que el identificador también está codificado, todo huele mal. - Nick Hristov

@tres Hasta que alguien esté en el maestría fase (ver el modelo Dreyfus de adquisición de habilidades) de una habilidad, simplemente debe obedecer absolutamente reglas como "NO pasar nombres de tablas a un procedimiento para usar en SQL dinámico". Incluso insinuar que no siempre es malo es en sí mismo mal consejo. Sabiendo esto, ¡el principiante se verá tentado a usarlo! Eso es malo. Solo los maestros de un tema deberían romper las reglas, ya que son los únicos con la experiencia para saber en cualquier caso particular si tal ruptura de reglas realmente tiene sentido. - ErikE

@ three-cups actualicé con muchos más detalles sobre por qué es una mala idea. - ErikE

Dentro del código plpgsql, El EJECUTAR La declaración debe usarse para consultas en las que los nombres de tablas o columnas provienen de variables. También el IF EXISTS (<query>) No se permite la construcción cuando query se 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;

contestado el 23 de mayo de 12 a las 00:05

Gracias, estaba haciendo lo mismo hace un par de minutos cuando leí tu respuesta. La única diferencia es que tuve que quitar quote_ident() porque agregó comillas adicionales, lo que me sorprendió un poco, bueno, porque se usa en la mayoría de los ejemplos. - Fulano de Tal

Esas comillas 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 tabla) - Daniel Vérité

Y, por cierto, ¿podría proporcionar un enlace que demuestre que IF EXISTS <query> construcción no existe? Estoy bastante seguro de que vi algo así como una muestra de código de trabajo. - Fulano de Tal

@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

lo siento, tienes razón if exists(<query>), es válido en el caso general. Acabo de comprobar y modificar la respuesta en consecuencia. - Daniel Vérité

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 lo ideal. Es un trabajo tedioso y propenso a errores, y pierde la capacidad de parametrizar: los parámetros deben interpolarse en constantes en el 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;

contestado el 18 de mayo de 20 a las 19:05

Es incluso un hilo más antiguo ahora :). Por si acaso, "temporal" requiere que el esquema también sea temporal. Puede omitir esa palabra clave y hacer la limpieza según sea necesario. Aparte de las discusiones ortodoxas aquí, es una técnica útil al menos para algunas tareas administrativas. - full.stack.ex

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

Trata 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 (llamada quote_ident) con una fila (la variable $1 o en este caso particular table_that_does_not_exist).

Lo que desea hacer requerirá SQL dinámico, que es en realidad el lugar donde el quote_* funciones están destinadas a ser utilizadas.

contestado el 22 de mayo de 12 a las 17:05

Muchas gracias Matt, table_that_does_not_exist dio el mismo resultado, tienes razón. - Fulano de Tal

Si la pregunta era para 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;

Respondido el 21 de diciembre de 17 a las 14:12

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

Respondido 28 ago 18, 11:08

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');

Me funciona bien.

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

contestado el 26 de mayo de 15 a las 23:05

Creación de un new table es diferente de 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 de 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 la inyección? - dm3

Ups, es realmente peligroso. ¡Gracias por la respuesta! - dm3

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.