Tabellenname als PostgreSQL-Funktionsparameter

84

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 folgendes:

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 beim Ändern erhalten habe 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...

Funktioniert wahrscheinlich, quote_ident($1)weil ohne den where quote_ident($1).id=1Teil, den ich bekomme 1, etwas ausgewählt wird. Warum darf das erste quote_ident($1)und das zweite nicht gleichzeitig funktionieren? Und wie könnte das gelöst werden?

John Doe
quelle
Ich weiß, dass diese Frage etwas alt ist, aber ich habe sie gefunden, als ich nach der Antwort auf ein anderes Problem gesucht habe. Könnte Ihre Funktion nicht einfach das Informationsschema abfragen? Ich meine, dafür ist es in gewisser Weise gedacht - damit Sie abfragen und sehen können, welche Objekte in der Datenbank vorhanden sind. Nur eine Idee.
David S
@ DavidS Danke für einen Kommentar, das werde ich versuchen.
John Doe

Antworten:

121

Dies kann weiter vereinfacht und verbessert werden:

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

Aufruf mit schemaqualifiziertem 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 OUTParameter , um die Funktion zu vereinfachen. Sie können das Ergebnis des dynamischen SQL direkt auswählen und fertig. Keine zusätzlichen Variablen und Code erforderlich.

  • EXISTSmacht genau das, was Sie wollen. Sie erhalten, trueob die Zeile vorhanden ist oder falsenicht. Es gibt verschiedene Möglichkeiten, dies zu tun, EXISTSist in der Regel am effizientesten.

  • Sie scheinen eine ganze Zahl zurück zu wollen , also habe ich das booleanErgebnis von EXISTSbis umgewandelt integer, was genau das ergibt, was Sie hatten. Ich würde stattdessen boolean zurückgeben .

  • Ich verwende den Objektkennungstyp regclassals Eingabetyp für _tbl. Das macht alles quote_ident(_tbl)oder format('%I', _tbl)würde es tun, aber besser, weil:

  • .. es verhindert genauso gut die SQL-Injection .

  • .. es schlägt sofort und eleganter fehl, wenn der Tabellenname ungültig ist / nicht existiert / für den aktuellen Benutzer unsichtbar ist. (Ein regclassParameter gilt nur für vorhandene Tabellen.)

  • .. funktioniert mit schemaqualifizierten Tabellennamen, bei denen eine einfache quote_ident(_tbl)oder format(%I)fehlschlagen würde, weil sie die Mehrdeutigkeit nicht auflösen können. Sie müssten Schema- und Tabellennamen separat übergeben und maskieren.

  • Ich benutze immer noch format(), weil es die Syntax vereinfacht (und um zu demonstrieren, wie es verwendet wird), aber mit %sstatt %I. In der Regel sind Abfragen komplexer und format()helfen daher mehr. Für das einfache Beispiel könnten wir auch einfach verketten:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
  • Die idSpalte muss nicht tabellenqualifiziert werden, solange die FROMListe nur eine einzige Tabelle enthält . In diesem Beispiel ist keine Mehrdeutigkeit möglich. (Dynamische) SQL-Befehle im Inneren EXECUTEhaben einen separaten Bereich , Funktionsvariablen oder Parameter sind dort nicht sichtbar - im Gegensatz zu einfachen SQL-Befehlen im Funktionskörper.

Hier ist , warum Sie immer richtig Benutzereingabe für die dynamische SQL entkommen:

db <> fiddle demonstriert hier die SQL-Injection
Old sqlfiddle

Erwin Brandstetter
quelle
2
@suhprano: Sicher. Probieren Sie es aus:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter
warum% s und nicht% L?
Lotus
3
@ Lotus: Die Erklärung ist in der Antwort. regclassWerte werden bei der Ausgabe als Text automatisch maskiert. %Lwäre in diesem Fall falsch .
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; Erstellen Sie eine Tabellenzeilenzahl,select table_rows('nf_part1');
l mingzhi
Wie können wir alle Spalten bekommen?
Ashish
13

Wenn überhaupt möglich, tu das nicht.

Das ist die Antwort - es ist ein Anti-Muster. Wenn der Client die Tabelle kennt, aus der er Daten erhalten möchte, dann SELECT FROM ThatTable. Wenn eine Datenbank so gestaltet ist, dass dies erforderlich ist, scheint sie nicht optimal gestaltet zu sein. Wenn eine Datenzugriffsschicht wissen muss, ob ein Wert in einer Tabelle vorhanden ist, ist es einfach, SQL in diesem Code zu erstellen, und das Verschieben dieses Codes in die Datenbank ist nicht gut.

Für mich scheint dies die Installation eines Geräts in einem Aufzug zu sein, in dem man die Nummer der gewünschten Etage eingeben kann. Nachdem die Go-Taste gedrückt wurde, bewegt sie eine mechanische Hand auf die richtige Taste für den gewünschten Boden und drückt sie. Dies führt zu vielen potenziellen Problemen.

Bitte beachten Sie: Hier besteht keine Spottabsicht. Mein albernes Beispiel für einen Aufzug war * das beste Gerät, das ich mir vorstellen konnte *, um kurz und bündig auf Probleme mit dieser Technik hinzuweisen. Es fügt eine nutzlose Indirektionsebene hinzu und verschiebt die Auswahl von Tabellennamen aus einem Aufruferbereich (unter Verwendung eines robusten und gut verstandenen DSL, SQL) in einen Hybrid, der obskuren / bizarren serverseitigen SQL-Code verwendet.

Eine solche Aufteilung der Verantwortung durch die Verlagerung der Abfragekonstruktionslogik in dynamisches SQL erschwert das Verständnis des Codes. Es verstößt gegen eine standardmäßige und zuverlässige Konvention (wie eine SQL-Abfrage auswählt, was ausgewählt werden soll) im Namen von benutzerdefiniertem Code, der mit potenziellen Fehlern behaftet ist.

Hier finden Sie detaillierte Punkte zu einigen potenziellen Problemen bei diesem Ansatz:

  • Dynamic SQL bietet die Möglichkeit der SQL-Injection, die im Front-End-Code oder nur im Back-End-Code schwer zu erkennen ist (man muss sie zusammen überprüfen, um dies zu sehen).

  • Gespeicherte Prozeduren und Funktionen können auf Ressourcen zugreifen, für die der SP- / Funktionsbesitzer Rechte hat, der Aufrufer jedoch nicht. Soweit ich weiß, führt die Datenbank ohne besondere Sorgfalt standardmäßig, wenn Sie Code verwenden, der dynamisches SQL erzeugt und ausführt, das dynamische SQL unter den Rechten des Aufrufers aus. Dies bedeutet, dass Sie entweder überhaupt keine privilegierten Objekte verwenden können oder sie für alle Clients öffnen müssen, um die Oberfläche potenzieller Angriffe auf privilegierte Daten zu vergrößern. Das Festlegen der SP / Funktion zur Erstellungszeit, um immer als ein bestimmter Benutzer (in SQL Server EXECUTE AS) ausgeführt zu werden, kann dieses Problem lösen, macht die Dinge jedoch komplizierter. Dies erhöht das im vorherigen Punkt erwähnte Risiko einer SQL-Injection, indem das dynamische SQL zu einem sehr verlockenden Angriffsvektor gemacht wird.

  • Wenn ein Entwickler verstehen muss, was der Anwendungscode tut, um ihn zu ändern oder einen Fehler zu beheben, fällt es ihm sehr schwer, die genaue SQL-Abfrage auszuführen. SQL-Profiler kann verwendet werden, dies erfordert jedoch besondere Berechtigungen und kann negative Auswirkungen auf die Leistung von Produktionssystemen haben. Die ausgeführte Abfrage kann vom SP protokolliert werden, dies erhöht jedoch die Komplexität für fragwürdige Vorteile (das Aufnehmen neuer Tabellen, das Löschen alter Daten usw.) und ist nicht offensichtlich. Tatsächlich sind einige Anwendungen so aufgebaut, dass der Entwickler nicht über Datenbankanmeldeinformationen verfügt, sodass er die übermittelte Abfrage kaum mehr sehen kann.

  • Wenn ein Fehler auftritt, z. B. wenn Sie versuchen, eine nicht vorhandene Tabelle auszuwählen, wird in der Datenbank eine Meldung mit dem Titel "Ungültiger Objektname" angezeigt. Das wird genauso passieren, egal ob Sie das SQL im Back-End oder in der Datenbank erstellen, aber der Unterschied ist, dass ein armer Entwickler, der versucht, das System zu beheben, eine Ebene tiefer in eine weitere Höhle unterhalb der Höhle spelunk muss Es gibt ein Problem, sich mit der Wunderprozedur zu befassen, die es tut, um herauszufinden, wo das Problem liegt. In den Protokollen wird nicht "Fehler in GetWidget", sondern "Fehler in OneProcedureToRuleThemAllRunner" angezeigt. Diese Abstraktion wird ein System im Allgemeinen verschlechtern .

Ein Beispiel in Pseudo-C # für das Umschalten von Tabellennamen basierend auf einem Parameter:

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

Dies beseitigt zwar nicht alle denkbaren Probleme, aber die Fehler, die ich mit der anderen Technik skizziert habe, fehlen in diesem Beispiel.

ErikE
quelle
3
Dem stimme ich nicht ganz zu. Angenommen, Sie drücken diese "Los" -Taste und dann überprüfen einige Mechanismen, ob der Boden vorhanden ist. Funktionen können in Triggern verwendet werden, die wiederum einige Bedingungen überprüfen können. Diese Entscheidung ist vielleicht nicht die schönste, aber wenn das System bereits groß genug ist und Sie einige Korrekturen in seiner Logik vornehmen müssen, ist diese Wahl vermutlich nicht so dramatisch.
John Doe
1
Bedenken Sie jedoch, dass der Versuch, eine nicht vorhandene Schaltfläche zu drücken, einfach eine Ausnahme generiert, unabhängig davon, wie Sie damit umgehen. Sie können eine nicht vorhandene Schaltfläche nicht drücken, daher ist es nicht vorteilhaft, zusätzlich zum Drücken einer Schaltfläche eine Ebene hinzuzufügen, um nach nicht vorhandenen Nummern zu suchen, da ein solcher Nummerneintrag nicht vorhanden war, bevor Sie diese Ebene erstellt haben! Abstraktion ist meiner Meinung nach das mächtigste Werkzeug in der Programmierung. Das Hinzufügen einer Ebene, die eine vorhandene Abstraktion nur schlecht dupliziert, ist jedoch falsch . Die Datenbank selbst ist bereits eine Abstraktionsschicht, die Datensätzen Namen zuordnet.
ErikE
3
Genau richtig. Der springende Punkt bei SQL ist, den Datensatz auszudrücken, den Sie extrahieren möchten. Diese Funktion kapselt nur eine "vordefinierte" SQL-Anweisung. Angesichts der Tatsache, dass die Kennung auch fest codiert ist, riecht das Ganze schlecht.
Nick Hristov
1
@three Bis sich jemand in der Beherrschungsphase (siehe das Dreyfus-Modell des Skill-Erwerbs ) einer Skill befindet, sollte er einfach Regeln wie "Tabellennamen NICHT an eine Prozedur übergeben, die in dynamischem SQL verwendet werden soll" unbedingt befolgen. Selbst der Hinweis, dass es nicht immer schlecht ist, ist selbst ein schlechter Rat . Wenn der Anfänger dies weiß, wird er versucht sein, es zu benutzen! Das ist schlecht. Nur Meister eines Themas sollten gegen die Regeln verstoßen, da sie die einzigen sind, die die Erfahrung haben, in einem bestimmten Fall zu wissen, ob ein solcher Regelverstoß tatsächlich Sinn macht.
ErikE
1
@ Drei-Tassen Ich habe mit viel mehr Details aktualisiert, warum es eine schlechte Idee ist.
ErikE
10

Innerhalb des plpgsql-Codes muss die EXECUTE- Anweisung für Abfragen verwendet werden, bei denen Tabellennamen oder Spalten von Variablen stammen. Auch das IF EXISTS (<query>)Konstrukt ist nicht erlaubt wennquery es 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;
Daniel Vérité
quelle
Vielen Dank, ich habe es vor ein paar Minuten genauso gemacht, als ich Ihre Antwort gelesen habe. Der einzige Unterschied ist, dass ich entfernen musste, quote_ident()weil es zusätzliche Anführungszeichen hinzufügte, was mich ein wenig überraschte , weil es in den meisten Beispielen verwendet wird.
John Doe
Diese zusätzlichen Anführungszeichen werden benötigt, wenn der Tabellenname Zeichen außerhalb von [az] enthält oder wenn er mit einem reservierten Bezeichner kollidiert (Beispiel: "Gruppe" als Tabellenname)
Daniel Vérité
Könnten Sie übrigens bitte einen Link angeben, der beweisen würde, dass das IF EXISTS <query>Konstrukt nicht existiert? Ich bin mir ziemlich sicher, dass ich so etwas als funktionierendes Codebeispiel gesehen habe.
John Doe
1
@ JohnDoe: IF EXISTS (<query>) THEN ...ist ein perfekt gültiges Konstrukt in plpgsql. Nur nicht mit dynamischem SQL für <query>. Ich benutze es oft. Auch diese Funktion kann erheblich verbessert werden. Ich habe eine Antwort gepostet.
Erwin Brandstetter
1
Entschuldigung, Sie haben Recht if exists(<query>), es ist im allgemeinen Fall gültig. Einfach überprüft und die Antwort entsprechend geändert.
Daniel Vérité
4

Das erste "funktioniert" nicht in dem Sinne, wie Sie es meinen, es funktioniert nur insoweit, als es keinen Fehler erzeugt.

Versuchen Sie es 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 (benannt quote_ident) mit einer Zeile (der Variablen $1oder in diesem speziellen Fall table_that_does_not_exist) zurück.

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

Matt
quelle
Vielen Dank, Matt, table_that_does_not_existgab das gleiche Ergebnis, Sie haben Recht.
John Doe
2

Wenn die Frage lautete, ob die Tabelle leer ist oder nicht (id = 1), finden Sie 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;
Julien Feniou
quelle
1

Ich weiß, dass dies ein alter Thread ist, aber ich bin kürzlich darauf gestoßen, als ich versucht habe, dasselbe Problem zu lösen - in meinem Fall für einige ziemlich komplexe Skripte.

Es ist nicht ideal, das gesamte Skript in dynamisches SQL umzuwandeln. Es ist mühsam und fehleranfällig, und Sie verlieren die Fähigkeit zur Parametrisierung: Parameter müssen in SQL in Konstanten interpoliert werden, was sich negativ auf Leistung und Sicherheit auswirkt.

Hier ist ein einfacher Trick, mit dem Sie SQL intakt halten können, wenn Sie nur aus Ihrer Tabelle auswählen müssen - verwenden Sie dynamisches SQL, um eine temporäre Ansicht zu erstellen:

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
quelle
0

Wenn Sie möchten, dass Tabellenname, Spaltenname und Wert dynamisch als Parameter übergeben werden

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
Sandip Debnath
quelle
-2

Ich habe eine 9.4-Version von PostgreSQL und verwende immer diesen Code:

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

Und dann:

SELECT add_new_table('my_table_name');

Es funktioniert gut für mich.

Beachtung! Das obige Beispiel ist eines davon, das zeigt: "Wie geht das nicht, wenn wir die Sicherheit beim Abfragen der Datenbank gewährleisten wollen?": P.

dm3
quelle
1
Das Erstellen einer newTabelle unterscheidet sich vom Arbeiten mit dem Namen einer vorhandenen Tabelle. In beiden Fällen sollten Sie als Code ausgeführte Textparameter maskieren, oder Sie sind offen für SQL-Injection.
Erwin Brandstetter
Oh ja, mein Fehler. Das Thema hat mich irregeführt und außerdem habe ich es nicht bis zum Ende gelesen. Normalerweise in meinem Fall. : P Warum ist Code mit einem Textparameter der Injektion ausgesetzt?
dm3
Ups, es ist wirklich gefährlich. Danke für die Antwort!
dm3