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=1
parte 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?
function
postgresql
plpgsql
dynamic-sql
identifier
John Doe
fuente
fuente
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$;
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
OUT
pará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.EXISTS
hace exactamente lo que quiere. Obtienestrue
si la fila existe ofalse
no. Hay varias formas de hacer esto,EXISTS
normalmente es la más eficiente.Parece que quieres recuperar un número entero , así que lanzo el
boolean
resultado deEXISTS
ainteger
, que da exactamente lo que tenías. En su lugar, devolvería boolean .Utilizo el tipo de identificador de objeto
regclass
como tipo de entrada para_tbl
. Eso hace todoquote_ident(_tbl)
oformat('%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
regclass
parámetro solo es aplicable a tablas existentes )... funciona con nombres de tabla calificados por esquema, donde un simple
quote_ident(_tbl)
oformat(%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%s
lugar de%I
. Normalmente, las consultas son más complejas, por lo queformat()
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
id
columna como tabla mientras solo haya una tabla en laFROM
lista. No es posible ambigüedad en este ejemplo. Los comandos SQL (dinámicos) internosEXECUTE
tienen 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
fuente
DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
regclass
los valores se escapan automáticamente cuando se imprimen como texto.%L
estaría mal en este caso.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');
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.
fuente
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 cuandoquery
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;
fuente
quote_ident()
porque agregó comillas adicionales, lo que me sorprendió un poco, bueno, porque se usa en la mayoría de los ejemplos.IF EXISTS <query>
construcción no existe? Estoy bastante seguro de que vi algo así como una muestra de código de trabajo.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.if exists(<query>)
, es válido en el caso general. Solo verifiqué y modifiqué la respuesta en consecuencia.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 (nombradaquote_ident
) con una fila (la variable$1
o en este caso particulartable_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.fuente
table_that_does_not_exist
dio el mismo resultado, tienes razón.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;
fuente
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;
fuente
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
fuente
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
fuente
new
tabla 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.