Skip to content
Discord Get Started

Compatibility Matrix

DB9 implements a PostgreSQL-compatible SQL engine over TiKV distributed storage. Most PostgreSQL clients, ORMs, and drivers work without changes. This page documents where DB9 matches PostgreSQL, where it diverges, and what is not available.

Use this matrix when evaluating DB9 for a new project or migrating an existing PostgreSQL application.

CategoryCoverageNotes
SQL DML (SELECT, INSERT, UPDATE, DELETE)FullJOINs, CTEs, window functions, subqueries, upsert, RETURNING; no MERGE
SQL DDL (CREATE, ALTER, DROP)Near-fullPartitioning is RANGE on a timestamp/timestamptz key only; no table inheritance or foreign data wrappers
Data types20+ typesAll common types incl. INET; no XML, CIDR/MACADDR; range types limited to a partial INT4RANGE
IndexesB-tree, Hash, GIN and HNSWHash covers integer and OID columns only; GiST/SP-GiST/BRIN are rejected
TransactionsNear-fullREAD COMMITTED and REPEATABLE READ enforced, but some write conflicts fail at COMMIT with 40001 instead of at the statement; SERIALIZABLE is downgraded to REPEATABLE READ
Built-in functions200+String, math, date/time, JSON/JSONB, array, aggregate, window, FTS
Wire protocolpgwire v3Simple Query, Extended Query, COPY, prepared statements
ORM compatibility99%+Prisma, Drizzle, Sequelize, Knex, TypeORM, GORM, SQLAlchemy tested
System catalogs50+ viewspg_catalog, information_schema, cron schema
ExtensionsFixed built-in sethttp, fs9, pg_cron, vector, embedding, uuid-ossp (uuid_generate_v4() only), parquet, zhparser (plus hstore, pgcrypto and plpgsql as metadata-only shims); no community installs
PL/pgSQLPartialBasics supported, including CONTINUE, dynamic EXECUTE, exception handling and nested blocks; no WHILE / FOREACH loops or cursors
ReplicationPartialLogical decoding from replication slots (wal2json through SQL, pgoutput streamed over the replication protocol); no CREATE SUBSCRIPTION and no physical replication
FeatureStatus
SELECT with FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT/OFFSETSupported
DISTINCT / DISTINCT ONSupported
JOINs (INNER, LEFT, RIGHT, FULL OUTER, CROSS)Supported
Subqueries (correlated, scalar, EXISTS, IN)Supported, including inside a LATERAL body
CTEs (WITH … AS)Supported, including a WITH inside a LATERAL body and a name reused across scopes of one statement. A duplicate name within one WITH list is rejected with 42712, as in PostgreSQL
Recursive CTEs (WITH RECURSIVE)Supported, up to 1,000 iterations
Data-modifying CTEs (WITH w AS (INSERT ... RETURNING ...))Supported over both the PostgreSQL wire protocol and the HTTP SQL API, in any position in the WITH list
Set operations (UNION, INTERSECT, EXCEPT)Supported
Window functions (ROW_NUMBER, RANK, LAG, LEAD, etc.)Supported
INSERT with VALUES, SELECT, DEFAULT, RETURNINGSupported
INSERT ON CONFLICT (upsert)Supported
MERGENot supported — every form, including WITH ... MERGE, EXPLAIN MERGE and PREPARE ... AS MERGE, fails with 0A000 (MERGE is not supported) before any table is read. Write upserts with INSERT ... ON CONFLICT
UPDATE with WHERE, subqueries, RETURNINGSupported
DELETE with WHERE, subqueries, RETURNINGSupported
CASE expressionsSupported
Row-constructor comparisons ((a,b) = (c,d))Supported — =, <>, <, <=, > and >=, and IN / NOT IN against a list of rows, follow PostgreSQL’s NULL rules. Two forms fail where PostgreSQL returns a result: IS [NOT] DISTINCT FROM with a bare NULL literal opposite a typed value or column (42804), and a VALUES list on the right of IN / NOT IN (42883). See the caution below
COPY (CSV, TEXT)Supported for the COPY <table> [(columns)] form (BINARY format not supported). The query form COPY (SELECT ...) TO STDOUT is rejected with 0A000
EXPLAIN / EXPLAIN ANALYZESupported (ANALYZE adds summary runtime stats — actual rows, execution time, KV counters — but no per-operator timing; FORMAT JSON is honored). Plan rows are returned over pgwire only — the HTTP SQL API returns an empty result
LATERAL joinsSupported, including correlated bodies, a WITH clause inside the body, and correlated subqueries inside the body
FeatureStatus
CREATE/ALTER/DROP TABLESupported
CREATE TABLE AS / SELECT INTOSupported — but the new table gets an extra _rowid primary key column that is visible in SELECT *, see DDL. WITH [NO] DATA is not parsed (42601). Once a replication slot has been created in the database, _rowid can no longer be dropped from a table that holds rows (55000), and a wal2json slot receives the rows these statements load even though the new table is in no publication, see Logical decoding
Column constraints (PRIMARY KEY, UNIQUE, NOT NULL, DEFAULT, CHECK)Supported — DEFERRABLE and INITIALLY DEFERRED are accepted on PRIMARY KEY and UNIQUE and recorded in pg_constraint, but the check still runs row by row: a statement that passes through a temporary duplicate (UPDATE t SET x = x + 1) fails with 23505, with or without SET CONSTRAINTS ... DEFERRED
Foreign keys (CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION)Supported — all five actions behave as declared, 23503 on violation, but a referencing table without a primary key cannot declare ON DELETE SET NULL / SET DEFAULT or an ON UPDATE CASCADE / SET NULL / SET DEFAULT action (0A000 cannot create foreign key ... because it does not have a primary key; PostgreSQL accepts them), and TRUNCATE or DROP TABLE of a referenced table is refused without CASCADE, see the caution below. A DEFERRABLE foreign key is checked at COMMIT when it is INITIALLY DEFERRED or after SET CONSTRAINTS ALL DEFERRED (or SET CONSTRAINTS <name> DEFERRED); a violation then fails the COMMIT with 23503 and the transaction rolls back. SET CONSTRAINTS ... IMMEDIATE restores the per-statement check
Generated columnsSupported
SERIAL / BIGSERIALSupported
CREATE/DROP INDEX (B-tree, Hash, GIN, HNSW)Supported — see Indexes for each method’s coverage
CREATE INDEX CONCURRENTLYSupported
Expression indexes, partial indexesSupported
CREATE/DROP VIEWSupported
CREATE/DROP MATERIALIZED VIEW, REFRESH MATERIALIZED VIEWSupported — WITH [NO] DATA is not parsed (42601), and a materialized view cannot be added to a publication (42809; PostgreSQL refuses it too, with 22023), so a replication slot never receives its rows, see Logical decoding
CREATE/DROP SEQUENCESupported
ALTER SEQUENCESupported — RESTART [WITH], INCREMENT BY, MINVALUE / MAXVALUE, START WITH, CACHE, [NO] CYCLE, AS, OWNER TO, OWNED BY and RENAME TO all take effect. SET SCHEMA, SET LOGGED and SET UNLOGGED are rejected with a bare internal error (0A000)
DROP SEQUENCE dependency checksSupported (2BP01 while a column default or identity column depends on the sequence). DROP SEQUENCE ... CASCADE removes a dependent column default silently, see DDL — Other DDL
CREATE/ALTER TYPE (enum)Supported, with gaps — enum values compare and sort by label text rather than declaration order (silently), see Advanced SQL — Custom Types. ALTER TYPE supports the enum-value forms, RENAME TO, RENAME ATTRIBUTE and OWNER TO; SET SCHEMA and the attribute-changing forms are rejected with 0A000 (ALTER TYPE ... is not supported). DROP TYPE refuses while a column or an array column uses the type (2BP01), and DROP TYPE CASCADE clears a function-argument dependency but not a column one. A function that returns the type (RETURNS t, t[] or SETOF t) also blocks DROP TYPE (2BP01) and is dropped by CASCADE; a RETURNS TABLE (...) column of the type is not checked, see DDL — Other DDL
CREATE/DROP SCHEMASupported
CREATE/DROP FUNCTION (PL/pgSQL)Supported
CREATE/DROP TRIGGER (BEFORE/AFTER, INSERT/UPDATE/DELETE)Supported, with gaps — a BEFORE trigger function may not use counted FOR loops, bare LOOPs, CASE statements, EXECUTE, exception handlers or nested blocks (CREATE TRIGGER refuses it — 0A000, or 42601 for WHILE and FOREACH); a FOR r IN SELECT … LOOP query loop is allowed, but only over a declared record variable. FOR EACH STATEMENT, row-level WHEN (...), UPDATE OF and trigger arguments (TG_NARGS / TG_ARGV) are supported; TRUNCATE, INSTEAD OF and constraint triggers, transition tables, WHEN on statement triggers, and a WHEN condition that references a temporal column, now() or current_user are rejected (0A000), see Advanced SQL — Triggers
CREATE/ALTER/DROP EVENT TRIGGERSupported for ddl_command_start, ddl_command_end and sql_drop (table_rewrite and login are rejected, 0A000). While an enabled event trigger exists, most DDL is refused (0A000) — views, sequences, foreign keys, ALTER TABLE ... DROP COLUMN, GRANT and more. Over the HTTP SQL API, disable the trigger in a separate request before running the migration — see Advanced SQL — Event triggers
TRUNCATETRUNCATE t / TRUNCATE TABLE t, optionally with RESTART IDENTITY or CONTINUE IDENTITY and with CASCADE or RESTRICT — all as in PostgreSQL: the identity clauses reset or preserve the table’s sequences, and CASCADE also empties every table whose foreign key references it. The multi-table form and ONLY are rejected (42601). A table that another table’s foreign key references cannot be truncated without CASCADE (0A000), as in PostgreSQL, see the caution below
Row-Level Security (ENABLE/DISABLE/FORCE RLS)Supported, with or without ONLY, see RLS
CREATE/ALTER/DROP POLICYSupported
Table partitioningPartial — PARTITION BY RANGE on a timestamp or timestamptz key, with PARTITION OF ... FOR VALUES FROM (...) TO (...). Rows route to their partition, and a row no partition covers fails with 23514. Any other key type fails with 0A000 RANGE partitioning currently requires a timestamp or timestamptz key; LIST, HASH, DEFAULT partitions, ATTACH PARTITION and DETACH PARTITION are syntax errors (42601). Only plain INSERT and SELECT go through the parent — INSERT ... ON CONFLICT fails with 0A000 ON CONFLICT on RANGE relations is not yet supported on the parent and on its partitions. UPDATE and DELETE on the parent fail with 0A000 UPDATE and DELETE on RANGE parents are not yet supported, TRUNCATE with 0A000 (TRUNCATE on partitioned table "..." is not supported while it has partitions) and COPY ... FROM with 0A000 (COPY FROM into partitioned table "..." is not yet supported; copy into its partitions or use INSERT) — run them against a partition, where every write is checked against the partition’s bounds (23514 new row for relation "..." violates partition constraint). Rows never move between partitions: a foreign-key ON UPDATE CASCADE or ON DELETE SET DEFAULT that would carry a row’s key into another partition fails with 23514, where PostgreSQL moves the row. A partitioned table cannot have an identity column — creating its first partition fails with 0A000 partition identity-column inheritance is not yet supported; declare the column BIGSERIAL instead. An index declared on the parent before its partitions exist is created on each partition; CREATE INDEX on a parent that already has partitions fails with 0A000. Once a partitioned table exists, querying pg_partitioned_table fails with 0A000
Table inheritanceNot supported
Foreign data wrappers (FDW)Not supported
TablespacesNot supported (TiKV manages storage placement)
FeatureStatus
BEGIN / COMMIT / ROLLBACKSupported
SAVEPOINT / RELEASE / ROLLBACK TOSupported
READ COMMITTED isolationSupported — statement-level snapshots, as in PostgreSQL. Default level.
REPEATABLE READ isolationSupported — transaction-level snapshot
SERIALIZABLE isolationDowngraded to REPEATABLE READ, see note below
READ UNCOMMITTED isolationSupported — behaves as READ COMMITTED, as in PostgreSQL
SET LOCAL (transaction-scoped settings)Supported
READ ONLY transactionsSupported — DML and DDL writes are rejected with SQLSTATE 25006
DEFERRABLE transactionsNot supported (requires SERIALIZABLE)
Advisory locks (pg_advisory_lock family)Supported (node-local; not coordinated across multiple db9-server processes)
TypeAliasesNotes
BOOLEANBOOL
SMALLINTINT2Stored as INT4 internally
INTEGERINT, INT4
BIGINTINT8
REALFLOAT4Stored as FLOAT8 internally
DOUBLE PRECISIONFLOAT8
NUMERICDECIMALWith precision and scale. A declared precision up to 1000 is accepted, but a value holds at most 28–29 significant digits (largest magnitude 79228162514264337593543950335) and 28 decimal places: a larger literal fails with 22P02, arithmetic beyond it with 22003, and decimal places beyond that capacity are rounded off without an error. 'NaN' and 'Infinity' are rejected (22P02). A declared precision and scale are enforced as in PostgreSQL — NUMERIC(5,2) rejects 1234.5 with 22003 (numeric field overflow) and rounds 12.345 to 12.35. See Data types
TEXTVariable-length, no limit
VARCHAR(n)CHARACTER VARYING
CHAR(n)CHARACTERStored as VARCHAR internally
BYTEABinary data
DATE
TIMEWithout time zone
TIMETZTIME WITH TIME ZONEBasic support — store, compare, ORDER BY, index, min/max, and COPY. Input needs a numeric UTC offset written as ±HH or ±HH:MM (12:34:56+02, 08:00-05:30); a value with no offset (12:34), a +0530 offset or a zone abbreviation (12:34 PST) fails with 22P02, and a DEFAULT CURRENT_TIME or DEFAULT now() column default fails with 42804. Casts to or from TIME and TIMESTAMPTZ (42846), +/- with an INTERVAL and AT TIME ZONE (42883) are not available, and EXTRACT returns NULL for every field, see EXTRACT
TIMESTAMPWithout time zone, microsecond precision
TIMESTAMPTZWith time zone, microsecond precision
INTERVAL
JSONStored as text
JSONBCanonicalized (sorted keys, normalized whitespace)
UUID
INETIPv4/IPv6 host or network address; equality and ordering only — no network operators/functions, no CIDR
BOOLEAN[] / INT[] / TEXT[] / etc.Arrays of any supported type; multi-dimensional values are stored and read back — see below
BIT(n) / VARBIT(n)BIT VARYINGLength and content validated as in PostgreSQL; BIT(1) and bare BIT become BOOLEAN — see below
TSVECTORFull-text search document representation
TSQUERYFull-text search query
VECTOR(n)pgvector-compatible; for HNSW indexes and distance operators
NAMEPostgreSQL identifier type
TypeNotes
XML
CIDR / MACADDRNetwork address types; INET is supported (equality/ordering only)
Range types (INT4RANGE, TSRANGE, etc.)INT4RANGE is accepted but only partly implemented — see the caution below. INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, and TSTZRANGE do not exist
Composite types (user-defined row types)Declarable and writable via a text literal, but unreadable field-wise — see the caution below. Enum types are supported via CREATE TYPE, but sort by label text rather than declaration order, see Advanced SQL — Custom Types
Large objects (OID-based streaming)BYTEA is available for binary data
Money

Both of these were previously listed as unsupported and are now in the supported table above. Each is usable, with the divergences noted here:

BIT / VARBIT — length and content are both validated. BIT(n) rejects any value that is not exactly n bits with 22026 bit string length N does not match type bit(n); VARBIT(n) leaves a short value alone and rejects an over-length one with 22001 bit string too long for type bit varying(n); a value that is not 0s and 1s is rejected with 22P02. PostgreSQL 16 raises the same SQLSTATEs, and the 22026 and 22001 messages are identical to DB9’s; only the 22P02 text differs (PostgreSQL says "h" is not a valid binary digit). All four spellings parse — BIT(n), VARBIT(n), bare BIT VARYING and BIT VARYING(n) — so pg_dump output loads unchanged, and B'1010' literals are accepted. information_schema.columns, pg_typeof() and format_type() all agree on the type name and v::text renders the bit string — though all three drop the length modifier (bit, not bit(8), and character_maximum_length is NULL) even though the length is enforced on write.

BIT(1) and bare BIT are the one divergence left: both are created as BOOLEAN, where PostgreSQL keeps them as bit(1). They round-trip '1'/'0' as true/false and reject anything longer with 22P02 invalid input syntax for type boolean, so single-flag columns migrate cleanly but report as boolean in the catalog. One consequence: a B'1' bit-string literal, as pg_dump --inserts emits, is rejected for those two with 42804 column "v" is of type boolean but expression is of type bit(1) — plain pg_dump, whose COPY data is 1/0, loads fine. B'1010' into a wider BIT(n) is accepted.

Multi-dimensional arrays — an INT[][] column stores and returns them. As in PostgreSQL the column is catalogued as integer[] (PostgreSQL does not record dimensionality either), and one-, two- and three-dimensional values all round-trip. Subscripting (v[1][1]), array_length(v, n) per dimension, unnest() (which flattens to scalars), and the rejection of ragged input all match PostgreSQL, though the two ragged forms are reported differently: a literal ('{{1,2},{3}}', the form pg_dump emits) gives 22P02 invalid input syntax for type array against PostgreSQL’s 22P02 malformed array literal, while the ARRAY[...] constructor gives 2202E multidimensional arrays must have array expressions with matching dimensions — the same SQLSTATE and message as PostgreSQL, plus a DB9-only type suffix. array_ndims() and array_dims() are still missing — both fail with 42883 in either call position.

TypeStatusNotes
B-treeNear-fullDefault. Point, range, bounded-range, in-list, expression, partial indexes. One divergence: an index entry must encode to under ~6,963 bytes, so a long value that PostgreSQL would compress into the index is rejected with 54000 — see CREATE INDEX
GINFullJSONB containment (@>), full-text search (@@), array operators. Only TSVECTOR, JSONB and array columns are indexable — any other column type fails with 42704, as in PostgreSQL. Used for a literal or bound-parameter probe — for @@, a tsquery value or a *_tsquery() call that names its configuration; a probe computed by an expression falls back to a sequential scan. See below
HNSWNear-fullApproximate k-NN over VECTOR columns. Builds on both transports and the planner uses it — for a literal or bound-parameter probe with a LIMIT and no WHERE clause; any filter falls back to an exact scan, and an inline embedding(...) probe runs once per row, failing with 22023 once the table has more than 100 rows. The indexed column must declare a width (VECTOR(1024), not bare VECTOR) and the table needs a single-column primary key — see the note below
HashPartialEquality lookups on integer and OID columns. Single-column, non-unique indexes only — see below
GiSTNot supportedCREATE INDEX is rejected
SP-GiSTNot supportedCREATE INDEX is rejected
BRINNot supportedCREATE INDEX is rejected

btree, hash, gin, and hnsw are the only usable access methods, and pg_am lists exactly those four plus heap. An unusable method is always rejected at CREATE INDEX time — never accepted-then-ignored — but it comes back as one of two different errors, and which one tells you whether the method is merely unsupported or entirely unknown:

-- gist, spgist, brin — named and rejected
ERROR: access method "gist" is not supported (0A000)
HINT: Only btree, hash, gin, and hnsw indexes are currently supported.
-- anything else, including ivfflat — not a known name at all
ERROR: access method "ivfflat" does not exist (42704)

So GiST, SP-GiST and BRIN give 0A000 with the HINT above, while every other spelling gives 42704 — despite none of the three appearing in pg_am. IVFFlat falls in the second group: HNSW is the only vector index type.

A hash index is built and used for equality — EXPLAIN SELECT * FROM t WHERE n = 42 reports Index Scan using <index> on t — and, as in PostgreSQL, is ignored for range predicates. It carries three DDL-time restrictions beyond PostgreSQL’s, each a distinct 0A000:

CREATE INDEX ON t USING hash (name); -- name is TEXT
ERROR: hash indexes currently support only integer and OID columns (0A000)
CREATE UNIQUE INDEX ON t USING hash (id);
ERROR: access method "hash" does not support unique indexes (0A000)
CREATE INDEX ON t USING hash (a, b);
ERROR: access method "hash" does not support multicolumn indexes (0A000)

SMALLINT, INTEGER, BIGINT and OID columns are all indexable; TEXT and BOOLEAN are not. Use a B-tree for everything else — it covers equality as well, so hash is a narrowing choice, not a strictly better one.

A fourth divergence has no error to announce it. The hash index answers a single-value equality predicate (n = 42, n IN (42)), but a multi-element IN list falls back to a sequential scan — a btree index on the same column still produces an Index Scan, and PostgreSQL 16 serves the same predicate from its hash index via a bitmap scan. Results stay correct; only the plan degrades.

A GIN index can only be built over a column type that has a GIN operator class — TSVECTOR, JSONB, and array types such as TEXT[] or INT[]. On any other column type — TEXT, JSON, INTEGER — the statement fails as it does in PostgreSQL:

CREATE INDEX ON docs USING gin (body); -- body is TEXT
ERROR: data type text has no default operator class for access method "gin" (42704)
HINT: You must specify an operator class for the index or define a default operator class for the data type.

There is no pg_trgm, so GIN cannot accelerate LIKE on a TEXT column; use full-text search over a TSVECTOR column instead.

HNSW indexes carry extra structural requirements: the indexed column must declare its width (VECTOR(1024), not bare VECTOR), the table needs a single-column primary key (a table with no primary key or a composite one fails with 0A000 HNSW indexes require a single-column primary key), and an INTEGER/BIGINT primary key must hold only non-negative values (a negative one at build time gives 22023). UUID and TEXT primary keys are fine. Partial (WHERE) and multi-column HNSW indexes are rejected (0A000). Building over a bare VECTOR column that holds rows drops the connection rather than returning an error — see Vector Search.

Queries fall back to an exact sequential scan when no usable index exists, so all distance operators (<->, <=>, <#>) and functions return correct results either way. See pgvector for details.

GIN indexes on JSONB columns are functional for containment queries (@>), and — as with HNSW — for a literal or a driver-bound parameter alike. A probe computed by an expression is not recognised: @> against jsonb_build_object(...) plans as a Seq Scan with KV Table Scan Pairs equal to the row count, exactly as if the index were not there, and the same holds for array containment against ARRAY[lower(...)] or a subquery — and against ARRAY['t7']::text[], although ARRAY['t7'] and '{t7}'::text[] are indexed. Full-text has one extra trap: @@ is indexed against a tsquery value or a to_tsquery / plainto_tsquery / websearch_to_tsquery call that names its configuration as a literal — a call that omits the configuration or binds it as a parameter is not recognised as a probe, see Full-Text Search. Results stay correct either way, so nothing signals the fallback — check EXPLAIN ANALYZE rather than assuming the index is in play.

FeatureStatus
pgwire v3Supported
Simple Query (text)Supported
Extended Query (Parse, Bind, Describe, Execute)Supported
Binary parameter encodingSupported
COPY (CSV, TEXT)Supported for the COPY <table> [(columns)] form (BINARY format not supported); COPY (SELECT ...) TO STDOUT is rejected
Prepared statementsSupported
PortalsSupported
Multiple result setsSupported
SCRAM-SHA-256 authenticationSupported (at pgwire layer)
LISTEN / NOTIFYSupported (notifications are delivered on commit to sessions holding an open connection; LISTEN requires a persistent pgwire session, so the stateless HTTP SQL API can send NOTIFY but cannot receive)
Logical replication protocolPartial — IDENTIFY_SYSTEM, CREATE_REPLICATION_SLOT ... LOGICAL, START_REPLICATION (for a pgoutput slot) and DROP_REPLICATION_SLOT work; physical slots are rejected, and pg_recvlogical fails before it streams anything. See Logical decoding
Streaming (physical) replicationNot supported

DB9 implements 200+ built-in functions across these categories:

CategoryExamples
Stringupper, lower, concat, substring, replace, trim, split_part, regexp_match, format
Mathabs, ceil, floor, round, sqrt, power, log, random, trunc, trig functions
Date/Timenow, date_trunc, date_part, extract, age, to_char (a fixed set of temporal patterns only — see below), to_timestamp (epoch argument only), make_date
Aggregatecount, sum, avg, min, max, string_agg, array_agg, json_agg, bool_and/or
Windowrow_number, rank, dense_rank, lag, lead, first_value, last_value, ntile
JSON/JSONBjsonb_build_object, jsonb_set, jsonb_extract_path, jsonb_array_elements, jsonb_each, jsonb_typeof, to_jsonb, row_to_json
Arrayarray_length, array_agg, unnest, array_append, array_cat, array_position, string_to_array
Full-text searchto_tsvector, to_tsquery, plainto_tsquery, ts_rank, ts_headline, setweight
UUIDgen_random_uuid, uuid_generate_v4, uuidv7 — the other uuid-ossp generators (uuid_generate_v1, v1mc, v3, v5) and constants (uuid_nil, uuid_ns_*) fail with 42883
Type conversioncast, :: casts, to_char (formatting only), to_timestamp(epoch), to_hex, to_json/to_jsonb
Conditionalcoalesce, nullif, greatest, least
HTTP (scalar)http_get, http_post, http_put, http_delete, http_head, http_patch and the generic http(method, url, ...), returning JSONB
Document chunkingCHUNK_TEXT — table-valued function for RAG pipelines
Storagedb9_refresh_storage_stats — trigger storage scan
Systemcurrent_user, current_database, current_schema, pg_typeof, version
OperatorDescriptionStatus
->Get JSON object field by key (returns JSON)Supported
->>Get JSON object field by key (returns text)Supported
#>Get element by path (returns JSON)Supported
#>>Get element by path (returns text)Supported
@>ContainsSupported
<@Contained bySupported
?Key existsSupported
?|Any key existsSupported
?&All keys existSupported
||ConcatenateSupported
-Delete keySupported
#-Delete pathSupported
FeatureStatus
DECLARE / BEGIN / END blocksSupported
Variable assignment (:=)Supported
SELECT INTOSupported
IF / THEN / ELSIF / ELSE / END IFSupported
FOR loops (query iteration and integer range)Supported
PERFORM (execute without result)Supported
EXIT (loop termination, plain and EXIT WHEN <condition>)Supported
CONTINUE (plain and CONTINUE WHEN <condition>)Supported
RAISE (NOTICE, WARNING, ERROR)Supported
RETURN / RETURN NEXT / RETURN QUERYSupported
RETURNS TABLE syntaxSupported
CASE statements (in PL/pgSQL)Supported
WHILE loopsNot supported
FOREACH … IN ARRAYNot supported — 42601 (syntax error at or near "FOREACH")
Cursor operations (FOR…IN CURSOR)Not supported
Exception handling (BEGIN…EXCEPTION)Supported, in both a CREATE FUNCTION body and a DO block
Dynamic SQL (EXECUTE, including INTO and USING)Supported, in both a CREATE FUNCTION body and a DO block
Nested BEGIN…END blocksSupported, in both hosts — a nested block may not open its own DECLARE section (42601)

A CREATE FUNCTION body and a DO block now support the same feature set, and what one host rejects the other rejects the same way:

FeatureCREATE FUNCTION bodyDO block
CASE statementsSupportedSupported
Exception handling (BEGIN ... EXCEPTION)SupportedSupported
Dynamic SQL (EXECUTE, including INTO and USING)SupportedSupported
Nested BEGIN ... END blocksSupportedSupported
A nested block with its own DECLARE section4260142601
WHILE and FOREACH loops4260142601
Cursor operations42601 / 4270442601 / 42704

EXECUTE takes a literal, a variable, or an expression as its command string, and INTO and USING behave as in PostgreSQL — EXECUTE 'SELECT $1 * 2' INTO n USING p works in both hosts.

Only the outermost block may declare variables. A nested block that opens its own DECLARE section is rejected with 42601 (syntax error at or near "<type>", naming the declared type) in both hosts; hoist those declarations to the top-level DECLARE, which is where PL/pgSQL scoping puts them for the whole routine anyway.

The WHILE and FOREACH errors are raised when the function is called; CREATE FUNCTION itself succeeds. Cursors fail earlier, when the cursor is declared — c CURSOR FOR ... with 42601 (unexpected token in PL/pgSQL type declaration: FOR) and c refcursor with 42704 (type "refcursor" does not exist) — so a function that declares one is never created.

Exception handlers follow PostgreSQL rollback semantics in both hosts: statements that ran before the exception are rolled back, and only the handler’s effects persist.

Multi-line DECLARE entries parse correctly in both hosts: a declaration whose type or value spans several lines yields the same value as PostgreSQL (v int := 1 / + 2; is 3, and v int / := 42; is 42).

Other procedural languages (PL/Python, PL/Perl, PL/v8) are not supported. CREATE FUNCTION with LANGUAGE plpythonu, plperl, plv8, c or any other language DB9 does not have fails with 42704 (language "plpythonu" does not exist) and nothing is added to pg_proc — PostgreSQL raises the same error for a language that is not installed. LANGUAGE internal fails with 0A000 (user-defined routines in language "internal" are not supported). Port such functions to plpgsql or sql.

The following full-suite results were measured on 2026-10-03 against server build 637464c98, running db9-server/orm-tests:

ORM / DriverVersion runResultNotes
node-postgres (pg)8.16104/104Native pgwire client
TypeORM0.3.28202/202None skipped
Knex.js3.1109/109Full query builder, window functions, CTEs
Prisma5.22107/107Binary wire protocol; $queryRaw for advanced SQL
Sequelize6.37104/104Raw queries for window/CTE features
Drizzle0.2993/93Type-safe queries; full query builder support
Kysely0.28, 0.29.750/50CRUD, DDL/transactions, prepared, JSON, vector
SQLAlchemy2.0+TestedJSONB operators, RETURNING, transaction patterns
GORM (Go)1.31TestedCRUD, transactions, foreign keys, AutoMigrate

Additional guide checks on 2026-10-20 used TypeORM 1.1.2, Prisma 6.19.3, Drizzle 0.45.4 (with drizzle-kit 0.31.11), and Sequelize 6.37.8. These covered the documented setup, CRUD and repeated schema-tool runs, including the failures described in each guide; they do not replace the historical full-suite counts above. Use the ORM guides for current examples.

These suites exercise queries at runtime. Schema tooling is a separate question, and it splits — see below.

The dividing line is creating a schema versus reading an existing one back. Creating works almost everywhere; introspection is where DB9 diverges, so several tools succeed once against an empty database and fail on every run after that.

ToolWorksIf not, why
Knex schema builder (createTable/alterTable/columnInfo)Yes—
TypeORM migration runner (runMigrations, undoLastMigration)Yes—
GORM AutoMigrate (including adding columns)Yes—
Sequelize sync() / sync({ force: true })Yes, unless the model has an ENUM columnfor a model with an ENUM column only the first sync() succeeds — see the sync({ alter: true }) row below; sync({ force: true }) keeps working
Kysely schema builder (createTable/alterTable/indexes)Yes—
prisma generateYesneeds no database connection
Alembic / Flask-Migrate upgrade, downgrade, currentYes—
Django makemigrations / migrate (including ALTER)Yes—
Django inspectdbYesmodels are generated for your tables; the output also carries three # Unable to inspect table stubs for pg_cron’s tables, and CharField without max_length — see the Django guide
TypeORM synchronize: trueFirst run onlyevery second run drops the default of each generated id column, so inserts fail with 23502 until the next run restores it — DB9 reports the default as nextval(CAST('…' AS pg_catalog.regclass)), which TypeORM does not recognise as a serial. See the TypeORM guide
prisma db pushFirst run only, if a model has an @default(autoincrement()) idsucceeds on an empty schema. With lower-case table names (@@map), every later run fails with relation "<table>_id_seq" already exists and applies nothing; with Prisma’s default upper-case table names ("User"), the second run exits 0 but re-points id at a new sequence starting at 1, so inserts into a table with rows fail with 23505 — see the Prisma guide. DB9 reports the serial default as nextval(CAST('…' AS pg_catalog.regclass)), which Prisma does not recognise as its own sequence, so it plans to create the sequence again. Models whose ids use @default(uuid()) push repeatedly, including schema changes (measured with Prisma 6.19.3)
drizzle-kit pushFirst run only, into a schema with no tablesthe first push into a schema that has no tables yet creates them. A push into a schema that already has tables — so every push after the first — fails and applies nothing: its introspection reads pg_stat_user_indexes for each existing table, and DB9 does not implement that view. drizzle-kit 0.31.11 exits with status 1 without printing an error; 0.31.8 prints error: relation "pg_stat_user_indexes" does not exist but exits with status 0, so a script that checks the exit code sees success. drizzle-kit generate followed by drizzle-kit migrate works, including a later ADD COLUMN migration (measured with drizzle-kit 0.31.11 and drizzle-orm 0.45.4)
Sequelize sync({ alter: true }), describeTable()Yes, unless the model has an ENUM columnwith an ENUM column these — and every plain sync() after the first — throw TypeError: text.replace is not a function: DB9 returns the enum’s labels as a text[], which Sequelize cannot parse. Only sync({ force: true }) keeps working
Kysely Migrator, introspection.getTables()Noboth fail with 42P01 relation "cron.job" does not exist: the introspection query calls pg_get_serial_sequence() for every table it lists, including pg_cron’s cron.job, and DB9 cannot resolve that call although the table itself is readable (measured with Kysely 0.29.6 and 0.29.7)
Alembic / Flask-Migrate revision --autogenerateWith a table filterruns on psycopg2 and on psycopg (v3), but needs an include_name hook limiting it to your models’ tables, or every migration tries to drop pg_cron’s tables and those of your other schemas — see the SQLAlchemy guide
SQLAlchemy MetaData.reflect(), autoload_withYeson both psycopg2 and psycopg (v3). An unfiltered reflect() also returns pg_cron’s tables and those of your other schemas; pass only=[...] to limit it
prisma db pullYesan @default(autoincrement()) id reads back as @default(dbgenerated("nextval(CAST('public.<table>_id_seq' AS pg_catalog.regclass))")), for the same reason as the db push row (measured with Prisma 6.19.3)
prisma migrate devNoshadow database name contains hyphens, and DB9 rejects any database name containing a hyphen, dot or space with 42602 invalid database name (P3014). With a shadow database whose name has none of those, the first run succeeds and a later one goes wrong like db push: it fails with P3018 (relation "<table>_id_seq" already exists) for lower-case table names, and for upper-case table names it exits 0 and records a migration that re-points id at a new sequence

DB9 implements 50+ virtual tables across pg_catalog, information_schema, and extension schemas.

ViewStatusNotes
pg_classSupportedRelations (tables, views, indexes, sequences)
pg_attributeSupportedColumn definitions
pg_indexSupportedIndex metadata
pg_constraintSupportedConstraints (PK, FK, UNIQUE, CHECK)
pg_typeSupportedData types
pg_procSupportedFunctions and procedures
pg_namespaceSupportedSchemas
pg_roles / pg_userSupportedUsers and roles
pg_databaseSupportedDatabase metadata
pg_sequenceSupportedSequence state
pg_attrdefSupportedColumn defaults
pg_extensionSupportedInstalled extensions
pg_amSupportedAccess methods
pg_triggerSupportedTrigger definitions, including the tgtype bitmask and trigger arguments (tgnargs, tgargs)
pg_depend / pg_descriptionSupportedObject dependencies and comments
pg_settingsSupportedServer and db9.* runtime parameters; category is always NULL and short_desc is sparse
pg_stat_activityNode-local17 columns with live values (state, query, backend_start, wait events), but only for sessions on the server node your connection landed on — not a database-wide list
pg_stat_databasePartial, node-localOne row per database; for the connected database numbackends, transaction and tuple counters are populated — per server node, so two connections can read different totals; block, timing and session counters are NULL
pg_stat_user_tables / pg_stat_all_tablesStubOne row per user table, but every counter is NULL
pg_stat_all_indexes / pg_statio_all_tables / pg_statio_all_indexesStubIdentifying columns only; every counter is NULL
pg_stat_replicationNode-localA client streaming from a replication slot appears only to sessions on the server node it is connected to, so the view is usually empty; read pg_replication_slots.active instead
pg_stat_sslNode-localOne row per session on the server node your connection landed on, with that connection’s real TLS state (ssl, version, cipher, bits)
pg_stat_statementsNot available
pg_sequencesSupportedPostgreSQL 17’s columns, one row per sequence. last_value is NULL until the first nextval(), and with CACHE above 1 it can run ahead of the values handed out so far, as in PostgreSQL
pg_stat_user_indexesNot availabledrizzle-kit push reads this for each existing table, so every push after the first fails. The other _user_ and _sys_ index and I/O views (pg_statio_user_tables, pg_statio_user_indexes, pg_stat_sys_tables, …) are absent too
pg_publicationSupportedCREATE PUBLICATION p;, ... FOR TABLE t [, ...] and ... FOR TABLE ONLY t, with any WITH (publish = ...) value, succeed on both transports and persist here. The publish value is recorded but does not filter what a slot captures. FOR ALL TABLES and FOR TABLES IN SCHEMA are rejected with 0A000. See Logical decoding
pg_publication_relSupportedALTER PUBLICATION ... ADD/DROP/SET TABLE records membership here, and membership decides which tables’ changes a slot captures — except the rows CREATE TABLE ... AS and SELECT ... INTO load, which a wal2json slot receives for a table in no publication, see Logical decoding
pg_publication_namespaceStub (empty)CREATE PUBLICATION ... FOR TABLES IN SCHEMA and ALTER PUBLICATION ... ADD TABLES IN SCHEMA are both rejected with 0A000, so this is never populated
pg_replication_slotsSupportedOne row per logical slot. 19 columns, including conflicting plus the PostgreSQL 17 additions failover/synced (two_phase_at and inactive_since are absent). See Logical decoding
pg_subscriptionStub (empty)18 columns — the PostgreSQL 16 set plus the PostgreSQL 17 addition subfailover. CREATE SUBSCRIPTION is not parsed at all (42601), so this is never populated; pg_subscription_rel does not exist

DB9 can be the source of a change stream: a logical replication slot captures the inserts, updates, deletes and truncates on published tables, and you read them either by polling SQL functions (wal2json slots) or by streaming over the replication protocol (pgoutput slots). DB9 cannot be a logical-replication subscriber — CREATE SUBSCRIPTION is not parsed (42601) — so you cannot replicate from another PostgreSQL server into DB9.

With the SQL functions, the round trip looks like this:

SQL
CREATE TABLE orders (id INT PRIMARY KEY, status TEXT);
CREATE PUBLICATION orders_pub FOR TABLE orders;
SELECT * FROM pg_create_logical_replication_slot('orders_slot', 'wal2json');
INSERT INTO orders VALUES (1, 'new');
UPDATE orders SET status = 'paid' WHERE id = 1;

A few seconds later, read the changes, then drop the slot when you no longer need it:

SQL
SELECT data FROM pg_logical_slot_get_changes('orders_slot', NULL, NULL, 'format-version', '2');
SELECT pg_drop_replication_slot('orders_slot');
DROP PUBLICATION orders_pub;

The pg_logical_slot_get_changes() call returns one row per change:

{"action":"I","columns":[{"name":"id","type":"integer","value":1},{"name":"status","type":"text","value":"new"}],"schema":"public","table":"orders"}
{"action":"U","columns":[{"name":"id","type":"integer","value":1},{"name":"status","type":"text","value":"paid"}],"identity":[{"name":"id","type":"integer","value":1}],"schema":"public","table":"orders"}

This works the same over the PostgreSQL wire protocol and the HTTP SQL API. What to expect:

  • Only wal2json, only format-version 2. Each row has lsn, xid and data, and data is one wal2json version-2 change: action I, U, D or T (truncate), with columns for new values and identity for the replica-identity columns of an update or delete. No transaction begin/commit rows are emitted. pg_logical_slot_get_changes() consumes what it returns; pg_logical_slot_peek_changes() takes the same arguments and leaves it in the slot.
  • Only tables in a publication when the change is written — and the rows CREATE TABLE ... AS loads. A write to a table that is in no publication is never emitted, even if the table is added later. The exception is the rows that CREATE TABLE ... AS or SELECT ... INTO loads into its new table: they are emitted as I changes, _rowid column included, even though the table is in no publication; later writes to that table follow the publication rule. Materialized views are never emitted. The publication’s publish option is not applied: with publish = 'insert', updates and deletes are captured too, so filter on action in the consumer if you need to.
  • A delay of several seconds. A read straight after the write returns no rows; the changes appear a few seconds later.

The errors you can hit:

CallResult
SELECT * FROM pg_create_logical_replication_slot(s, 'pgoutput')22023 only the wal2json output plugin is available through SQL
SELECT * FROM pg_create_logical_replication_slot(s, 'test_decoding')22023 output plugin "test_decoding" is not supported
SELECT * FROM pg_logical_slot_get_changes(s, NULL, NULL) (or ..._peek_changes)22023 wal2json format-version 2 must be requested explicitly
SELECT * FROM pg_logical_slot_get_changes(s, NULL, NULL, 'format-version', '1')22023 only wal2json format-version 2 is supported
SELECT pg_drop_replication_slot(s) for a slot that does not exist42704 replication slot "<s>" does not exist on a provisioned database; 55000 logical replication is not provisioned for this database if provisioning has not completed

Each slot function resolves in only one call position and reports 42883 does not exist in the other:

FunctionSELECT f(...)SELECT * FROM f(...)
pg_create_logical_replication_slot(s, plugin)42883 does not existworks
pg_logical_slot_get_changes(s, ...)42883 does not existworks
pg_logical_slot_peek_changes(s, ...)42883 does not existworks
pg_drop_replication_slot(s)works42883 does not exist
pg_create_physical_replication_slot(...)4288342883 — genuinely absent

PostgreSQL accepts either position for all of these, so the idiomatic SELECT pg_create_logical_replication_slot('s', 'wal2json') is exactly the spelling that misleadingly answers “does not exist”. Which position works is per-function and not predictable from whether the function is set-returning; separately, most scalar functions cannot be called in FROM position at all — see call position.

Over the replication protocol, a connection made with replication=database is accepted. IDENTIFY_SYSTEM answers with a system identifier, timeline and LSN; CREATE_REPLICATION_SLOT <name> LOGICAL pgoutput creates a slot that START_REPLICATION streams, and DROP_REPLICATION_SLOT removes it. Measured with psycopg2’s LogicalReplicationConnection and the options proto_version '1' and publication_names, inserts, updates and deletes arrived as begin / relation / change / commit messages within about a second. A wal2json slot cannot be streamed (55000 replication slot "<name>" does not use pgoutput), and a pgoutput slot cannot be read with the SQL functions (55000 replication slot "<name>" does not use wal2json). Physical slots are rejected. pg_recvlogical fails before it streams anything, at SHOW data_directory_mode (unrecognized configuration parameter "data_directory_mode"), so use a client library’s replication API. Right after a streaming client disconnects, the slot can still be reported as active, and dropping it fails with 55006 until it is released; retry the drop a little later.

pg_stat_replication is node-local: a streaming client appears only to sessions on the server node it is connected to, so the view is usually empty; read pg_replication_slots.active instead.

ALTER PUBLICATION ... ADD TABLES IN SCHEMA is rejected with 0A000 only ALTER PUBLICATION table membership is supported, and two CREATE PUBLICATION shapes are syntax errors:

CREATE PUBLICATION p FOR TABLE t (id); -- column list
ERROR: syntax error at or near "(id)" (42601)
CREATE PUBLICATION p FOR TABLE t WHERE (id > 0); -- row filter
ERROR: syntax error at or near "WHERE" (42601)

ALTER TABLE ... REPLICA IDENTITY is accepted — DEFAULT, FULL, NOTHING and USING INDEX are all recorded in pg_class.relreplident — and enforced as in PostgreSQL. With FULL, the identity of an update carries every old column value. On a table that belongs to a publication, REPLICA IDENTITY NOTHING makes UPDATE and DELETE fail:

ERROR: cannot update table "public.t" because it does not have a replica identity and publishes updates (55000)

INSERT still works, as do UPDATE and DELETE once the table is removed from the publication or its identity is set back to DEFAULT or FULL. A table that is in no publication is unaffected by NOTHING. USING INDEX does not accept the table’s primary-key index (42704 index "<table>_pkey" for table does not exist); name a separately created unique index instead.

A dump that carries publications restores unchanged. The sequence pg_dump emits for a published, RLS-protected table —

SQL
CREATE TABLE t (id integer NOT NULL, v text);
ALTER TABLE t ENABLE ROW LEVEL SECURITY;
ALTER TABLE ONLY t FORCE ROW LEVEL SECURITY;
CREATE PUBLICATION p WITH (publish = 'insert, update, delete, truncate');
ALTER PUBLICATION p ADD TABLE ONLY t;

— runs clean end to end. What needs checking before you cut over is each consumer of those publications, against the behaviour above: polling a wal2json slot through SQL, and streaming a pgoutput slot with proto_version '1', work; pg_recvlogical, test_decoding and physical slots do not, and DB9 cannot subscribe to another server. Creating the first slot also turns on Realtime capture, with the restrictions in the caution above.

ViewStatus
columnsSupported
tablesSupported
schemataSupported
table_constraintsSupported
key_column_usageSupported
referential_constraintsSupported
check_constraintsSupported
constraint_column_usageSupported
table_privilegesSupported
sequencesSupported
routinesSupported
triggersSupported
viewsSupported

The views above are the complete information_schema surface in DB9.

DB9 includes 11 built-in extension descriptors — 8 functional extensions plus hstore, pgcrypto and plpgsql, which are metadata-only shims. None of the three registers a function of its own, and the hstore type that hstore adds to pg_type cannot be used, though PL/pgSQL itself is compiled into the server and works with or without CREATE EXTENSION plpgsql. Custom or third-party extensions cannot be installed.

ExtensionVersionDefaultDescription
http1.0.0YesHTTP client (GET, POST, PUT, DELETE, PATCH, HEAD)
pg_cron1.0.0YesJob scheduler with cron expressions. Scheduling is disabled in the current release — cron.schedule() returns feature "cron" is unavailable (PreActivationSeal) (55000). Catalog views and cron.unschedule / cron.cancel resolve. See pg_cron
fs91.0.0NoFile system operations (read, write, list, glob)
vector0.8.1Nopgvector-compatible vector type and HNSW indexes
embedding1.0.0NoBuilt-in text embedding generation
uuid-ossp1.1Nouuid_generate_v4() only — a built-in, so it works with or without CREATE EXTENSION. uuid_generate_v1(), uuid_generate_v1mc(), uuid_generate_v3(), uuid_generate_v5(), uuid_nil() and uuid_ns_dns() / uuid_ns_url() fail with 42883
hstore1.0NoMetadata shim only — DB9 does not implement hstore values, so any use of the type fails with 0A000 (type "hstore" is not supported), installed or not; use JSONB. While it is registered, pg_dump cannot dump the database. See hstore
parquet1.0.0NoParquet file import
zhparser2.0.0NoChinese full-text search tokenizer
pgcrypto1.3NoMetadata shim only — registers no functions (see below)
plpgsql1.0NoMetadata shim; PL/pgSQL itself is compiled in

CREATE EXTENSION pgcrypto is accepted so Supabase and ORM bootstrap scripts run unchanged, but it provides no functions. crypt(), gen_salt(), hmac(), encrypt()/decrypt(), pgp_sym_encrypt() and gen_random_bytes() all fail with 42883. gen_random_uuid() and digest() work, but they are DB9 built-ins and need no extension.

Extensions not available: PostGIS, pg_partman, pg_stat_statements, pg_trgm, ltree, citext, and all other PostgreSQL contrib extensions. These raise 42704 extension is not available at CREATE EXTENSION time.

These PostgreSQL features are not available in DB9:

FeatureCategory
MERGE (0A000 MERGE is not supported) — write upserts with INSERT ... ON CONFLICTDML
LIST and HASH partitioning, and RANGE partitioning on any key other than timestamp / timestamptzDDL
Table inheritanceDDL
Foreign data wrappers (FDW)DDL
TablespacesDDL
Rules (CREATE RULE)DDL
Logical replication into DB9 (CREATE SUBSCRIPTION is unparsed). DB9 can be a source — see Logical decodingReplication
Physical (streaming) replication and physical replication slotsReplication
SERIALIZABLE isolation (true serializable)Transactions
DEFERRABLE transactionsTransactions
Large objects (OID-based)Data
XML typeData
Network types (CIDR, MACADDR)Data
Range types (except a partial INT4RANGE)Data
PL/Python, PL/Perl, PL/v8Languages
PL/pgSQL WHILE and FOREACH loops, and cursorsLanguages
Custom extensionsExtensions
pg_dump of a database that has a partitioned table or a registered hstore extension, and parallel pg_dump -j (pg_export_snapshot() does not exist). Other databases dump serially, in every format, with pg_dump 17 or newer and -N extensions — see the rollback plan for the flags and for vector columnsTools
pg_basebackupTools

You can verify compatibility for your specific use case:

SQL
-- List the types the engine knows about. This is NOT the list of types you can
-- declare a column with. Many of the names it returns are rejected by
-- CREATE TABLE: every array alias (`_int4`, `_text`, ...), the pseudo-types
-- (`any`, `anyarray`, `record`, `trigger`, `cstring`, `void`, ...), several
-- internal types (`xid`, `cid`, `tid`, `pg_lsn`, `pg_node_tree`), and `xml`,
-- which is listed here but not supported at all. Membership is not a
-- reliable signal in either direction — some equally internal-looking names
-- (`oid`, `regclass`, `aclitem`, even the pseudo-type `anyelement`) ARE
-- accepted. Use the Data Types tables above as the authoritative list, and
-- confirm any individual type with a throwaway CREATE TABLE.
SELECT typname FROM pg_type WHERE typnamespace = 11 ORDER BY typname;
-- Check available extensions
SELECT * FROM pg_extension;
-- Check installed functions ('f' = function, 'a' = aggregate, 'w' = window).
-- Note: pg_proc lists only catalog-registered functions. Many built-ins (UNNEST,
-- ARRAY_AGG, MIN, MAX, ...) run in the executor and never appear here — see /docs/sql/catalog/
SELECT proname, prokind FROM pg_proc WHERE pronamespace = 11 ORDER BY proname;
-- Check catalog coverage. pg_tables lists user schemas only, so query pg_class
-- to enumerate the pg_catalog relations:
SELECT n.nspname AS schema, c.relname AS catalog_table
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'pg_catalog'
ORDER BY c.relname;
-- Verify transaction isolation
SHOW transaction_isolation; -- returns 'read committed' (the default)
-- SERIALIZABLE is not implemented. On the PostgreSQL wire protocol it is accepted
-- with a warning and downgraded; on the HTTP SQL API it returns an error.
-- SET TRANSACTION only takes effect inside a transaction block — issued standalone it
-- warns 25P01 "SET TRANSACTION can only be used in transaction blocks" and changes nothing:
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- WARNING: TiKV provides snapshot isolation; SERIALIZABLE has been downgraded to REPEATABLE READ
SHOW transaction_isolation; -- repeatable read
COMMIT;