OriginChainDB docs
reference · oracle sql dialect

Oracle SQL dialect

OriginChainDB answers a documented subset of the Oracle SQL dialect — sequences, NVL, FROM DUAL, MINUS, CONNECT BY and more — so that SQL written for Oracle can run here largely unchanged. It is reachable through an ordinary PostgreSQL driver, over the same doors every other SQL client uses.

read this first · the dialect, not the transport

We implement the Oracle SQL dialect. We do not implement Oracle’s network protocol, and it is not planned — a decision, not a backlog item. There is no TNS listener, no Oracle Net negotiation and no OCI surface, so an Oracle client library cannot connect at all.

SQL*Plus, SQL Developer, Oracle Instant Client, OCI, ODP.NET, the Oracle JDBC driver and python-oracledb in thick mode all speak Oracle Net. None of them will reach an instance, and no connection setting changes that. What ports is your SQL: point a PostgreSQL driver at the database and keep the statements.

YOUR ORACLE-SHAPED APP Your SQL sequences · NVL · MINUS CONNECT BY · FROM DUAL Oracle Net (TNS) · OCI · SQL*Plus no TNS listener PostgreSQL driver :5432 TLS or HTTP /sql ORIGINCHAINDB INSTANCE One store SQL · vectors · graph
The dialect travels; the transport does not. Your statements arrive over a PostgreSQL driver or the HTTP /sql endpoint — never over Oracle Net.

How to connect.

There is no Oracle-specific endpoint and nothing to switch on for the dialect. You connect exactly as any other SQL client does, and the Oracle spellings are understood on arrival. Two doors, same behaviour:

  • The PostgreSQL wire protocol — port 5432, TLS required, SCRAM-SHA-256. Any PostgreSQL driver works: JDBC’s PostgreSQL driver, psycopg, node-postgres, pgx, libpq, the PostgreSQL ODBC driver. Setup and per-client walkthroughs are on Connect a SQL client.
  • The HTTP /sql endpoint — one bearer token, no driver at all. See the SQL reference.

The dialect work happens in the SQL translator, above the door, on the one seam both surfaces share — so a statement that is accepted over HTTP is accepted over the wire, and refused the same way.

PostgreSQL wire access is in limited preview

This paragraph is about the PostgreSQL wire protocol — the only wire this page’s Oracle dialect is served over. PostgreSQL-wire access is enabled gradually, and self-serve enablement covers single-node instances today. If your instance has no SQL access panel in the console yet, the PostgreSQL listener is not switched on for you — everything on this page still works over the HTTP /sql endpoint.

Do not read that across to the other wire protocols. Enablement of the MySQL- and SQL Server-compatible listeners is not self-serve today and is arranged with us; neither serves the Oracle dialect described here.

# A PostgreSQL client, talking Oracle SQL. Nothing Oracle-specific here.
psql "host=<your-database-host> port=5432 dbname=postgres \
      user=<user> password=<password> sslmode=require"

=> SELECT NVL(region, 'unknown') AS region FROM sales.orders MINUS
   SELECT NVL(region, 'unknown') FROM sales.returns;

What the three tiers mean.

The failure that matters in a dialect is not a refusal — it is a construct that is accepted and answered differently than Oracle would answer it, because you then get a wrong number with a 200 next to it. The engine is built to refuse loudly instead. So:

  • Generally available — shipped, and Oracle-equivalent within the scope named in the row.
  • Preview — shipped and reachable, but deliberately narrower than Oracle. The served shape is described; everything outside it is refused with a 400 that names the construct, never answered approximately.
  • Not supported — not accepted today. The rewrite that gets you the same answer is in the row.

A boundary in the “preview” column is a real boundary, not a caveat for form’s sake. Read the row before you port a statement that depends on it.

Generally available.

These behave as Oracle does within the scope described. Two dialect-wide differences apply throughout and are worth internalising once: a zero-length string is not NULL here (Oracle treats '' as NULL), and a chosen operand is returned with its own type rather than coerced to the first argument’s type — so keep the operands of one call in one type.

Construct What ships, and its scope
CREATE SEQUENCE
nextval() / currval()
CREATE SEQUENCE [IF NOT EXISTS] name [START WITH n] [INCREMENT BY n], then nextval('name') / currval('name') in an INSERT/UPDATE value position or as a bare FROM-less SELECT. The counter is durable before use — flushed to disk before the value is handed out — so a crash never re-issues an id, and a refused statement burns no value. Refused at CREATE, each by name: CYCLE, MINVALUE/MAXVALUE, OWNED BY, AS <datatype>, INCREMENT BY 0. Oracle’s CACHE/NOCACHE has no analogue: every value here is durable, which is the NOCACHE behaviour.
seq.NEXTVAL
seq.CURRVAL
The Oracle pseudocolumn spelling, any case, routing to the same durable counter as the function spelling. It inherits every restriction of that spelling deliberately — same permitted call sites, same read-only and replica gating. INSERT ... SELECT seq.NEXTVAL FROM t is therefore still refused: that is the table-scanning case, and per-row sequence generation is not what this surface does.
NVL(a, b) Yields a when it is not NULL, else b — the same result as COALESCE(a, b), which also ships. Wrong arity is refused at translate time rather than answered NULL, because a NULL is indistinguishable from a legitimate result. Carries the two dialect-wide differences above.
NVL2(a, b, c) Yields b when a is not NULL, else c; only the branch actually taken is evaluated. CASE WHEN a IS NOT NULL THEN b ELSE c END is the portable spelling and is also served.
MINUS Oracle’s MINUS and standard EXCEPT are the same operator, so the word is rewritten and the statement is then the EXCEPT the set-operation path has always served — it adds no narrowing of its own. The rewrite applies only outside string literals, quoted identifiers and comments, so SELECT 'a MINUS b' keeps its value and a column named "MINUS" keeps its name.
FROM DUAL The clause is recognised on a single un-joined, un-aliased factor named dual or sys.dual (any case) and stripped, so SELECT 1 FROM dual behaves exactly like the FROM-less spelling — constants, arithmetic, scalar functions, CASE, CAST, and WHERE / ORDER BY / LIMIT over the one synthetic row. The residual: recognition is textual, so a table genuinely named dual is reached only by shapes that read table content (a column reference, *, GROUP BY, a join, an alias). Avoid naming a table dual.

Preview.

Shipped and reachable, narrower than Oracle. Each row names the shape that is served; outside it the statement is refused rather than translated into a different question.

Construct Served shape — and where it stops
ROWNUM The row cap ports: WHERE ROWNUM <= n becomes a limit, a projected ROWNUM rn numbers the returned rows from 1, and the nested Oracle pagination idiom (... WHERE ROWNUM <= 20) WHERE rn > 10) returns the window it returns in Oracle. Oracle’s counter semantics are preserved rather than approximated: ROWNUM > 1 matches nothing, as in Oracle, and is never lowered to an offset. Refused (because ROWNUM is assigned before the sort and before grouping, so the obvious lowering would silently change the answer): a cap in the same block as ORDER BY, or alongside DISTINCT / GROUP BY / HAVING / an aggregate / a window function; a cap on a level that already has LIMIT/OFFSET/FETCH; ROWNUM under an OR, compared to a column, or used in GROUP BY / HAVING / ORDER BY / a join condition. For “top n by x” use ORDER BY x FETCH FIRST n ROWS ONLY.
CONNECT BY
START WITH
START WITH ... CONNECT BY [PRIOR] col = col over a single relation is rewritten to the equivalent recursive query and does the hierarchy walk. Refused by name, never executed with the clause dropped: LEVEL, SYS_CONNECT_BY_PATH, CONNECT_BY_ROOT, CONNECT_BY_ISLEAF, ORDER SIBLINGS BY, joins or multiple relations, and any relationship that is not a single PRIOR-anchored equality. Carry depth as a column of your own until LEVEL lands.
WITH RECURSIVE The standard-SQL replacement for CONNECT BY, executed to a fixed point. UNION ALL only, exactly one recursive CTE per statement, no CTE column list, no aggregate over the recursive relation in the outer projection. Two caps you will meet first: 100 iterations of depth and 1,000,000 accumulated rows; crossing either returns a 400 naming the cap. There is no CYCLE clause — carry the visited set as a column.
SUBSTR For positive offsets it agrees with Oracle exactly — SUBSTR('abcdef', 2, 3) is 'bcd'. A negative start offset is refused with a 400 naming the divergence rather than answered, because Oracle counts it from the end of the string and these are PostgreSQL semantics. For the last N characters write SUBSTR(s, LENGTH(s) - (N - 1), N).
INSTR The 2-argument form ships and is Oracle-equivalent: the 1-based position of the first occurrence, 0 when absent. The 3rd and 4th arguments (start position, occurrence number) are refused rather than evaluated with the extras ignored — a query using them needs restructuring, not a rename.
DECODE Ships with the Oracle semantic that trips hand-rewrites: DECODE compares NULL-equals-NULL, so a NULL search term matches a NULL input. (If you rewrite to CASE instead, remember WHEN x = NULL is never true — that arm must become WHEN x IS NULL.) Narrower in one way: there is no implicit conversion of search terms to the first term’s datatype, so DECODE(1, '1', 'y', 'n') is 'y' in Oracle and does not match that arm here. Nothing is silently converted or silently matched — the ambiguous case is refused. Keep the input, search terms and results in one type per call.
SYSDATE
SYSTIMESTAMP
Both spellings ship (SYSDATE and SYSDATE()), folded to an ISO-8601 value at translate time on the same path as NOW(), so one statement sees one value. The difference to carry across: the engine answers in UTC, where Oracle’s SYSDATE is the database server’s local time.
TO_DATE Constant arguments only — both the value and the format model must be quoted literals. Format models served: 'YYYY-MM-DD', 'DD-MON-YYYY', 'YYYY/MM/DD', 'MM/DD/YYYY'. Where it binds cleanly is the direct comparison, bare column on one side: WHERE d >= TO_DATE('2026-01-01', 'YYYY-MM-DD'). Express a date restriction as a range over the bare column rather than through Oracle’s TRUNC(d) = TO_DATE(...) idiom, which does not give the same answer here. TO_CHAR, TO_NUMBER and TO_TIMESTAMP do not ship — supply ISO-8601 text.
(+) outer join One shape: a single SELECT over exactly two comma-joined tables, every (+) marking the same table, each marked predicate a plain t1.col = t2.col(+). It is rewritten to a LEFT OUTER JOIN with the marked table as the optional side, and unmarked conjuncts move to the residual WHERE — which is Oracle-faithful, not a shortcut. Everything else is refused: three or more relations, a mix with explicit JOIN syntax, marks on both sides of one equality, a mark against a constant or inside a function, and WITH/GROUP BY/HAVING/DISTINCT alongside it. Refusing matters here because the natural fallback — drop the marks and join — is an INNER join, which silently returns fewer rows. The anti-join AND t2.id IS NULL is also refused, the same refusal the hand-written ANSI spelling gets.
MERGE INTO One narrow shape: MERGE INTO t USING (VALUES ...) AS s (c1, ...) ON t.pk = s.k WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED THEN INSERT .... It is lowered onto the same upsert plan as INSERT ... ON CONFLICT, inheriting its conflict-target rules and its access gate. Refused: a USING source that is a table or subquery, conditional WHEN ... AND, WHEN MATCHED THEN DELETE, BY SOURCE/BY TARGET, more than one clause of a kind, and an ON that is not a conjunction of equalities covering exactly the primary key or one unique index. A set-based MERGE driven by a query has to be restructured.

Not supported.

Not accepted today. Each row carries the rewrite that gets you the same answer, so you can plan the port rather than discover the gap at cutover.

Construct What to write instead
PIVOT / UNPIVOT Build the cross-tab with conditional aggregates — SUM(CASE WHEN k = 'a' THEN v END) AS a, ... with a GROUP BY — which is served over a single table and over a join. What is not expressible is the dynamic form, where the output columns come from the data: the engine plans a fixed projection, so those column names must be known when the statement is written.
ROWID Row identity here is the declared primary key, which every registered table has; row version for optimistic concurrency is the value the If-Match surface uses. Note that an Oracle ROWID is a physical address and is not stable across a reorganisation, so code that persisted one was already relying on something no primary key provides — that logic needs revisiting, not translating. (The row-cap uses of ROWNUM are a separate construct and do port — see Preview above.)
Flashback query AS OF TIMESTAMP, AS OF SCN and VERSIONS BETWEEN are not accepted, and no session setting makes a SELECT read an earlier state. Historical recovery here is an operator-driven point-in-time restore against a recovery point, which produces a restored database rather than answering a query mid-session — a different tool for a different job. See Operations.
PL/SQL proper Anonymous BEGIN blocks submitted as statements, packages, %ROWTYPE / %TYPE anchored declarations, BULK COLLECT / FORALL and autonomous transactions are not accepted. Stored procedures do ship in a different shape — see below.
Oracle Net (TNS) Not planned — a decision rather than a gap. The engine’s SQL wire protocol is PostgreSQL’s, so an Oracle-shaped application connects through a PostgreSQL driver and its SQL is what has to be portable, which is what the rest of this page is about. There is no TNS listener, no Oracle Net negotiation and no OCI surface. Do not read this row as work that is queued.

Stored procedures: the boundary.

Stored procedures ship in preview, in a PL/pgSQL-shaped subset. This is the part of a port most likely to be mis-scoped, so it is worth being blunt: this is not PL/SQL, and a package body will not move across. What travels is the control flow, the cursors and the exception structure — not the packaging.

What ships.

  • CREATE PROCEDURE name(p1 TYPE, ...) AS BEGIN ... END, invoked with CALL. A CALL outside a client transaction runs the whole body atomically: an uncaught failure discards every prior statement in the body.
  • Control flow: DECLARE, assignment, IF/ELSIF/ELSE, CASE, LOOP, WHILE, FOR over a range and FOR over a query, EXIT/CONTINUE, RETURN, RAISE, PERFORM, dynamic EXECUTE, and nested BEGIN ... END scopes.
  • SELECT ... INTO [STRICT], binding the first row’s columns positionally.
  • Cursors, including a client-returning fetch: DECLARE c CURSOR FOR / OPEN c FOR, then FETCH ... INTO to bind locals, or FETCH without INTO to deliver rows to the caller. Forward fetch only.
  • RETURN QUERY and RETURN NEXT to build a result set for the caller.
  • EXCEPTION WHEN ... THEN handlers on any block, with the block’s own writes rolled back before the handler runs.
the boundary, stated plainly
  • A client CALL cannot collect OUT parameters. Return values to the caller with RETURN QUERY or a client-returning FETCH instead. CALL arguments are literals.
  • No packages, no %ROWTYPE / %TYPE anchored declarations, no BULK COLLECT / FORALL, and no anonymous blocks submitted as a statement.
  • No COMMIT inside a body, and no autonomous transactions — the body commits as one unit at the end.
  • Catchable conditions are OTHERS, no_data_found, too_many_rows and integrity_constraint_violation. A finer SQLSTATE name is refused at CREATE rather than silently caught as OTHERS.
  • Loops are capped at 100,000 iterations, and a CALL expands to a bounded statement budget — a RETURN NEXT loop is not the way to return a large result set; RETURN QUERY is.
  • No loop labels, no FOREACH ... IN ARRAY, no dynamic cursors, and no backward fetch directions.

A CALL issued inside a client BEGINCOMMIT joins that transaction rather than opening its own, which forfeits the body’s all-or-nothing property: on a failure the body’s earlier statements stay in your transaction and a later COMMIT persists them. Roll back explicitly if that is not what you want.

Identifier case: the first thing that will bite you.

This is not a dialect function, it is a whole-schema property, and it breaks ports before any of the constructs above get a chance to. Oracle folds an unquoted identifier to UPPER case. This engine, like PostgreSQL, folds it to lower case.

The consequence is that a ported schema fails in a way that looks like a missing object. Oracle DDL that wrote CREATE TABLE EMP stored a table called EMP; if you replay the same DDL here it creates emp. Then a query that quotes "EMP" reports that no such table exists — and you go looking for a failed migration instead of a case rule.

per-tenant case resolution is in development

A per-tenant identifier case-resolution setting — which would let an instance resolve identifiers the way Oracle does — is in development and is not available on any instance today. Do not plan a migration around it. The workaround below is the supported answer for now, and it is a good habit regardless.

What to do today: pick one convention and never leave it.

  • Preferred — unquoted everywhere. Strip the double quotes from your DDL and from your queries. Every identifier folds to lower case consistently, and Oracle-style unquoted SQL keeps working because it folds too — just in the other direction, to the same place.
  • Or — quoted everywhere, in one case. If your DDL created "EMP", then every reference to it must also be "EMP", in every query, view and procedure body. This works, but it is unforgiving: one unquoted mention resolves to emp and fails.
  • Never mix. A schema with both "EMP" and emp is legal and is the worst outcome — two real tables, one typo apart.
-- Oracle DDL, replayed as-is. Creates a table named `emp` (folded down).
CREATE TABLE hr.EMP (id NUMBER, ename VARCHAR2(30));

-- Fails: there is no "EMP", because the quotes ask for it exactly.
SELECT * FROM hr."EMP";        -- error: table not found

-- Works: unquoted, folds to `emp`, matches what the DDL created.
SELECT * FROM hr.EMP;
SELECT * FROM hr.emp;          -- the same table

Worked examples.

Real Oracle statements, and what happens to them here. Each block runs unchanged over the HTTP /sql endpoint or through a PostgreSQL driver.

1. A sequence-backed insert

Both spellings work and share one durable counter, so you can port the Oracle spelling and migrate it later — or not at all.

CREATE SEQUENCE hr.emp_seq START WITH 1000 INCREMENT BY 1;

-- The Oracle pseudocolumn spelling: ships as-is.
INSERT INTO hr.emp (id, ename) VALUES (hr.emp_seq.NEXTVAL, 'KING');

-- The function spelling: the same counter, same durability.
INSERT INTO hr.emp (id, ename) VALUES (nextval('hr.emp_seq'), 'CLARK');

-- Read the value the session last took.
SELECT hr.emp_seq.CURRVAL FROM dual;

-- REFUSED - this is the table-scanning case, and it would mean
-- "one sequence value per scanned row", which this surface does not do:
--   INSERT INTO hr.emp (id, ename) SELECT hr.emp_seq.NEXTVAL, ename FROM staging.emp;

2. NULL handling and FROM DUAL

The one difference to remember: an empty string is not NULL here, so NVL('', 'x') is '' rather than Oracle’s 'x'.

SELECT NVL(commission, 0)                    AS commission,
       NVL2(manager_id, 'reports', 'top')    AS position,
       DECODE(dept, 10, 'ops', 20, 'sales', 'other') AS dept_name
FROM   hr.emp;

-- FROM DUAL is recognised and stripped:
SELECT SYSDATE FROM dual;            -- answers in UTC
SELECT 1 + 1 FROM sys.dual;          -- either spelling, any case

3. Row caps and pagination

The cap and the nested pagination idiom port. The combination that would silently change meaning is refused, and the refusal names both rewrites.

-- Works: a plain cap.
SELECT id FROM hr.emp WHERE ROWNUM <= 10;

-- Works: the classic nested pagination idiom, rows 11-20.
SELECT * FROM (
  SELECT a.*, ROWNUM rn FROM ( SELECT id, ename FROM hr.emp ORDER BY id ) a
  WHERE ROWNUM <= 20
) WHERE rn > 10;

-- Matches NOTHING - exactly as in Oracle, where the counter only
-- advances on a returned row. It is not an OFFSET.
SELECT id FROM hr.emp WHERE ROWNUM > 1;

-- REFUSED: ROWNUM is assigned BEFORE the sort, so this is
-- "10 arbitrary rows, then sorted" in Oracle - not the 10 smallest.
--   SELECT id FROM hr.emp WHERE ROWNUM <= 10 ORDER BY id;
-- Write the top-n you actually meant:
SELECT id FROM hr.emp ORDER BY id FETCH FIRST 10 ROWS ONLY;

4. Hierarchy and set difference

CONNECT BY does the walk; LEVEL does not ship yet, so carry depth yourself if you need it.

-- Walks the reporting tree from the top down.
SELECT id, ename
FROM   hr.emp
START WITH manager_id IS NULL
CONNECT BY PRIOR id = manager_id;

-- The portable spelling of the same walk, with depth carried as a column:
WITH RECURSIVE tree AS (
  SELECT id, ename, 1 AS depth FROM hr.emp WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.ename, t.depth + 1 FROM hr.emp e JOIN tree t ON e.manager_id = t.id
)
SELECT id, ename, depth FROM tree;

-- MINUS is a spelling of EXCEPT and inherits its behaviour exactly.
SELECT region FROM sales.orders
MINUS
SELECT region FROM sales.returns;

5. The (+) outer join

The two-table shape is rewritten to a LEFT OUTER JOIN. Anything wider is refused rather than turned into an inner join behind your back.

-- Served: two tables, all marks on one side, plain col = col.
SELECT e.ename, d.dname
FROM   hr.emp e, hr.dept d
WHERE  e.dept_id = d.id(+);

-- Which is exactly this, and you may prefer to write it directly:
SELECT e.ename, d.dname
FROM   hr.emp e LEFT OUTER JOIN hr.dept d ON e.dept_id = d.id;

Next.

Everything the base SQL surface serves — joins, aggregates, window functions, CTEs, constraints and transactions — is on the SQL reference, and it is what you fall back to wherever an Oracle spelling stops. To get a client connected, start at Connect a SQL client. If a statement you depend on is refused, the message names the construct — send it to us and it becomes a tracked gap rather than a surprise.

Oracle and Java are registered trademarks of Oracle Corporation and/or its affiliates. OriginChainDB is not affiliated with, endorsed by, or sponsored by Oracle Corporation. OriginChainDB implements a compatible subset of the Oracle SQL dialect so that existing SQL can be ported to it; it does not distribute, embed or resell Oracle software, and it does not implement Oracle’s network protocol.