Convert Sybase T-SQL to PostgreSQL PL/pgSQL with a mapping table and the six ASE behaviors that change results
9 min read Migration The Adapters team
Field mapping auto-plugged · tap a port to rewire
Plug a source port into
Transform on this cable
JSON in
JSON out
5 sample records ready
To convert Sybase T-SQL to PostgreSQL, run the schema and procedures through AWS SCT, DMS Schema Conversion, SQLines or Ispirer, then review six ASE behaviors by hand, because they translate into PL/pgSQL that compiles and returns a different answer. Most of the work is mechanical: @@rowcount becomes GET DIAGNOSTICS, raiserror becomes RAISE EXCEPTION, *= becomes LEFT OUTER JOIN. The six that are not mechanical are below, with the rewrite for each.
Key takeaways
- Converters exist and work. AWS SCT reads ASE 12.5.4 to 16.0, and DMS Schema Conversion has handled ASE 16 to PostgreSQL with generative AI since November 2025.
- pgloader is not one of them. Its documentation lists no Sybase source.
- ASE stored empty strings as a space for years. SAP documents it, and PostgreSQL will not, so tables end up with two kinds of empty.
- Transactions change shape. ASE commits statement by statement by default. A PL/pgSQL function commits all or nothing.
- Procedures stop returning rows. Anything the app reads as a result set has to become a function.
SAP ASE T-SQL
create proc close_batch @batch int as set rowcount 1000 delete from staging where batch_id = @batch if @@error != 0 raiserror 20001 'delete failed' select @@rowcount set rowcount 0
POSTGRESQL PL/pgSQL
CREATE FUNCTION close_batch(p_batch int)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE n bigint;
BEGIN
DELETE FROM staging WHERE ctid IN (
SELECT ctid FROM staging
WHERE batch_id = p_batch LIMIT 1000);
GET DIAGNOSTICS n = ROW_COUNT;
RETURN n;
END $$;
How do I convert Sybase T-SQL to PostgreSQL?
Inventory first, convert second, review third. Pull the procedure, trigger and view source from syscomments, count objects by type, and run the whole set through a converter that writes an assessment. AWS SCT lists SAP ASE 12.5.4, 15.0.2, 15.5, 15.7 and 16.0 as sources and converts object, variable and parameter names to lowercase by default. DMS Schema Conversion, in the AWS console, added SAP ASE to RDS and Aurora PostgreSQL on 20 November 2025 and uses generative AI on procedures, functions and triggers. Ispirer and SQLines sell converters that do the same outside AWS.
A converter gets the syntax right. What it cannot know is what your application relied on. The mapping table below is the mechanical part, and the six behaviors after it are the part that needs a person who knows the code. If the data side of the project is still open, every tool for the schema and the load is compared on our Sybase to PostgreSQL migration tools page.
Sybase ASE to PostgreSQL T-SQL mapping table
Eighteen constructs that appear in almost every ASE code base, with the PL/pgSQL each one becomes and the note a reviewer should check.
| SAP ASE | PostgreSQL | Check |
|---|---|---|
| CREATE PROCEDURE p @id int AS | CREATE PROCEDURE p(p_id int) LANGUAGE plpgsql AS $$ ... $$ | A FUNCTION if it returns rows |
| @@rowcount | GET DIAGNOSTICS n = ROW_COUNT, or FOUND | Read it right after the statement |
| @@error after each statement | BEGIN ... EXCEPTION WHEN ... END | Error checks become a block |
| @@identity | INSERT ... RETURNING id | Safer than lastval() |
| raiserror 20001 'msg' | RAISE EXCEPTION 'msg' USING ERRCODE = ... | Callers match on SQLSTATE now |
| print 'msg' | RAISE NOTICE 'msg' | Clients must read notices |
| select top 10 ... | SELECT ... LIMIT 10 | Add ORDER BY if order mattered |
| set rowcount 1000 | LIMIT on SELECT, a key subquery on DELETE | PostgreSQL DELETE has no LIMIT |
| select ... into #work | CREATE TEMP TABLE work ON COMMIT DROP AS ... | Lifetime differs, see below |
| a.id *= b.id | a LEFT OUTER JOIN b ON a.id = b.id | Filters move into ON |
| getdate() | clock_timestamp() or now() | now() is the transaction start |
| isnull(a, b) | COALESCE(a, b) | Same result for two arguments |
| convert(varchar, d, 101) | to_char(d, 'MM/DD/YYYY') | One format code per style number |
| datediff(day, a, b) | b::date - a::date | Counts boundaries, as ASE does |
| dateadd(dd, n, d) | d + n * interval '1 day' | make_interval() also works |
| charindex(s, str) | strpos(str, s) | Argument order flips |
| datalength(s) | octet_length(s) | Bytes, not characters |
| exec p @id = 5 | CALL p(5), or SELECT * FROM f(5) | Depends on what p returns |
Six ASE behaviors that convert cleanly and return a different answer
Each of these produces valid PL/pgSQL. None raises an error in testing unless the test data happens to hit it. They are listed in the order they tend to surface after go-live.
| Behavior | On SAP ASE | On PostgreSQL | Rewrite |
|---|---|---|---|
| Blank strings | '' is stored as one space in varchar | '' is stored as empty | Pick one meaning and apply it to old and new rows |
| getdate() in a loop | Returns the current time on every call | now() returns the transaction start every call | clock_timestamp() where the time must move |
| Unchained mode | Each statement commits on its own by default | A function runs as one unit and rolls back whole | Decide where the commits belong, then use a procedure |
| set rowcount on DELETE | Caps deletes, updates and selects in the session | No LIMIT on DELETE, so the batch cap vanishes | DELETE ... WHERE key IN (SELECT ... LIMIT n) |
| #temp tables | Dropped when the procedure ends | Live until the session ends | ON COMMIT DROP, or IF NOT EXISTS plus TRUNCATE |
| Result sets | Any SELECT in a procedure returns rows | Procedures return no rows | A function with RETURNS TABLE, and new call sites |
Blank strings. SAP's manual says an empty string "is interpreted as a single blank
in insert or assignment statements on varchar data". Every row your ASE application wrote with ''
holds a space. Move it as is and the first PostgreSQL insert stores a true empty string, so
WHERE middle_name = '' finds only new rows. Decide once: trim the old blanks during the
load, or keep them and make new code write a space. Then search the procedures for
= '' and = ' ', because both forms will be in there.
getdate() in a loop. PostgreSQL documents that now() returns the start time of the current transaction, and a PL/pgSQL function runs inside one. A converted procedure that stamps each row with getdate() while it loops writes the same timestamp on every row. Audit trails and elapsed-time logic break first. Use clock_timestamp() wherever the value must move.
Unchained mode. ASE's default is unchained, or Transact-SQL, mode, where a statement outside an explicit transaction commits by itself. A batch procedure that fails at row 900 has already kept rows 1 to 899. The same logic as a PL/pgSQL function rolls all 900 back. Sometimes that is an improvement, and sometimes a retry job now redoes work it used to skip. Where the commits matter, write a PostgreSQL procedure and COMMIT inside it on purpose.
set rowcount on DELETE. ASE teams use set rowcount to delete in
batches and keep the log small. PostgreSQL's DELETE has no LIMIT clause, so a converter that drops
the setting turns a 1,000-row batch into one delete of everything. The rewrite in the figure above
selects the keys with LIMIT and deletes those.
#temp tables. In ASE a temporary table created inside a procedure disappears when the procedure ends. In PostgreSQL it lives until the session ends, so the second call on a pooled connection fails with "relation already exists". Create it with ON COMMIT DROP, or with IF NOT EXISTS followed by TRUNCATE.
Result sets. Any SELECT inside an ASE procedure returns rows to the caller. A PostgreSQL procedure returns none. Procedures the application reads from have to become functions with RETURNS TABLE, and every call site changes from exec to SELECT. Count these early, because the application team owns that work, not the DBA.
What is the PostgreSQL equivalent of @@rowcount?
GET DIAGNOSTICS n = ROW_COUNT; inside PL/pgSQL, placed directly after the statement you
want to measure. For a yes or no check, the built-in FOUND variable is shorter and reads the same
way. AWS's own ASE migration guidance notes that PostgreSQL has no @@rowcount function, so every
reference has to be rewritten, and the read has to move next to its statement.
How do I convert raiserror to PostgreSQL?
Use RAISE EXCEPTION 'message' USING ERRCODE = '...'. The message carries over, but the
ASE error number does not, so any caller that branches on 20001 or 20002 must branch on a SQLSTATE
instead. Pick a small set of custom codes, document them, and change the application's error
handling in the same release. Informational print statements become RAISE NOTICE.
Can AWS SCT convert Sybase stored procedures?
Yes. AWS SCT converts SAP ASE procedures, functions and triggers to PL/pgSQL for PostgreSQL and Aurora PostgreSQL, and flags what it cannot convert in an assessment report. DMS Schema Conversion does the same for ASE 16 in the AWS console with generative AI. Neither knows your application, so the six behaviors above still need a reviewer.
How do I verify a Sybase to PostgreSQL conversion?
Run the same inputs through both and compare outputs, not just row counts. For each converted procedure, capture a set of real calls on ASE, replay them on PostgreSQL against the same data, and diff the returned rows and the rows changed. Compare money columns to four decimal places, since a MONEY column mapped to PostgreSQL's MONEY keeps two under a US locale. During the cutover window, point uptime monitoring at the application's health endpoints so a failing procedure shows up as an outage alert rather than a support ticket.
If ASE stays live for months while the code is rewritten, PostgreSQL has to be kept current and checked against it the whole time. That is a data sync, not a code conversion, and it is what our plans do: declare the columns once with NUMERIC(19,4) for money, then sync incrementally with per-record logs. The closest relative of this guide, for Microsoft's dialect, is converting SQL Server T-SQL to PostgreSQL, and the capture options for a long cutover are compared in change data capture tools.
Keep PostgreSQL current from Sybase ASE while the code is rewritten
Declare the columns once with MONEY kept to four places and BIT as BOOLEAN NOT NULL, run the backfill, then sync incrementally with retries, alerts and per-record logs. From $49 a month, with no per-row overage fees.
The live demo needs no card, and Starter is $49 a month.