Skip to main content

CREATE SERVER

A foreign server names a remote database — ClickHouse or PostgreSQL — together with the connection details needed to reach it, and stores that definition in the catalog. Once created, the server is a catalog you query like any other: server_name.schema.table.

The difference from ATTACH is persistence and access control. An attachment is session-level state that disappears when the server process restarts, and it carries no privileges of its own. A foreign server is catalog DDL: it survives restart, re-connects automatically on boot, has an owner, and is protected by USAGE privileges.

ATTACHCREATE SERVER
Survives restartNo — re-attach every sessionYes — replayed on boot
CredentialsIn the connection stringIn the server's OPTIONS
OwnerNoneThe creating role
Access controlNoneUSAGE on the server
Catalog visibilitySHOW DATABASESpg_foreign_server + SHOW DATABASES

CREATE SERVER

Syntax

Create a server over ClickHouse:

CREATE SERVER analytics FOREIGN DATA WRAPPER clickhouse_fdw
OPTIONS (host 'clickhouse.internal', port '9000', database 'events');

…and over PostgreSQL:

CREATE SERVER orders FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'pg.internal', port '5432', database 'shop', user 'reader', password 'secret');

Foreign-data wrappers

The wrapper name selects the connector. Two are implemented:

FOREIGN DATA WRAPPERConnects to
clickhouse_fdwClickHouse, over its native protocol
postgres_fdwPostgreSQL, over the wire protocol

Any other wrapper name is rejected at CREATE SERVER time.

Server names

A server name is a bare identifier — servers are not schema-qualified, so CREATE SERVER a.b is a syntax error rather than a server named a.b. Reserved keywords must be quoted (CREATE SERVER "select" …).

The name is also the attach alias, and that alias is instance-wide: it shares a namespace with SereneDB databases and with the aliases created by ATTACH. Two servers cannot share a name, and a server cannot take the name of an existing database.

OPTIONS

OPTIONS is a list of key 'value' pairs. Keys are identifiers — unquoted or quoted, and normalized to lower case, so "PASSWORD" and password are the same key. Values are always single-quoted strings, including numeric ones like port '9000'.

Common keys, with the aliases each connector accepts:

OptionAliasesMeaning
hosthostnameRemote host
portRemote port
userusernameRemote role
passwordpasswdPassword for that role
databasedbname, dbRemote database
securessl (ClickHouse)Use TLS

Keys beyond this list are passed through to the connector, so any connection parameter the underlying connector understands (for example sslpassword, connect_timeout) can be set the same way. An option the connector does not recognize surfaces as a connection error at CREATE SERVER.

Values are stored verbatim and are not parsed further, so a value containing spaces (password 'pass word') round-trips intact.

Connectivity is validated eagerly

CREATE SERVER connects to the remote before persisting anything. If the connection fails — wrong host, unknown role, bad password — the statement raises the connector's error and leaves behind neither a catalog row nor an attachment:

-- Rejected: no such role on the remote. Nothing is persisted.
CREATE SERVER orders FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'pg.internal', port '5432', database 'shop', user 'nosuchrole');

This makes a successful CREATE SERVER a positive statement about reachability, not just a recorded intention.

IF NOT EXISTS

IF NOT EXISTS makes the statement a no-op when the name is already taken. The check happens before the connection attempt, so the OPTIONS of a skipped CREATE SERVER are never applied — an existing server is not reconfigured by re-running CREATE SERVER IF NOT EXISTS with different options.

Querying through a server

A server behaves as a catalog. Reference remote objects with the server name in the catalog position:

SELECT id, val FROM analytics.events.pageviews ORDER BY id;

Where the connector supports writes, DDL and DML work through the server too — the PostgreSQL connector, for example, accepts schema and table creation:

CREATE SCHEMA orders.staging;
CREATE TABLE orders.staging.import (id INTEGER PRIMARY KEY, val TEXT);
INSERT INTO orders.staging.import VALUES (1, 'first');

Filter pushdown applies as it does for an attachment: equality, IN and other supported predicates are translated into the remote query rather than filtered locally.

Persistence and boot replay

A foreign server is stored in the catalog, so it survives a restart — including an unclean one. On boot, SereneDB replays its foreign servers and re-connects each one, so queries through the server work again without any re-ATTACH:

CREATE SERVER analytics FOREIGN DATA WRAPPER clickhouse_fdw
OPTIONS (host 'clickhouse.internal', port '9000', database 'events');
-- restart serened, then:
SELECT count(*) FROM analytics.events.pageviews; -- still works

If a remote is unreachable at boot, the replay for that server is skipped rather than failing startup; the catalog row remains and queries through the server error until the remote returns.

DROP SERVER

Syntax

DROP SERVER removes the catalog row and detaches the live connection, so the name becomes immediately reusable:

DROP SERVER analytics;

After the drop the server is gone from pg_foreign_server and from SHOW DATABASES, and querying through it errors. DROP SERVER IF EXISTS on a missing server is a no-op.

RESTRICT is the default. CASCADE is accepted for PostgreSQL compatibility; because nothing in SereneDB can depend on a foreign server — there are no user mappings and no foreign tables — the two behave identically today.

What DROP SCHEMA and DROP DATABASE do

Foreign servers are children of the database, not of a schema (matching PostgreSQL, whose pg_foreign_server has no namespace column). That determines which cascades reach them:

StatementEffect on foreign servers
DROP SCHEMA … CASCADENone — servers are untouched and keep serving
DROP DATABASE …Removes the database's servers and detaches their connections
DROP SERVER …Removes that server and detaches it
-- The server outlives the schema, cascade or not.
DROP SCHEMA public CASCADE;
SELECT count(*) FROM analytics.events.pageviews; -- still served

Catalog visibility

Created servers appear in pg_foreign_server, scoped to the current database:

SELECT srvname FROM pg_foreign_server WHERE srvname = 'analytics';

srvowner holds the owning role, and srvoptions is a text[] of key=value entries in the order they were given:

SELECT r.rolname, s.srvoptions
FROM pg_foreign_server s JOIN pg_roles r ON s.srvowner = r.oid
WHERE s.srvname = 'analytics';

Two gaps are worth knowing: srvfdw is always 0 — the wrapper name is not currently exposed through the catalog — and pg_foreign_data_wrapper is an empty compatibility stub, so wrappers cannot be enumerated from SQL. See System Table Compatibility.

Privileges

A foreign server is owned by the role that created it and carries a single privilege, USAGE.

ActionRequirement
CREATE SERVERCREATE on the current database
DROP SERVEROwnership of the server, or superuser
Querying through the serverUSAGE on the server (owner and superusers exempt)
GRANT / REVOKE USAGEOwnership, WITH GRANT OPTION, or superuser

Because servers are database children, the create gate is a database grant — granting CREATE on a schema does not allow it:

GRANT CREATE ON DATABASE shop TO analyst;

USAGE is checked at query time, so granting and revoking it takes effect on the next statement:

GRANT USAGE ON FOREIGN SERVER analytics TO analyst;
REVOKE USAGE ON FOREIGN SERVER analytics FROM analyst;

Without it, a query through the server fails with permission denied for foreign server analytics. USAGE is the only privilege the object accepts — GRANT SELECT ON FOREIGN SERVER … is rejected as an invalid privilege type.

Differences from PostgreSQL

  • No USER MAPPING. Credentials live in the server's OPTIONS and one shared connection serves every authorized role, the way a ClickHouse connection is normally shared. CREATE USER MAPPING and DROP USER MAPPING are syntax errors; per-role remote identities are not available.
  • No CREATE FOREIGN TABLE. Remote tables are reached through the server's catalog (server.schema.table), discovered live, rather than declared one by one.
  • No ALTER SERVER, no CREATE OR REPLACE SERVER. Drop and recreate to change options.
  • No CREATE FOREIGN DATA WRAPPER. The two wrappers are built in.
  • pg_foreign_server is superuser-only, and its option values are unredacted (see above).
  • CREATE SERVER connects eagerly, so it fails on an unreachable remote instead of deferring the error to first use.

See also