SQLite Data Studio: Routinen
Zurück: SQLite Data Studio
Eine Routine ist eine benannte SQL-Arbeitseinheit: ein Manifest mit Parametern und Rückgabeform, dazu die Abfrage selbst oder ein nativer Prozeduraufruf.
[Live]
Begriffe
| Begriff | Bedeutung |
|---|---|
| Routine | Benannte SQL-Arbeitseinheit: ein Manifest (.json) + eine/mehrere SQL-Dateien (.sql) oder ein nativer Prozeduraufruf. |
| Routine-Set | Verzeichnis mit Routinen, nach Art getrennt in die Ordner Functions/ und Procedures/. Das Set default ist der sql/-Ordner des Plugins; Consumer-Plugins registrieren eigene Sets (siehe API-Referenz). Adressierung: name (Default-Set) oder set:name. |
| Manifest | JSON mit parameters, returns und einer/mehreren per-Backend-implementations. |
| Backend | Engine-spezifisches Verhalten (Identifier-Quoting, Savepoints, lastInsertId, nativer Aufrufdialekt), gewählt aus dem PDO-Treiber der Verbindung. |
Schnellstart
$routines = plugin_load('helper', 'wksqliteds'); // gegen ein vorhandenes PDO: $affected = $routines->execute($pdo, 'close_inspection', ['id' => 123, 'user' => 'wolfgang']); // gegen eine benannte Verbindung: $rows = $routines->execute('reporting', 'reports:monthly', ['month' => '2026-07']); // über den wkcore-Service: $svc = ServiceHub::fromDokuWiki()->service('sql.routines'); $svc?->execute($pdo, 'close_inspection', ['id' => 123, 'user' => 'wolfgang']);
execute($connection, $routine, $params):
- $connection:
\PDO, ein Objekt mitgetPdo(), ein Verbindungs-Name, odernull(nutzt die Optiondefault_connection). - $routine:
„name“(Default-Set) oder„set:name“; ein angehängtes.jsonwird toleriert. - $params:
['name' => wert, ...]. - Rückgabe gemäß
returnsdes Manifests, sonst wird eineRoutineExceptiongeworfen.
Manifest
Ein Manifest liegt als <name>.json im Ordner seiner Art, etwa sql/Functions/close_inspection.json. Die SQL-Datei, die es nennt, liegt im selben Ordner.
Flach (eine Implementierung – ursprüngliche Spezifikation, weiterhin unterstützt):
{ "name": "close_inspection", "type": "nonquery", "file": "close_inspection.sql", "transaction": true, "parameters": { "id": "integer", "user": "string" }, "returns": "rows_affected" }
Multi-Backend (per-Treiber-Implementierungen):
{ "name": "close_inspection", "parameters": { "id": "integer", "user": "string" }, "returns": "rows_affected", "implementations": { "default": { "type": "sql", "file": "close_inspection.sql", "transaction": true }, "mysql": { "type": "procedure", "call": "close_inspection", "transaction": true }, "pgsql": { "type": "sql", "file": "close_inspection.pgsql.sql", "transaction": true } } }
| Feld | Werte |
|---|---|
name | [A-Za-z0-9_-]+; muss dem Dateinamen entsprechen. |
parameters | Map name → Typ: integer, string, boolean, float. Suffix [] = IN-Liste (Array → (?, ?, …)); Suffix ? = nullable. |
returns | rows_affected | insert_id | rows | row | scalar | none. |
type | sql (Datei-Statements) oder procedure (nativer Aufruf). Alt-Wörter nonquery/query → sql; proc/call/function → procedure. Die Ergebnisform steuert returns, nicht type. |
file | (sql) Pfad relativ zum Ordner der Art (Functions/ bzw. Procedures/), Endung .sql, kein ..; die Datei muss in diesem Ordner liegen. |
call | (procedure) [A-Za-z_][A-Za-z0-9_.]*. |
transaction | bool (Default true). |
Auflösung: der treiberspezifische Schlüssel gewinnt, sonst default.
SQL-Dateien
Eine type:sql-Datei darf mehrere Statements enthalten; die Routinen-Engine trennt
sie selbst (Regeln unten). Jeder
:name-Platzhalter wird in ein positionelles ? umgeschrieben und gebunden
(nie konkateniert) – einheitlich für alle Backends inkl. ODBC/Access. Ein Statement
bindet nur die tatsächlich vorkommenden Platzhalter (kein PDO-HY093).
Trennregeln: Getrennt wird an ; – aber nicht innerhalb von Zeichenketten
('…' mit ''-Escape), Kommentaren (-- auch hinter Code,
/* … */) und gequoteten Identifiern ("…", Backticks, […]). Ein
; innerhalb eines BEGIN … END- oder CASE … END-Blocks trennt ebenfalls nicht:
ein Trigger- oder Prozedurrumpf bleibt ein Statement. Ein nicht geschlossener
Blockkommentar bricht mit RoutineException ab, statt den Rest der Datei zu verschlucken.
Dynamisches SQL
- IN-Listen: Parameter als
integer[]/string[]deklarieren, Array übergeben →(?, ?, …). Leeres Array →(NULL). Im SQL-Text steht der Platzhalter ohne umschließende Klammern (WHERE id IN :ids, nichtIN (:ids)– sonst entsteht nach der Listen-Expansion das ungültigeIN ((?, ?))). - Dynamische Identifier / Inline-PHP: Eine Routinen-Datei darf PHP enthalten; im Scope stehen
$paramsund$ctx.$ctx->ident('spalte')liefert einen validierten, engine-gequoteten Identifier (einziger sicherer Weg für dynamische Tabellen-/Spaltennamen). Werte bleiben stets:name-Platzhalter.
Inline-PHP: Bedrohungsmodell
Inline-PHP läuft mit voller Anwendungsberechtigung (eval, wie bei wksnippet).
Daher mehrfach abgesichert:
- Benötigt sowohl die Plugin-Option
allowPhpals auch die globale Option$conf['phpok'](Doppel-Gate). Default: aus. Enthält eine Datei PHP bei geschlossenem Gate, schlägt die Ausführung fail-closed fehl (RoutineException). - Seit dem Berechtigungsmodell Stufe 2 (Details): selbst wenn beide Config-Gates offen sind, dürfen nur echte Superuser eine Routinen-SQL-Datei mit PHP-Inhalt speichern (
design-Fähigkeit allein reicht nicht) oder über die Studio-Oberfläche interaktiv auslösen (use-Fähigkeit allein reicht nicht) – unabhängig davon, was die Fähigkeiten-Matrix sonst erlaubt. Grund: ohne diese zusätzliche Bindung könnte ein Nicht-Superuser mitdesign-Freigabe PHP-Code hinterlegen, der später mit vollen Anwendungsrechten läuft (durch sich selbst, durch einen Superuser oder durch densql.routines-Servicepfad). Dersql.routines-Servicepfad selbst (programmatische Aufrufer wiewkblog) bleibt von dieser zusätzlichen Bindung unberührt – er vertraut der bereits gespeicherten Routinen-Datei, genau wie vor Stufe 2.
Routine-Sets aus einem anderen Plugin registrieren
// (a) per Event $controller->register_hook('WKSQLITEDS_REGISTER_ROUTINE_SETS', 'BEFORE', $this, 'sets'); public function sets(\dokuwiki\Extension\Event $event) { $event->data->register('reports', __DIR__ . '/sql'); } // (b) programmatisch plugin_load('helper', 'wksqliteds')->registerSet('reports', __DIR__ . '/sql');
Adressierung dann als reports:monthly. Vollständiger Methodenvertrag:
API-Referenz (inkl. des Legacy-Event-Shims aus der
Rename-Migration).
Konfiguration
| Option | Default | Bedeutung |
|---|---|---|
allowPhp | 0 | Inline-PHP in Routinen erlauben (zusätzlich globales phpok nötig; seit Stufe 2 zusätzlich strukturell an Superuser gebunden). |
default_connection | '''' | Verbindung, wenn execute() keine bekommt. |
slow_ms | 200 | Routinen protokollieren, die länger dauern (ms); 0 = aus. |
Sicherheit
- CSRF: jeder Admin-Schreibvorgang via
checkSecurityToken(). - Autorisierung: Ausführung über einen Web-Zugriff (Studio-Oberfläche) folgt der Berechtigungs-Matrix; kein direkter Web-Endpunkt führt eine Routine allein aus Request-Eingaben ohne Gate aus.
- Path Traversal (CWE-22): Namen allowlisted;
fileperrealpathim Set-Verzeichnis eingeschlossen, Endung.sql. - SQL-Injection (CWE-89): Werte immer gebunden; Identifier nur via
$ctx→ident(); SQL nie aus Werten zusammengesetzt. - Credentials: nie im Klartext im JSON; nicht in Fehlermeldungen – Details: Verbindungen.
- XSS (CWE-79): alle Admin-Ausgaben durch
hsc(). - Transaktionen:
transaction:trueatomar (begin/commit bzw.SAVEPOINTbei Schachtelung), Rollback bei Fehler.
Backend-Hinweise
| Backend | Treiber | Identifier | Savepoints | Prozeduraufruf | insert_id |
|---|---|---|---|---|---|
| SQLite | sqlite | „x“ | ja | – (nur SQL) | lastInsertId() |
| MySQL/MariaDB | mysql | `x` | ja | CALL p(…) | LAST_INSERT_ID() |
| PostgreSQL | pgsql | „x“ | ja | SELECT * FROM p(…) | lastval() – besser RETURNING + returns:scalar |
| SQL Server | sqlsrv | [x] | SAVE TRANSACTION | EXEC p … | SCOPE_IDENTITY() |
| MS Access | odbc | [x] | keine (an äußere TX, geloggt) | {CALL p(…)} | SELECT @@IDENTITY |
MS Access benötigt den ACE/Jet-ODBC-Treiber „Microsoft Access Driver (*.mdb, *.accdb)“
und pdo_odbc (Windows).
Häufige Fehler
Symptom: PDOException „HY093“ bei einem Multi-Statement-Manifest.
Ursache: ein Statement bindet einen Platzhalter, der in einem anderen Statement der Datei deklariert, aber in diesem konkreten Statement nicht verwendet wird.
Lösung: entfällt in der Praxis – der Executor bindet je Statement nur die dort tatsächlich vorkommenden Platzhalter. Tritt der Fehler dennoch auf, liegt meist ein Tippfehler im Platzhalternamen vor (Groß-/Kleinschreibung muss übereinstimmen).
Symptom: IN-Filter mit Listenparameter liefert ein SQL-Syntaxfehler.
Ursache: der Platzhalter steht mit eigenen Klammern im SQL-Text (IN (:ids)); nach
der automatischen Listen-Expansion entsteht das ungültige IN ((?, ?)).
Lösung: Klammern im SQL-Text weglassen – IN :ids (siehe Abschnitt „Dynamisches SQL“).
Verwandte Themen
- SQLite Data Studio: Verbindungen – welche Verbindung
execute()anspricht - SQLite Data Studio: Berechtigungen – Fähigkeiten-Gate für Studio-getriggerte Ausführung
- SQLite Data Studio: API-Referenz – vollständiger Helper-Methodenvertrag
- SQLite Data Studio: How-to – Rezept „Routine anlegen“