30.7 Other Procedural Languages: PL/Python, PL/V8 (JavaScript)
Now, let’s talk about the fun stuff. While PL/pgSQL is the native, battle-hardened workhorse for your stored procedures, PostgreSQL’s secret weapon is its ability to let you write functions in languages you probably already know. This isn’t some janky, half-baked integration; it’s first-class citizenship. You can escape the sometimes-verbose SQL paradigm and solve problems with the elegant power of a full-blown programming language, right inside the database. It feels a bit like smuggling a flamethrower into a knife fight. The two most popular contenders for this are PL/Python and PL/V8 (JavaScript).
The Superpower and the Footgun: untrusted vs trusted
Before you get too excited, you need to understand the single most important security concept: language trust. When you install a procedural language, you do it as a superuser. The key question is, do you install it as trusted or untrusted?
An untrusted language (the default for most, like plpythonu) is a superpower. It can do anything the PostgreSQL server process user can do. This means importing modules, writing files to the filesystem, making HTTP calls—the whole nine yards. It’s incredibly powerful and, consequently, incredibly dangerous. You are essentially handing a user with EXECUTE privileges on that function the keys to your server.
A trusted language (like plpython3u if built that way, or plv8) is sandboxed. It’s designed to be safe for non-superusers to use. It can’t import arbitrary modules or access the filesystem directly. This is what you want 99% of the time.
To install PL/Python3 (untrusted) and PL/V8 (trusted, usually), you’d do something like this as a superuser:
-- Typically installed as untrusted, hence 'u' at the end
CREATE EXTENSION plpython3u;
-- PL/V8 is typically installed as a trusted language
CREATE EXTENSION plv8;
The rule of thumb is simple: if a function doesn’t absolutely need to touch the outside world, write it in a trusted language. Reserve untrusted languages for superuser-only functions where that external access is the entire point.
PL/Python: Data Wrangling Made Ridiculously Easy
Python’s massive standard library and ecosystem of data tools (think json, datetime, collections, even pandas if you’re brave) make it perfect for complex data transformation that would be a nightmare in pure SQL.
Let’s say you need to parse and validate a complex JSON structure, something SQL’s native operators struggle with. PL/Python makes it trivial.
CREATE OR REPLACE function validate_user_profile(profile_json jsonb)
RETURNS boolean
AS $$
import json
try:
profile = json.loads(profile_json)
# Perform some complex validation logic
if not profile.get("email") or "@" not in profile["email"]:
return False
if profile.get("preferences", {}).get("theme") not in ["light", "dark", "system"]:
return False
# Check for a required nested key
return "consent" in profile.get("privacy_settings", {})
except json.JSONDecodeError:
return False
$$ LANGUAGE plpython3u;
Why this rules: You just used Python’s excellent json module and clear exception handling to perform validation that would require a labyrinth of jsonb_path_exists() and CASE statements in SQL. The pitfall? Remember, data type conversion between PostgreSQL and Python isn’t free. Passing huge datasets row-by-row to a PL/Python function will be slow. It’s best used for set-based operations or complex, row-specific calculations.
PL/V8 (JavaScript): The JSON Native
If your data lives and breathes JSON, PL/V8 might feel like home. Its integration is seamless because JSON is its native data structure. It’s also, as a trusted language, much safer for general use.
Here’s a classic use case: you want to deeply merge two JSONB objects, with custom logic for handling conflicts. SQL can’t do this elegantly.
CREATE OR REPLACE FUNCTION deep_merge_jsonb(old_json jsonb, new_json jsonb)
RETURNS jsonb
AS $$
// Simple recursive merge function
function merge(obj1, obj2) {
const result = {...obj1};
for (let key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (typeof obj2[key] === 'object' && obj2[key] !== null &&
typeof result[key] === 'object' && result[key] !== null) {
// Recursively merge objects
result[key] = merge(result[key], obj2[key]);
} else {
// Our rule: new values overwrite old ones
result[key] = obj2[key];
}
}
}
return result;
}
// Convert JSONB to JS objects, merge, then convert back to a string
// for the `jsonb` return type.
return JSON.stringify(merge(old_json, new_json));
$$ LANGUAGE plv8;
Why this rules: You’re leveraging JavaScript’s natural affinity for objects. The code is concise and expressive. The major pitfall here is the same as with any recursive function in a database: stack overflows on extremely deeply nested objects. You’d need to add a depth limit for production use. Also, note the JSON.stringify() at the end. PL/V8 automatically converts jsonb to a JavaScript object, but you must convert the result back to a string for PostgreSQL to understand it as jsonb.
The Golden Rule: Know When to Leave the Database
This is the most important insight I can give you. These languages are brilliant for data-centric logic. But the moment you catch yourself writing a function that spends more time, say, building an elaborate HTML email or performing complex arithmetic on a massive array, you have to ask: shouldn’t this be in the application layer?
The database is for data. Use PL/Python and PL/V8 to solve data problems that are awkward in SQL. Don’t use them to reinvent your entire application backend inside a trigger. That way lies madness, terrible performance, and a system that is impossible to debug. They are brilliant tools, not a new architecture. Wield them accordingly.