Postgres Protocol Plugin

ArcadeDB Server supports a subset of the Postgres wire protocol, such as connection and queries.

If you’re using ArcadeDB as embedded, please add the dependency to the arcadedb-postgresw library. If you’re using Maven include this dependency in your pom.xml file.

<dependency>
    <groupId>com.arcadedb</groupId>
    <artifactId>arcadedb-postgresw</artifactId>
    <version>26.10.2</version>
</dependency>

To start the Postgres plugin, enlist it in the server.plugins settings. To specify multiple plugins, use the comma , as separator. Example:

~/arcadedb $ bin/server.sh -Darcadedb.server.plugins="Postgres:com.arcadedb.postgres.PostgresProtocolPlugin"

If you’re using MS Windows OS, replace server.sh with server.bat.

In case of an incompatibility, restart the server with the additional option -Darcadedb.postgres.debug=true, repeat the connection attempt, and add the debug output to the issue report.

In case you’re running ArcadeDB with Docker, use -e to pass settings and open the Postgres default port 5432:

docker run --rm -p 2480:2480 -p 5432:5432 \
       --env ARCADEDB_SETTINGS="-Darcadedb.server.rootPassword=playwithdata \
          -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin " \
          arcadedata/arcadedb:latest

The Server output will contain this line:

2026-10-29 05:19:27.030 INFO  [PostgresNetworkListener] <ArcadeDB_0> Listening for incoming connections on /0.0.0.0:5432 (protocol v.-1, TLS: DISABLED)
2026-10-29 05:19:27.031 INFO  [PluginManager] <ArcadeDB_0> - PostgresProtocolPlugin plugin started

Once you have enabled the Postgres Protocol, you can interact with ArcadeDB server by using any Postgres drivers. The driver sends the queries to the ArcadeDB server without parsing or checking the syntax. For this reason, even if ArcadeDB SQL is different from Postgres SQL, you’re still able to execute any ArcadeDB SQL command through the Postgres driver. Check out the following list with the official drivers for the most popular programming languages:

For the complete list, please check Postgres website.

Other query languages

By default the Postgres driver interprets all the commands as SQL. To use another supported language, like Cypher, Gremlin, GraphQL or MongoDB, prefix the command with the language to use between curly brackets.

Example to execute a query by using GraphQL:

{graphql}{ bookById(id: "book-1"){ id name authors { firstName, lastName } }

Example to use Cypher:

{cypher}MATCH (m:Movie)<-[a:ACTED_IN]-(p:Person) WHERE id(m) = '#1:0' RETURN *

Example of using Gremlin:

{gremlin}g.V()

TLS/SSL

The plugin supports TLS through the Postgres protocol’s own handshake, so psql, JDBC and BI tools can connect with sslmode=require. Set arcadedb.postgres.ssl to one of:

  • DISABLED (default): no TLS. A client asking for TLS is told to continue in plain text.

  • OPTIONAL: the client chooses TLS or plain text. A client that does not ask for TLS sends its password in the clear.

  • REQUIRED: plain text connections are refused before any password is requested. Recommended once certificates are in place.

TLS uses the same key store and trust store settings as the HTTP server (arcadedb.ssl.keyStore, arcadedb.ssl.keyStorePassword, arcadedb.ssl.trustStore, arcadedb.ssl.trustStorePassword), and all four must be set. The server does not ask clients for a certificate.

bin/server.sh -Darcadedb.server.plugins="Postgres:com.arcadedb.postgres.PostgresProtocolPlugin" \
  -Darcadedb.postgres.ssl=REQUIRED \
  -Darcadedb.ssl.keyStore=/path/keystore.pkcs12 -Darcadedb.ssl.keyStorePassword=secret \
  -Darcadedb.ssl.trustStore=/path/truststore.jks -Darcadedb.ssl.trustStorePassword=secret
psql "host=localhost port=5432 dbname=mydb user=root sslmode=require"
PostgreSQL 17 "direct" TLS (sslnegotiation=direct) is not supported.

(Since v26.10.2.) The user a connection authenticated as is checked again before every message: a user deleted, given a new password or stripped of the grant on the connected database is disconnected with a FATAL error, SQLSTATE 28P01. See User changes on open connections.

Current limitations

The documentation about Postgres wire protocol is not exhaustive to build a bullet proof protocol. In particular the state machine. For this reason this plugin was created by reading the available documentation online (official and not official) and looking into Postgres drivers or implementations.

Both the "simple" query protocol and the extended query protocol (Parse/Bind/Describe/Execute/ Sync, what PreparedStatement uses) are supported.

Since v26.10.1, BEGIN, COMMIT and ROLLBACK sent through the extended query protocol are acknowledged the way PostgreSQL acknowledges them. Previously a libpq-based client - psycopg3, asyncpg, the Apache Arrow ADBC PostgreSQL driver - could fail to open a transaction with Failed to begin transaction. In the same release, a statement the server rejects no longer leaves the previously prepared statement usable under the same name, so a client can no longer receive the earlier statement’s rows in answer to one that was refused.

Since v26.10.1, a portal (the bound statement Execute runs) lives only as long as the transaction it was created in, as in PostgreSQL: in autocommit it ends at Sync, inside BEGIN it ends at COMMIT/ROLLBACK. Fetching a large result in chunks (JDBC setFetchSize()) therefore needs a transaction, which is what the PostgreSQL JDBC driver already requires. Binding from a prepared statement that does not exist is refused with SQLSTATE 26000, and executing or describing a portal that does not exist with 34000, instead of answering with a success message.

Since v26.11.1, a SELECT whose columns are known before its first row is read is fetched from a live cursor: each Execute (each chunk of a JDBC setFetchSize() fetch) reads only the rows it asks for, instead of the server reading the whole result before it sends the first row, and the query is not subject to arcadedb.postgres.queryMaxRows. The columns are known when the query projects only properties its type declares (SELECT name, age FROM Person), of a scalar type, BINARY, MAP or a LIST of a scalar type, or when its prepared statement was already described to the client (Describe on the statement, or a statement the PostgreSQL JDBC driver re-executes after prepareThreshold). Any other query - SELECT FROM Person, SELECT *, an expression, a property the type does not declare, another query language - can only be described from its rows, so it is still read whole first, within that limit. To fetch a large type in chunks, list the declared properties you need.

A live cursor is a running statement until it is drained or its portal ends: it is listed by list queries and SHOW TRANSACTIONS, and a cancel request or terminate query stops it, also while the client is between two fetches. Its arcadedb.command.timeout (or the session’s statement_timeout) runs from the moment the query starts to its last fetch, including the time the client takes between two fetches, where PostgreSQL restarts statement_timeout for each Execute. An idle cursor does not hold a slot of arcadedb.queryMaxConcurrent: each fetch is admitted on its own. A write the same transaction makes between two fetches can be seen by the rows not fetched yet, unless the query sorts (ORDER BY reads every row on the first fetch).

Since v26.10.1, running a prepared statement without asking for its row description again (what the PostgreSQL JDBC driver does once a statement has run prepareThreshold times, 5 by default) no longer sends an unrequested row description. Before, the JDBC connection failed with NoSuchElementException from the 6th execution on.

Since v26.10.1, when a prepared statement is described (Describe on the statement, what asyncpg, npgsql and pgx do) and the server answers that it returns no rows, running it keeps that promise. An INSERT, UPDATE, DELETE, CREATE VERTEX or CREATE EDGE without RETURN answers only its command tag with the number of affected records (e.g. INSERT 0 1), like PostgreSQL. A statement whose columns cannot be known before it runs (e.g. a {cypher} query) and that then returns rows is refused with SQLSTATE 0A000: describe the portal instead (what the PostgreSQL JDBC driver, libpq and psycopg do), which runs the statement first and reports its real columns.

System catalog and schema introspection

Since v26.9.1, queries against pg_catalog and information_schema - the ones a client’s driver sends to discover schemas, tables, columns and types, typically via DatabaseMetaData.getSchemas()/getTables()/getColumns()/getTypeInfo() - are answered by recognizing the shape of the query (which relations it names, which columns it projects, including a client’s own CASE expressions) rather than by matching one tool’s exact spelling. This means the same question is answered the same way whichever Postgres client sends it, rather than only for a short list of tools this plugin happened to be tested against. A catalog query in a shape the server does not recognize, or over a catalog relation it does not model (pg_proc, pg_trigger, pg_extension, information_schema.routines and so on), fails with SQLSTATE 0A000 (feature_not_supported) whose message names the relation, since v26.11.1. Before, it returned an empty result set, which a client reads as an empty catalog: an ungrouped SELECT count(*) FROM pg_proc WHERE …​ came back with no row at all where PostgreSQL always sends one, and psql’s `\di, psqlodbc’s SQLPrimaryKeys and SQLStatistics reported nothing. A refusal a client can see is recoverable; a wrong empty answer is not. The connection stays usable after the error. One kind of statement keeps the empty result, on purpose: a statement that names pg_catalog but reads no catalog relation, such as SELECT pg_catalog.set_config(…​), the session probes pg_dump sends.

Since v26.11.1, the type lookups a driver sends to name a type it has not met are answered like any other catalog query, from the types this protocol can produce, with the values the driver bound: the OID of a type name, the array type of an element type, the element of an array type, the name of an OID and whether its schema is on the search path. A name or an OID this server does not have is an empty answer, which is what those drivers are built to handle; one it has is no longer reported as missing. The evaluator behind them reads the constructs those lookups are written with, so a differently spelled lookup, or another client’s, is answered too: a table function in FROM (generate_series(start, stop [, step]) and unnest(array)), USING (column), = ANY(array) and <> ALL(array), a derived table joined to other relations, and one relation read through two aliases (pg_type t JOIN pg_type arr ON arr.oid = t.typarray). A series is charged to the work budget of the statement before it is built, so a lookup cannot ask for a billion rows. pg_range and pg_enum are modelled, and have no row.

Every type is a table in one schema, named after the connected database - matching SELECT current_schema() - because a PostgreSQL connection is bound to one database and sees the schemas inside it, and in ArcadeDB a connection is likewise bound to one database whose types are its tables. Since v26.11.1, pg_namespace (and so DatabaseMetaData.getSchemas()) also lists pg_catalog, the schema every built-in type belongs to, as PostgreSQL does: a schema browser may show it, empty.

Since v26.11.1, a materialized view and a continuous aggregate have columns in the catalog: the ones their query returns, in that order and under their aliases, so \d <view> prints them.

Since v26.11.1, each materialized view and continuous aggregate also has its _RETURN rule in pg_rewrite, as in PostgreSQL, and pg_class.relhasrules is true for it. pg_depend links that rule to its view and to every type the view reads (including types read only in a subquery), so a tool asking what is built on a table, for example before dropping it, finds the views that depend on it. Dependencies are listed per type, not per column. pg_get_ruledef(oid) prints the rule.

Since v26.10.1, pg_type can also be queried on its own, and not only as part of a column list. Some clients build a map of every type the server has before they run a single query, and refuse to connect if that map comes back empty - the Apache Arrow ADBC PostgreSQL driver is one of them. Such a query is answered with the types this protocol can actually produce (see Column type mapping below), each described the way PostgreSQL describes it. A filter the server cannot interpret returns no rows rather than every type, so a client asking whether an extension type such as hstore exists is correctly told it does not.

Since v26.10.1, the regclass and regtype casts resolve, in both directions: 'mytable'::regclass is that type’s OID and attrelid::regclass is its name, and likewise 'int4'::regtype and atttypid::regtype for types. The function spellings to_regclass() and to_regtype() do the same. Some drivers ask for a table’s columns by writing the name as a regclass cast and comparing it against pg_class.oid - the Apache Arrow ADBC PostgreSQL driver does this in GetTableSchema - and before this the cast was ignored, so the comparison silently matched nothing and the client saw a table with no columns. A name no type carries resolves to NULL, as to_regclass() does in PostgreSQL, so asking about a table that does not exist returns nothing rather than describing every table.

Type names are matched case-sensitively first, as ArcadeDB stores them, and only then ignoring case - so 'MyType'::regclass finds the type spelled that way even though PostgreSQL would have folded the unquoted name to lower case. If two types differ only in case, an unquoted name that matches neither of them exactly resolves to nothing rather than picking one; quote the name to say which you mean.

Since v26.10.1, a query that joins a catalog relation to pg_roles or pg_user to name an owner - what a GUI browser sends to list tables with their owner - answers with the connected user rather than NULL:

SELECT c.relname, r.rolname AS owner FROM pg_class c JOIN pg_roles r ON r.oid = c.relowner

Only the connected user is described as a role; an emulated catalog does not hand out the server’s account list. A query about roles, databases, privileges, character sets or collations that also mentions pg_namespace is now answered as the question it asks, instead of being answered as a schema query.

Since v26.11.1, psql’s `\dt, \d <table> and \d+ <table> list and describe the database’s types, and the PostgreSQL ODBC driver (psqlodbc) answers SQLColumns and SQLTables(SQL_ALL_TABLE_TYPES). Before, \dt printed Did not find any relations, SQLColumns returned no columns and SQLTables(SQL_ALL_TABLE_TYPES) failed with a syntax error. The catalog queries these clients send are now read as PostgreSQL reads them:

  • a parenthesised join in FROM - FROM (pg_class c JOIN pg_namespace n ON …​) JOIN pg_attribute a ON …​ - and a join to pg_am (every table reports the heap access method);

  • the condition of an INNER JOIN filters rows exactly like the WHERE clause, and a LEFT JOIN whose condition does not hold reads NULL from the joined relation instead of removing the row;

  • OPERATOR(pg_catalog.=) and the other OPERATOR(…​) spellings, and COLLATE (ignored: catalog names sort by code point, as with COLLATE "C", whatever collation is named);

  • UNION, INTERSECT and EXCEPT (with or without ALL) whose arms are catalog queries or SELECT statements over literals, such as SELECT 1 UNION SELECT 2. ArcadeDB SQL has no set operations, so a set operation that reads your own types is still passed to the SQL engine, which rejects it;

  • count(*) and the other aggregates (count, sum, avg, min, max, bool_and, bool_or, string_agg, array_agg), with or without GROUP BY;

  • correlated sub-selects: (SELECT …​) as a value, EXISTS (SELECT …​), ARRAY(SELECT …​) and IN (SELECT …​);

  • a column default appears in pg_attrdef and is printed by pg_get_expr(), so \d and a JDBC client’s COLUMN_DEF show it, as information_schema.columns already did.

A projection that names two columns alike, such as SELECT NULL, NULL, relkind, returns both columns.

A catalog query that would take more work than the server spends on one statement - a regular expression that backtracks heavily, or deeply nested correlated sub-selects - fails with SQLSTATE 54000 (program_limit_exceeded) instead of returning an empty result.

format_type() and the regtype cast now return the name PostgreSQL prints for a type (integer, character varying, double precision) instead of its internal name (int4, varchar, float8), matching PostgreSQL. pg_type.typname is unchanged.

Since v26.11.1, information_schema.columns.data_type answers the SQL standard name, as format_type() does (integer, character varying, double precision, timestamp without time zone), and the literal ARRAY for any array column. udt_name keeps the pg_type name (int4, varchar, float8, _text), so the two columns differ exactly where PostgreSQL’s do. The precision columns the relation declares are written for the built-in types that have them: numeric_precision (16, 32 and 64 for the integers, 24 for real, 53 for double precision), numeric_scale (0 for the integers), datetime_precision (6 for a timestamp, 0 for a date) and character_octet_length (2^30 for a text column with no declared length). numeric_precision_radix is NULL for a type that is not a number, where it used to answer 2.

Since v26.11.1, a catalog query that matches no row describes its columns exactly as one that matches: every column of a catalog relation has a fixed type (pg_class.oid is always int4, relhasindex always bool). A pg_type lookup of a type that does not exist returns the columns it asked for with no rows. (Before, such a lookup returned no columns at all, and an empty answer from another relation announced every column as varchar, issue #10869.)

Since v26.11.1, the indexes of the database are in the catalog, so psql’s `\d <table> prints the Indexes: section, \d <index> describes an index, \di lists them, and the index queries of psqlodbc and of the GUI tools are answered:

  • pg_index has a row per index of a type, pg_class a row of kind i for it, pg_attribute a row per key column, pg_indexes the statement that creates it (also printed by pg_get_indexdef()), and pg_class.relhasindex and pg_tables.hasindexes are true for the types that have one. An index is listed under the name ArcadeDB gives it (Person[name]) on the type that declares it, not on its sub-types, and its access method is the family it belongs to in PostgreSQL’s terms: btree for an LSM tree, hash, gin for a full-text index, gist for a geospatial one and hnsw for the vector indexes.

  • ArcadeDB declares uniqueness with a unique index and has nothing else that could be the constraint, so each unique index is also the unique constraint of the same name: it is in pg_constraint (kind u), in information_schema.table_constraints and information_schema.key_column_usage, and psql prints it as UNIQUE CONSTRAINT. There is no primary key, foreign key or check constraint in ArcadeDB, so none is reported and SQLPrimaryKeys returns no row.

  • pg_inherits has a row per super-type of a type, which is what \d prints as Inherits: and Child tables:. pg_policy, pg_statistic_ext, pg_publication, pg_publication_rel and pg_publication_namespace have no row: ArcadeDB has no row-level security policy, extended statistics object or publication. psql’s `\d asks about all of them for every table, and it used to fail on the first one it was refused.

  • indkey, indcollation, indclass and indoption are read as text, the values separated by a space, and the first can be subscripted from 0 (indkey[0]), as PostgreSQL’s int2vector is. The collations and operator classes have no equivalent here and are 0.

  • A statement may read one relation more than once - pg_class as the table and as each of its indexes, pg_attribute as a column of the table and of the index - and an outer join on these relations keeps the rows nothing matches, reading NULL. The conditions are evaluated as soon as the relations they read are bound, and a condition equating a column with something already bound finds the rows by it, so a table’s indexes are found without reading every index of the database. A catalog statement that reads one of the other relations twice is still answered the way it was.

The PostgreSQL JDBC driver’s DatabaseMetaData.getIndexInfo() and getPrimaryKeys() - what Hibernate, Flyway, Liquibase and BI tools call - are answered too. They list an index’s key columns with the set-returning function information_schema._pg_expandarray() (a row for each element of the array, the calls of one row in step) and read its members as (expression).n; the catalog reads those, SQL’s trim(both '"' from …​) and the bitwise & the driver uses to find a descending key. getPrimaryKeys() returns no row, as ArcadeDB has no primary key. A set-returning call under GROUP BY or an aggregate is not read.

A text compared with a flag is read as the boolean it spells (indisprimary = 't').

An index key over a HIDDEN property is not a column of its table in the catalog, so it is described the way PostgreSQL describes an index on an expression: its column is named expr, and a unique index with such a key is not listed as a unique constraint.

COPY …​ TO STDOUT

COPY in its export direction is supported (since 26.10.1), in every form PostgreSQL accepts for it:

COPY (SELECT name, age FROM Person WHERE age > 18) TO STDOUT
COPY Person TO STDOUT (FORMAT csv, HEADER)
COPY Person (name, age) TO STDOUT WITH (FORMAT binary)
COPY (SELECT FROM Person) TO STDOUT CSV HEADER DELIMITER ';' NULL 'nil'

The query in parentheses runs as it stands, in the session’s query language (so a {cypher} prefix applies to it), and a type name with an optional column list stands for the equivalent SELECT. Quote the name if it is not a plain identifier - COPY "My Type" TO STDOUT - and any character is allowed in it, backslashes and backticks included (fixed in 26.10.1: such a name used to be misread or refused). The rows travel in PostgreSQL’s own text, csv or binary COPY encoding, so anything that reads a PostgreSQL COPY stream reads them: psql’s `\copy (\copy (SELECT FROM Person) TO 'people.csv' CSV HEADER), pgjdbc’s CopyManager, pandas, and the Apache Arrow ADBC PostgreSQL driver, which reads every result set through COPY …​ TO STDOUT (FORMAT binary) by default and no longer needs adbc.postgresql.use_copy=false.

The option list (FORMAT, DELIMITER, NULL, HEADER, QUOTE, ESCAPE, FORCE_QUOTE, ENCODING) and the older keyword spelling (BINARY, CSV HEADER, DELIMITER AS, NULL AS, QUOTE AS, ESCAPE AS, FORCE QUOTE) are both accepted, with the same restrictions PostgreSQL applies to their combinations. The output is always UTF-8.

The exported columns are fixed before the first row goes out, the way RowDescription fixes them for a query. For a whole type they are every property the type declares, inherited ones included, plus whatever a sample row adds on top - a declared property is exported even when the first row does not have one. Because ArcadeDB types can hold rows of different shapes, that set is a good answer but not a provable one: if a row turns out to carry a property the export never announced, the COPY fails with SQLSTATE 0A000 naming that column, rather than writing the row without it. List the columns yourself to make the set exact and the export complete:

COPY Person (name, age, email) TO STDOUT
COPY (SELECT name, age, email FROM Person) TO STDOUT

A COPY whose columns the schema can name is streamed row by row and is not subject to arcadedb.postgres.queryMaxRows, which is what makes it the way to export a large type over this protocol; a query whose columns only its rows can name (a schemaless type with no sample row, another query language) is materialized first, within that limit, exactly as a plain query is. The streamed form holds the query’s cursor open for as long as the client reads, which is what a bulk export needs; where that is not wanted for every user of the wire, bound the query itself with a WHERE or a LIMIT, as psql \copy users do.

Not supported, and refused with SQLSTATE 0A000 (feature_not_supported): the ingest direction COPY …​ FROM STDIN (use INSERT, or the HTTP import API), and the server-side targets COPY …​ TO 'file' and COPY …​ TO PROGRAM, which would let a wire client write files or run commands on the server host. In binary format a column without a binary encoding on this server (an array) is refused the same way; export it in text or CSV format, or project it as a string.

Column type mapping

A column’s advertised PostgreSQL type must not depend on whether the query happens to return any rows - a client that prepares a statement against an empty result (or a schema-discovery DESCRIBE) and later re-executes it against a populated one would otherwise see the column’s type change under it. As of v26.9.1 this holds for every type ArcadeDB maps onto a native PostgreSQL OID:

  • BINARY is announced as bytea (with the standard \x<hex> text encoding), so ResultSet.getBytes()/PreparedStatement.setBytes() round-trip it as raw bytes, rather than the lossy varchar/"char"[] answers used before v26.9.1. Since v26.11.1, a LIST OF BINARY is announced as bytea[] (_bytea) whatever rows a query returns, and each element is sent as its bytes, so ResultSet.getArray() returns a byte[][]. Before, it was announced as text[], json or varchar depending on the rows, and its elements were sent as JSON arrays of numbers.

  • DECIMAL is announced as numeric (OID 1700), with a real binary encoder/decoder, rather than the lossy double precision or the text-only varchar used before v26.9.1 - ResultSet.getBigDecimal() round-trips the full precision. A NUMERIC value is capped at 16,000 total decimal digits (well under PostgreSQL’s own limit of 131,072 integer / 16,383 fractional digits), because ArcadeDB’s DECIMAL type has no configured precision or scale limit of its own; a value beyond this cap is declined with an error rather than accepted and silently truncated.

  • SHORT and BYTE are announced as int2 (smallint), matching the declared schema type, rather than widening to int4 (integer) as they did before v26.9.1.

  • DATE is announced as date. Before v26.9.1, a DATE property’s value on a database left at its default configuration (arcadedb.dateImplementation=java.time.LocalDate) was announced as varchar for a populated result.

  • DATETIME is announced as timestamp. The one case where a DATETIME column’s announced type can still depend on whether a row was sampled is a database explicitly configured with arcadedb.dateTimeImplementation=java.util.Date (the default is java.time.LocalDateTime): java.util.Date is also DATE’s default representation, so a bare sampled value cannot always tell the two apart on its own. The server resolves this from the schema whenever the query names a single source type, which covers ordinary column selection; a column reached through a table alias, a computed expression, or a multi-table `JOIN can still fall back to the value-only answer in that one configuration.

Since v26.10.1, a column is typed from all the rows of a result, not from the first one, and a list from all its elements: [1, 2.5] is announced as float8[], [1, 3000000000] as int8[], and a list that also holds a DECIMAL as numeric[]. A list or column that mixes values no common number type can hold is announced as text.

Since v26.10.1, a prepared statement keeps ONE layout for all its rows, because a client may describe it once and reuse that description. A property the type does not declare (ArcadeDB is schemaless) can hold an integer in one record and a long, a double or a string in the next, so such a property is announced as varchar (a list as text[]) when the statement is described (Describe on the statement, or on a portal of a named statement, which is what the PostgreSQL JDBC driver does from its prepareThreshold-th execution). Declared properties keep their exact native type. To get a native type back for a property in a prepared statement, declare it, for example CREATE PROPERTY Event.score INTEGER.

Since v26.11.1, pg_type also lists the array types of int2, bytea, bpchar, varchar, date and timestamp, and a parameter bound as one of them is accepted - for example the varchar[] the PostgreSQL JDBC driver sends for Connection.createArrayOf("varchar", …​), which used to fail.

Since v26.11.1, an array parameter sent in text format (a {…​} literal, what a client that does not negotiate the binary format sends) is read as PostgreSQL reads it. An unquoted NULL element is a SQL NULL, while "NULL" is the string NULL. An empty element "" is kept. A malformed literal, such as {a,,b}, is refused with malformed array literal instead of being stored as something else. A parameter sent without a type is read as an array only when it is a valid array literal, so a JSON document such as {"k": 1} stays a string. (Before, an empty element was dropped and NULL was stored as the string NULL, issue #10870.)

Since v26.11.1, every type of the database (a table or a materialized view in the catalog) also has its row type in pg_type, as every PostgreSQL table has: a composite type (typtype = 'c', typcategory = 'C') named after the type, in its schema, whose typrelid is the relation’s pg_class.oid. pg_class.reltype holds its OID, so the join pg_class c JOIN pg_type t ON t.oid = c.reltype finds it, and format_type(c.reltype, NULL) and c.reltype::regtype print the relation’s name. Only an index has reltype = 0. A column’s pg_attribute.attcollation, and each key of pg_index.indcollation, is the collation of its type: 100 (default) for text, varchar, bpchar and their arrays, 0 for every other type, as pg_type.typcollation says. (Before, reltype and attcollation were 0 for every relation and column, so the join from a table to its row type answered no rows, issue #10873.) A client that enumerates pg_type now reads one more row per type of the database.

Transactions

In autocommit mode (the JDBC default) every statement is one transaction: a write statement that touches many records - UPDATE, DELETE, INSERT, CREATE VERTEX, CREATE EDGE - either lands whole or is rolled back whole, for vertex, edge and document types alike. The one exception is a statement that asks for chunked commits with BATCH <n>, which commits every <n> records: use it on a statement touching millions of records, since one transaction otherwise holds every modified page until the statement ends. To span several statements with one transaction, send an explicit BEGIN …​ COMMIT (or ROLLBACK).

A client that PIPELINES several statements before waiting for their answers - a JDBC executeBatch(), psycopg3 pipeline mode, asyncpg - groups them differently (since 26.10.1): everything between two Sync messages is one transaction, so a batch either lands whole or leaves nothing behind, and the statements after the one that failed are not run at all. This is what PostgreSQL does and what BatchUpdateException handling assumes. Before 26.10.1 each statement in such a batch committed on its own, so a failure halfway left the earlier ones persisted; if you relied on that, split the batch into separate round trips.

Since v26.10.1 a transaction-control statement is recognized in every spelling ArcadeDB SQL accepts, not only the bare keyword: with a leading or trailing comment (as added by sqlcommenter, OpenTelemetry or connection-pool routing hints, e.g. COMMIT /traceparent='…​'/), with extra ;, BEGIN ISOLATION <level> (which opens the transaction with that isolation level, e.g. BEGIN ISOLATION REPEATABLE_READ) and COMMIT RETRY <n> (a plain COMMIT outside a SQL script, which has no block to retry). A COMMIT that fails, for example on a duplicated unique key, reports the error and ends the transaction, as in PostgreSQL: the session is back in idle state and no ROLLBACK is needed.

Since v26.10.2 PostgreSQL’s own transaction-control spellings are accepted too, as ORMs and drivers send them:

  • BEGIN [WORK | TRANSACTION] [<mode> [, …​]] and START TRANSACTION [<mode> [, …​]], where <mode> is ISOLATION LEVEL <level>, READ WRITE or [NOT] DEFERRABLE;

  • {COMMIT | END} [WORK | TRANSACTION] [AND NO CHAIN];

  • {ROLLBACK | ABORT} [WORK | TRANSACTION] [AND NO CHAIN].

ISOLATION LEVEL READ COMMITTED and READ UNCOMMITTED open a READ_COMMITTED transaction (in PostgreSQL too READ UNCOMMITTED behaves as READ COMMITTED), and REPEATABLE READ a REPEATABLE_READ one. What this server cannot honor is refused with SQLSTATE 0A000 (feature not supported) instead of a syntax error: ISOLATION LEVEL SERIALIZABLE, READ ONLY, and COMMIT AND CHAIN / ROLLBACK AND CHAIN. In v26.10.1 and earlier only ArcadeDB’s SQL spellings above were recognized.

Savepoints are not supported. SAVEPOINT <name> and RELEASE [SAVEPOINT] <name> are accepted and do nothing, but ROLLBACK TO [SAVEPOINT] <name> is refused with SQLSTATE 0A000 (feature not supported) and the transaction is marked aborted, so the client must ROLLBACK it. (Since v26.10.1; before, ROLLBACK TO SAVEPOINT reported success while every write made since the savepoint stayed pending and was persisted by the next COMMIT.) An ORM or driver that wraps statements in savepoints (for example a nested transaction.atomic() in Django, or psql’s ON_ERROR_ROLLBACK) only works as long as it never rolls back to one.

Setting auto commit to false is not 100% supported: it does not start a transaction on its own, so a write sent after setAutoCommit(false) without a BEGIN is refused with "Transaction not begun". With JDBC, leave the default settings or set:

conn.setAutoCommit(true);

Session settings (SET, SHOW, RESET)

SET changes a setting for the current connection only: it never changes the database or any other connection, and SHOW reads it back. Since v26.10.1 it follows PostgreSQL’s rules:

  • a SET made inside a transaction that is rolled back, or that fails, is undone;

  • SET LOCAL lasts only until the end of the current transaction (outside a transaction it has no lasting effect);

  • RESET <name>, RESET ALL and SET <name> TO DEFAULT restore the value the client sent when it connected, else the server default;

  • SET TIME ZONE '<zone>' is accepted as a synonym of SET TimeZone TO '<zone>', and SHOW TIME ZONE / RESET TIME ZONE as synonyms of SHOW TimeZone / RESET TimeZone;

  • SHOW TRANSACTION ISOLATION LEVEL is a synonym of SHOW transaction_isolation: it answers the level of the transaction that is open (for example repeatable read after BEGIN ISOLATION REPEATABLE_READ), under the column transaction_isolation, which is also what the JDBC driver’s Connection.getTransactionIsolation() reads;

  • SHOW answers under PostgreSQL’s spelling of the parameter (DateStyle, TimeZone), and SHOW ALL lists the parameters the server answers for plus every one the connection has set;

  • a SET answers only SET, like in PostgreSQL, instead of a one-row result;

  • the parameters PostgreSQL reports to drivers (DateStyle, TimeZone, application_name, client_encoding, IntervalStyle, is_superuser, server_version, …​) are sent at connection time and again whenever they change, so a driver that caches them sees the new value.

Some settings describe what the server does and cannot be changed: dates always travel in ISO format (DateStyle keeps only the field order you choose) and text is always UTF8. A SET of a read-only setting such as server_version is refused.

String literals are read as PostgreSQL reads them. With standard_conforming_strings on, the default and what the server reports to drivers, a backslash in a plain '…​' literal is an ordinary character ('C:\' is the three characters C:\) and a quote inside a literal is doubled ('it''s'). Backslash escapes are processed in an E'…​' escape string (E’a\tb' holds a tab), with PostgreSQL’s escape set: \b \f \n \r \t, octal \ooo, hex \xhh, \uXXXX and \UXXXXXXXX. SET standard_conforming_strings = off (or the same option in the startup packet, e.g. pgjdbc’s options=-c standard_conforming_strings=off) makes every plain literal an escape string too, as in PostgreSQL. Inside an ArcadeDB JSON object (CONTENT {…​}) a single-quoted string keeps the ArcadeDB meaning.

Since v26.11.1. Before, standard_conforming_strings was reported on while every literal was read with the ArcadeDB SQL escapes: 'a\b' was stored as ab, a literal ending in a backslash was a syntax error, E'…​' did not parse, and a SET of the parameter to off was accepted and ignored. A client that wrote the ArcadeDB escapes in plain literals over the PostgreSQL protocol ('line\nbreak' for a line break, 'it's') now gets them literally: write them in an E'…​' literal, or set standard_conforming_strings to off for the connection. Values bound as parameters were never affected.

A SET is answered SET only when it did what it asked. Since v26.10.1 every other SET is answered with an error, on both the simple and the extended query protocol, rather than accepted and ignored:

  • a SET the server cannot parse (for example SET x, with neither = nor TO) is a syntax error (42601), reported at Parse on the extended protocol, like in PostgreSQL;

  • SET ROLE <role> and SET SESSION AUTHORIZATION <user> are refused (0A000): a connection always keeps the privileges of the user it authenticated as, and a client told otherwise would believe it had dropped privileges it still holds. SET ROLE NONE, SET SESSION AUTHORIZATION DEFAULT, RESET ROLE and RESET SESSION AUTHORIZATION are accepted, since that is already the case;

  • SET TRANSACTION ISOLATION LEVEL …​ and SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL …​ (what the JDBC driver’s Connection.setTransactionIsolation() sends) are accepted for the level transactions really run at (the database’s READ COMMITTED by default, which is also what READ UNCOMMITTED means in PostgreSQL) and refused (0A000) for any other, such as SERIALIZABLE. To run a transaction at REPEATABLE READ, open it with BEGIN ISOLATION LEVEL REPEATABLE READ;

  • READ ONLY transactions are not supported, so SET TRANSACTION READ ONLY and SET default_transaction_read_only = on are refused (0A000); READ WRITE and [NOT] DEFERRABLE are accepted;

  • SET CONSTRAINTS ALL DEFERRED | IMMEDIATE is accepted without effect, as in PostgreSQL for constraints that are not deferrable, which ArcadeDB’s never are;

  • SET NAMES '<encoding>' and SET SCHEMA '<schema>' are accepted as synonyms of SET client_encoding and SET search_path. Since v26.10.2 search_path is accepted but not stored: a connection always resolves names in the database it is connected to, so SHOW search_path answers that database, as current_schema() does;

  • statement_timeout is enforced since v26.10.2: a statement that runs longer is cancelled with 57014. It takes a number of milliseconds or a value with a unit (30s, 2min), and 0 disables it. The shorter of it and arcadedb.command.timeout applies;

  • DISCARD ALL (what connection poolers such as pgbouncer send between two client sessions) resets every setting and drops the prepared statements and portals. It is refused inside a transaction block (25001). DISCARD PLANS | SEQUENCES | TEMP are accepted without effect, and DEALLOCATE [ALL | <name>] and CLOSE [ALL | <name>] are supported;

  • a parameter PostgreSQL does not know is refused by SET, SHOW and RESET alike with 42704 unrecognized configuration parameter, as in PostgreSQL (for example SET bogus_param = 1 or SHOW databases). Every parameter PostgreSQL 17 knows is accepted, as is any name the client sent when it connected. A name with a dot (SET myapp.tenant = 'acme') is a custom setting: it is always accepted, and SHOW reads it back once it is set;

  • a parameter PostgreSQL lets only the server configuration change, such as shared_buffers or wal_level, is refused with 55P02.

Cancelling a statement

Since v26.10.2 a cancel request works as in PostgreSQL. The CancelRequest that JDBC’s Statement.cancel(), pgjdbc’s setQueryTimeout() and libpq’s PQcancel send stops the statement the connection is running: it fails with SQLSTATE 57014 (query_canceled), what it wrote is rolled back as after any other failed statement, and the connection stays open for the next one. A connection that is not running a statement is not affected.

Each connection gets its own backend process id and a random 32-bit secret key, which the client receives at connect time (BackendKeyData) and must quote in its cancel request. The statements a connection runs are listed by the HTTP list queries command and by Cypher SHOW TRANSACTIONS with protocol postgres and the process id as connectionId, and stopped by terminate query or TERMINATE TRANSACTIONS with the same 57014.

As on PostgreSQL, a cancel request is not authenticated: the secret key is its only credential, and process ids are sequential. A cancel request that quotes a live connection with the wrong secret stops nothing and gets no answer; the server logs it, at WARNING at most once a minute with the number refused since, so guessing is visible in the log. A cancel is best effort, as on PostgreSQL: one that arrives between two statements stops nothing, and one that crosses the start of the next statement may stop that one.

Three SQL functions work on connections by process id, as their PostgreSQL namesakes:

Function Returns

pg_backend_pid()

The process id of the calling connection, null when the statement does not run on a PostgreSQL connection.

pg_cancel_backend(pid)

Stops the statement that connection is running and leaves the connection open. true when the connection was signalled.

pg_terminate_backend(pid)

Stops the statement that connection is running and closes the connection, rolling back its open transaction. true when the connection was signalled.

root may signal every connection, any other user only their own. Both signal functions answer false for a process id that names no connection the caller may signal. They answer NULL for a NULL process id and fail on a fractional one (such as 1.5), as on PostgreSQL. They signal only connections of the server that runs them, and are registered with the SQL functions of the JVM once the PostgreSQL plugin starts, so SQL sent over any protocol can call them.

SELECT pg_backend_pid();          -- this connection's process id, e.g. 1042
SELECT pg_cancel_backend(1042);   -- stop what connection 1042 is running
SELECT pg_terminate_backend(1042);-- stop it and close the connection
Before v26.10.2 a cancel request closed the whole connection, and since every connection had the backend process id 0, a cancel could reach a different connection than the one it was meant for. A client that relied on a cancel request closing the connection must now call pg_terminate_backend() instead.

Creating an object whose name is taken

(Since v26.11.1.) A statement that creates, or renames onto, a name that is already taken fails with the SQLSTATE PostgreSQL uses for the same case: 42P07 (duplicate_table) for a type, an index or a view, 42701 (duplicate_column) for a property, and 42710 (duplicate_object) for anything else, such as a constraint, a bucket, a trigger or a function. Before v26.11.1 it was XX000 (internal error), or 42P01 (undefined table) for some of them.

Postgres Tools Known to Work

Some tools compatible with Postgres may execute queries on internal Postgres tables to retrieve the schema. Most of these queries - the ones asking about schemas, tables, columns and a few other well-known catalogs - are now answered generically (see above), but a tool asking about a pg_catalog object with no ArcadeDB equivalent (functions, triggers, extensions, and similar) gets an error with SQLSTATE 0A000 for that specific question, since v26.11.1, instead of an empty result that reads as an empty catalog. See tested compatible tools below. If the tool that you use to work with Postgres is not compatible with ArcadeDB, please open an issue.

PostgreSQL Client psql

Postgres’s psql tool works out of the box, just like with an actual Postgres server. To install this Postgres client, see here.

Connect from a terminal or console like this:

psql -h localhost -p 5432 -d mydatabase -U root

After authenticating, you can run SQL queries as normal, and \copy exports a query or a type to a local file (see COPY …​ TO STDOUT above). One can also submit the password via the environment:

PGPASSWORD=password psql -h localhost -p 5432 -d mydatabase -U root

or use the postgres protocol address:

psql postgres://username:password@host:port/database

In case the password contains special characters (like /, \, @, ?, !, &), it needs to be URL encoded (also known as "percent encoding").

Note, that in the psql console queries or commands need to be terminated with a semi-colon ; to be submitted.