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.5.1</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:

2021-07-08 19:05:06.081 INFO  [ArcadeDBServer] <ArcadeDB_0> - Postgres Protocol 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()

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.

ArcadeDB does not support SSL/TLS connections over this protocol.
Both the "simple" query protocol and the extended query protocol (Parse/Bind/Describe/Execute/ Sync, what PreparedStatement uses) are supported.

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 returns an empty result set rather than an error, matching how PostgreSQL itself answers a pg_catalog object it does not have.

The emulated schema list always contains exactly 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.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.

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. 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.

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.

  • 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.

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).

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);

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 (indexes, foreign keys, triggers, and similar) still gets an empty result for that specific question rather than an error. 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.