it-swarm.com.de

Tabellenname als PostgreSQL-Funktionsparameter

Ich möchte einen Tabellennamen als Parameter in einer Postgres-Funktion übergeben. Ich habe diesen Code ausprobiert:

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

Und ich habe das bekommen:

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 "."

Und hier ist der Fehler, den ich bekam, als ich select * from quote_ident($1) tab where tab.id=1 änderte:

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

Wahrscheinlich funktioniert quote_ident($1), denn ohne den where quote_ident($1).id=1-Teil bekomme ich 1, was bedeutet, dass etwas ausgewählt ist. Warum kann die erste quote_ident($1) und die zweite nicht gleichzeitig arbeiten? Und wie konnte das gelöst werden?

60
John Doe

Dies kann weiter vereinfacht und verbessert werden:

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

Aufruf mit Schema-qualifiziertem Namen (siehe unten):

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

Oder:

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

Hauptpunkte

  • Verwenden Sie einen OUT-Parameter, um die Funktion zu vereinfachen. Sie können das Ergebnis der dynamischen SQL direkt auswählen und fertig sein. Keine zusätzlichen Variablen und Code erforderlich.

  • EXISTS macht genau das, was Sie wollen. Sie erhalten true, wenn die Zeile existiert, oder false. Dafür gibt es verschiedene Möglichkeiten, EXISTS ist normalerweise am effizientesten.

  • Sie scheinen ein integer zurück zu haben, also wandere ich das boolean-Ergebnis von EXISTS in integer um, was genau das ergibt, was Sie hatten. Ich würde stattdessen boolean zurückgeben.

  • Ich benutze den Objektkennungstyp regclass als Eingabetyp für _tbl. Das tut alles quote_ident(_tbl) oder format('%I', _tbl) , würde aber besser sein, weil: 

    • Es verhindert ebenso die SQL-Injection

    • Wenn der Tabellenname ungültig ist/nicht existiert/für den aktuellen Benutzer nicht sichtbar ist, schlägt er sofort und fehlerhafter fehl. (Ein regclass-Parameter gilt nur für vorhandene Tabellen.)

    • Es funktioniert mit Schema-qualifizierten Tabellennamen, bei denen eine einfache quote_ident(_tbl) oder format(%I) fehlschlagen würde, da sie die Mehrdeutigkeit nicht auflösen können. Sie müssen Schema- und Tabellennamen getrennt übergeben und entgehen.

  • Ich benutze weiterhin format() , weil es die Syntax vereinfacht (und um zu zeigen, wie es verwendet wird), aber mit %s anstelle von %I. In der Regel sind Abfragen komplexer, sodass format() mehr hilft. Für das einfache Beispiel könnten wir auch nur verketten:

    EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Es ist nicht erforderlich, die Spalte id in Tabellen zu qualifizieren, während sich in der Liste FROM nur eine einzige Tabelle befindet. In diesem Beispiel ist keine Mehrdeutigkeit möglich. (Dynamische) SQL-Befehle in EXECUTE haben einen separaten Gültigkeitsbereich, Funktionsvariablen oder Parameter sind dort nicht sichtbar - im Gegensatz zu einfachen SQL-Befehlen im Funktionskörper. 

Getestet mit PostgreSQL 9.1. format() erfordert mindestens diese Version.

Dies ist der Grund, warum Sie immer Benutzereingaben für dynamisches SQL ordnungsgemäß Escape-Anweisungen ausführen:

SQL Fiddle demonstriert die SQL-Injektion

91

Tu das nicht.

Das ist die Antwort. Es ist ein schreckliches Anti-Muster. Welchem ​​Zweck dient es? Wenn der Client die Tabelle kennt, von der er Daten haben möchte, dann SELECT FROM ThatTable! Wenn Sie Ihre Datenbank so erstellt haben, dass dies erforderlich ist, haben Sie sie wahrscheinlich falsch entworfen. Wenn Ihre Datenzugriffsebene wissen muss, ob ein Wert in einer Tabelle vorhanden ist, ist es äußerst einfach, den dynamischen SQL-Teil in diesem Code auszuführen. Es ist nicht gut, es in die Datenbank zu schieben.

Ich habe eine Idee: Lassen Sie uns ein Gerät in Aufzügen installieren, in das Sie die gewünschte Stockwerknummer eingeben können. Wenn Sie dann auf "Los" drücken, bewegt er eine mechanische Hand zur richtigen Taste für die gewünschte Etage und drückt sie für Sie. Revolutionär!

Anscheinend war meine Antwort zu kurz, so dass ich diesen Fehler detaillierter beheben kann.

Ich hatte nicht die Absicht, Spaß zu haben. Mein dummes Aufzugsbeispiel war das beste Gerät, das ich mir vorstellen konnte, um die Mängel der in der Frage vorgeschlagenen Technik kurz herauszustellen. Diese Technik fügt eine völlig nutzlose Schicht der Indirektion hinzu und verschiebt die Auswahl der Tabellennamen aus einem Aufruferbereich mit einem robusten und gut verstandenen DSL (SQL) unnötigerweise in einen Hybridbereich, der obskuren/bizarren, serverseitigen SQL-Code verwendet.

Diese Aufteilung der Verantwortung durch die Verschiebung der Abfragekonstruktionslogik in dynamisches SQL macht den Code schwieriger zu verstehen. Es zerstört eine völlig vernünftige Konvention (wie eine SQL-Abfrage die Auswahl auswählt) im Namen des benutzerdefinierten Codes, der mit Fehlerpotenzial behaftet ist.

  • Dynamic SQL bietet die Möglichkeit einer SQL-Injection, die im Front-End-Code oder im Back-End-Code nur schwer zu erkennen ist.

  • Gespeicherte Prozeduren und Funktionen können auf Ressourcen zugreifen, für die der SP/Funktionsbesitzer Rechte hat, der Aufrufer jedoch nicht. Soweit ich es verstehe, führt die Datenbank, wenn Sie Code verwenden, der dynamisches SQL erzeugt und ausführt, das dynamische SQL unter den Rechten des Aufrufers aus. Dies bedeutet, dass Sie privilegierte Objekte entweder überhaupt nicht verwenden können, oder Sie müssen sie für alle Clients öffnen, wodurch die Angriffsfläche auf privilegierte Daten vergrößert wird. Wenn Sie die SP/Funktion zum Zeitpunkt der Erstellung so einstellen, dass sie immer als bestimmter Benutzer ausgeführt wird (in SQL Server EXECUTE AS), kann dieses Problem möglicherweise behoben werden, was jedoch komplizierter wird. Dies verstärkt das im vorigen Punkt erwähnte Risiko einer SQL-Injektion, indem das dynamische SQL zu einem sehr verlockenden Angriffsvektor wird.

  • Wenn ein Entwickler verstehen muss, was der Anwendungscode tut, um ihn zu ändern oder einen Fehler zu beheben, wird es sehr schwierig sein, die genaue SQL-Abfrage zu erhalten, die gerade ausgeführt wird. Der SQL-Profiler kann verwendet werden, dies erfordert jedoch besondere Privilegien und kann negative Auswirkungen auf die Leistung auf Produktionssystemen haben. Die ausgeführte Abfrage kann von SP protokolliert werden. Dies erhöht jedoch die Komplexität ohne Grund (neue Tabellen pflegen, alte Daten löschen usw.) und ist absolut nicht offensichtlich. Tatsächlich sind einige Anwendungen so aufgebaut, dass der Entwickler keine Datenbankanmeldeinformationen hat, sodass es fast unmöglich wird, die übergebene Abfrage tatsächlich zu sehen.

  • Wenn ein Fehler auftritt, z. B. wenn Sie versuchen, eine nicht vorhandene Tabelle auszuwählen, erhalten Sie eine Meldung in der Zeile "ungültiger Objektname" aus der Datenbank. Dies geschieht genau gleich, egal ob Sie die SQL im Backend oder in der Datenbank zusammenstellen, aber der Unterschied ist, dass ein armer Entwickler, der versucht, das System zu behandeln, eine Ebene tiefer in eine weitere Höhle unterhalb der einen Ebene hinein spelt Problem besteht tatsächlich darin, in die Wunderprozedur zu graben, die Does It All macht und herauszufinden, was das Problem ist. Protokolle zeigen nicht "Fehler in GetWidget" an, sondern "Fehler in OneProcedureToRuleThemAllRunner". Diese Abstraktion macht Ihr System einfach schlimmer.

Hier ist ein weitaus besseres Beispiel für Pseudo-C # von Tabellennamen, die auf einem Parameter basieren:

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

Jeder Fehler, den ich bei der anderen Technik erwähnt habe, fehlt in diesem Beispiel vollständig.

Es gibt einfach keinen Zweck, keinen Nutzen, keine mögliche Verbesserung, wenn Sie einen Tabellennamen an eine gespeicherte Prozedur übergeben.

15
ErikE

Im plpgsql-Code muss die Anweisung EXECUTE für Abfragen verwendet werden, bei denen Tabellennamen oder -spalten aus Variablen stammen. Das IF EXISTS (<query>)-Konstrukt ist auch nicht zulässig, wenn query dynamisch generiert wird.

Hier ist Ihre Funktion mit beiden Problemen behoben:

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;
9
Daniel Vérité

Ersteres "arbeitet" eigentlich nicht in dem Sinne, wie Sie meinen, es funktioniert nur insoweit, als es keinen Fehler erzeugt.

Versuchen Sie SELECT * FROM quote_ident('table_that_does_not_exist'); und Sie werden sehen, warum Ihre Funktion 1 zurückgibt: Die Auswahl gibt eine Tabelle mit einer Spalte (mit dem Namen quote_ident) und einer Zeile zurück (die Variable $1 oder in diesem speziellen Fall table_that_does_not_exist).

Was Sie tun möchten, erfordert dynamisches SQL, das eigentlich der Ort ist, an dem die quote_*-Funktionen verwendet werden sollen.

3
Matt

Wenn die Frage war, ob die Tabelle leer ist oder nicht (id = 1), ist hier eine vereinfachte Version von Erwins gespeichertem Prozess: 

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;
0
Julien Feniou

Wenn der Tabellenname, der Spaltenname und der Wert dynamisch an die Funktion als Parameter übergeben werden sollen

verwenden Sie diesen Code

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
0
Sandip Debnath