plxtsql lets you write PostgreSQL functions with Transact-SQL syntax, the
procedural language of Microsoft SQL Server and Sybase. At CREATE FUNCTION
time plx transpiles the body to plpgsql and stores the plpgsql in
pg_proc.prosrc. The function then runs on the standard plpgsql interpreter.
Unlike plxplsql, T-SQL is not Ada-descended, so plxtsql is a restructuring front
end rather than a pass-through rewriter. It has its own tokenizer and parser and
emits plpgsql directly. It handles the parts of T-SQL that differ structurally
from plpgsql: @-prefixed variables, DECLARE anywhere in the body, IF/WHILE
bodies written as BEGIN ... END instead of THEN ... END IF, and
TRY/CATCH. This makes plxtsql a practical path for moving SQL Server stored
logic into PostgreSQL.
CREATE EXTENSION plx;The body is a sequence of T-SQL statements. Local variables are declared with
DECLARE @name type and referenced with the @ sigil; plx hoists every
declaration into a plpgsql DECLARE block and drops the sigil in the generated
code.
CREATE FUNCTION grade(score int) RETURNS text LANGUAGE plxtsql AS $$
DECLARE @g varchar(10);
IF @score >= 90
SET @g = 'A';
ELSE IF @score >= 80
SET @g = 'B';
ELSE
SET @g = 'F';
RETURN @g;
$$;An outer BEGIN ... END wrapping the whole body is optional; if present it is
unwrapped (the declarations inside still hoist).
The body is T-SQL, but the function signature (the parameter and return
types in CREATE FUNCTION) is parsed by PostgreSQL before plx sees the body, so
it must use PostgreSQL type names: write RETURNS int, not RETURNS INT inside
a T-SQL CREATE PROCEDURE. Parameters are referenced in the body with the @
sigil (@score above binds to the parameter score). Inside the body, T-SQL
type names in DECLARE are translated (see below).
| T-SQL | plpgsql |
|---|---|
DECLARE @x int |
hoisted to DECLARE x integer; |
DECLARE @x int = 5 |
x integer; plus x := 5; at that point |
DECLARE @a int, @b int |
both hoisted |
SET @x = e |
x := e; |
SET @x += e |
x := x + (e); (also -=, *=, /=, %=) |
SELECT @x = e (no FROM) |
x := e; |
SELECT @x = a, @y = b |
x := a; y := b; |
SELECT @x = col FROM t ... |
SELECT col INTO x FROM t ...; |
| T-SQL | plpgsql |
|---|---|
IF cond stmt |
IF cond THEN stmt END IF; |
IF cond BEGIN ... END |
IF cond THEN ... END IF; |
IF cond ... ELSE ... |
IF cond THEN ... ELSE ... END IF; |
ELSE IF ... |
nested ELSE IF ... END IF; |
WHILE cond BEGIN ... END |
WHILE cond LOOP ... END LOOP; |
BREAK |
EXIT; |
CONTINUE |
CONTINUE; |
RETURN e |
RETURN e; |
The condition after IF/WHILE runs up to the start of the body (a statement
keyword or BEGIN); there is no THEN keyword in T-SQL.
| T-SQL | plpgsql |
|---|---|
PRINT e |
RAISE NOTICE '%', e; |
RAISERROR('msg', sev, state) |
RAISE EXCEPTION '%', 'msg'; |
THROW n, 'msg', s |
RAISE EXCEPTION '%', 'msg'; |
THROW; (bare, in CATCH) |
RAISE; (re-raise) |
BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH |
BEGIN ... EXCEPTION WHEN OTHERS THEN ... END; |
ERROR_MESSAGE() |
SQLERRM |
RAISERROR's format arguments (its %d/%s printf-style substitutions) are not
applied; the message argument is emitted as-is.
| T-SQL | PostgreSQL |
|---|---|
INT, INTEGER |
integer |
BIGINT, SMALLINT, TINYINT |
bigint, smallint, smallint |
BIT |
boolean |
DECIMAL(p,s), NUMERIC, MONEY |
numeric(p,s), numeric, numeric(19,4) |
FLOAT, REAL |
double precision, real |
VARCHAR(n), NVARCHAR(n) |
varchar(n) |
VARCHAR(MAX), NVARCHAR(MAX) |
text |
CHAR(n), NCHAR(n) |
char(n) |
TEXT, NTEXT |
text |
DATE, TIME |
date, time |
DATETIME, DATETIME2, SMALLDATETIME |
timestamp |
DATETIMEOFFSET |
timestamptz |
UNIQUEIDENTIFIER |
uuid |
VARBINARY(n), VARBINARY(MAX) |
bytea |
An unrecognized type name is passed through, so PostgreSQL type names also work.
| T-SQL | PostgreSQL |
|---|---|
ISNULL(a, b) |
coalesce(a, b) |
IIF(c, a, b) |
CASE WHEN c THEN a ELSE b END |
CONVERT(type, e) |
CAST(e AS type) (the style argument is ignored) |
LEN(x) |
length(x) |
DATALENGTH(x) |
octet_length(x) |
CHARINDEX(sub, str) |
strpos(str, sub) (arguments swapped) |
GETDATE(), SYSDATETIME() |
now() |
NEWID() |
gen_random_uuid() |
CEILING(x) |
ceil(x) |
@@IDENTITY |
lastval() |
CAST(e AS type) and most standard functions (COALESCE, SUBSTRING, UPPER,
LOWER, ABS, ROUND, ...) are already valid in PostgreSQL and pass through.
EXEC('<sql>') (equivalently EXECUTE('<sql>')) becomes EXECUTE '<sql>';.
CREATE FUNCTION run(tbl text) RETURNS void LANGUAGE plxtsql AS $$
EXEC('TRUNCATE ' || @tbl);
$$;A bare SELECT (not an assignment) in a function declared RETURNS TABLE(...)
or RETURNS SETOF ... becomes RETURN QUERY SELECT ....
CREATE FUNCTION series(n int) RETURNS TABLE(k int) LANGUAGE plxtsql AS $$
SELECT g FROM generate_series(1, @n) AS g;
$$;In a scalar function a bare SELECT becomes PERFORM (evaluated for effect).
End statements with ;. T-SQL treats ; as optional in many places, but plx
uses it (together with block boundaries such as END, ELSE, and END TRY) to
find statement boundaries, so a missing ; between two statements can cause them
to be read as one. Control-flow blocks (IF, WHILE, BEGIN ... END,
TRY/CATCH) are self-delimiting and do not require a trailing ;.
A function that returns trigger can back a trigger. Read the row through NEW
(and OLD on updates and deletes), and assign to a field with SET NEW.col = e,
which becomes NEW.col := e. Return NEW.
CREATE FUNCTION stamp() RETURNS trigger LANGUAGE plxtsql AS $$
SET NEW.total = NEW.qty * NEW.price;
SET NEW.tag = 'row ' || CONVERT(varchar, NEW.id);
RETURN NEW;
$$;Statements such as SET NOCOUNT ON and SET XACT_ABORT ON are recognized and
ignored (they configure the SQL Server session and have no PostgreSQL
equivalent). SET @variable = ... and SET NEW.col = ... are always
assignments; a SET with no qualified target and no assignment is treated as a
session option.
@@ROWCOUNT,@@ERROR, and other@@globals (except@@IDENTITY). UseGET DIAGNOSTICSor an exception handler in a construct plx does map, or write that part in another dialect.- Table variables (
DECLARE @t TABLE (...)) and T-SQL cursors (DECLARE c CURSOR). GOTO,WAITFOR, and label targets.- Transaction control (
BEGIN TRAN,COMMIT,ROLLBACK): a plpgsql function runs inside the caller's transaction and cannot manage transactions. - Calling a stored procedure by name (
EXEC procname); onlyEXEC('<sql>')dynamic SQL is supported. - String concatenation with
+. T-SQL overloads+for both numeric addition and string concatenation; plx cannot tell which is meant without type information, so it leaves+as+. Use||orCONCAT(...)for string concatenation.
See PARITY.md for the full per-dialect feature matrix and USERGUIDE.md for cross-dialect examples.