A PostgreSQL-compatible wire protocol for the MS Access / Jet thin TCP endpoint, incorporating MariaDB features where the Jet engine supports them.
The JET wire protocol adopts PostgreSQL's v3 message-oriented structure as its foundation: every message is a single type byte followed by a 4-byte big-endian length (including the length word itself), then the payload. This framing is simple, self-describing, and used identically by both sides — client to server and server to client.
Where PostgreSQL's protocol defines features the Jet/ACE engine cannot support (parse trees, portals, COPY, LISTEN/NOTIFY, logical replication), those message types are omitted or repurposed. Where MariaDB's protocol adds capabilities that are relevant to Jet (multi-statement batches, connection-level statistics, mid-session user switching, compression), those are incorporated as extension messages.
PostgreSQL uses ASCII characters as message type identifiers. JET follows the same convention, with the letter J (0x4A) reserved as a prefix byte for JET-specific extension messages that have no PostgreSQL equivalent.
| Range | Usage |
|---|---|
A..Z, a..z | Standard messages (compatible with PostgreSQL v3 where applicable) |
J + second byte | JET extension messages (two-byte type: J prefix + subtype). Examples: JS = Statistics, JU = ChangeUser, JM = MultiStatement |
PostgreSQL assumes a single message fits in one TCP segment (practical limit ~1 GB with int32 length). MySQL's protocol handles >16 MB payloads by splitting across packets with a sequence number. JET adopts PostgreSQL's single-message model — the int32 length field allows up to 2 GB per message, more than sufficient for Jet/ACE result sets. No multi-packet splitting is needed.
Identical to PostgreSQL. The client sends an 8-byte SSLRequest message (int32 length=8, int32 magic=80877103). The server responds with S to upgrade or N to refuse. On S, a TLS handshake begins immediately.
Identical to PostgreSQL's v3 StartupMessage. Protocol version 3.0 (int32 = 196608). Key-value pairs follow: user (required), database (path to .accdb/.mdb file, required), dbpassword (optional — .accdb/.mdb file password, tried only if the database is password-protected), options (Jet-specific: --jet-exclusive=false, --jet-systemdb=path).
JET supports three authentication methods, using PostgreSQL's AuthenticationRequest message format (int32 type code):
| Code | Method | DAO Mapping |
|---|---|---|
| 0 | AuthenticationOk | Proceed to ReadyForQuery |
| 3 | AuthenticationCleartextPassword | Client sends password in cleartext; server validates against workspace user list or Jet system database |
| 10 | AuthenticationSASL (SCRAM-SHA-256) | Full SCRAM exchange (same as PG). Server stores SCRAM verifiers per user. |
Jet/ACE's native user-level security (workgroup file, user/group permissions) maps to the wire protocol's user identity. The Workspace is created with that user name:
After authentication, the server sends ParameterStatus messages for server metadata. Standard PG keys plus JET-specific keys:
| Key | Value |
|---|---|
server_version | "16.0.0" |
server_version_num | "160000" |
server_encoding | "UTF8" |
client_encoding | "UTF8" |
DateStyle | "ISO, MDY" |
integer_datetimes | "on" |
standard_conforming_strings | "on" |
jet_version | "ACE 16.0.0" |
jet_database | Full path to the open .accdb/.mdb file |
jet_collating_order | Active collating order (e.g. "General", "General_CI_AS") |
jet_ansi_mode | "on" or "off" (ANSI-92 Query Mode) |
Identical to PostgreSQL's simple query flow: client sends Query, server executes it, sends back result sets and ReadyForQuery. A single Query message may contain multiple SQL statements separated by semicolons — the server processes each in sequence, sending a result descriptor + rows (or CommandComplete) for each, then a single ReadyForQuery at the end.
The JET endpoint accepts Jet SQL (Access SQL). The worker does not translate between dialects. A PostgreSQL psql client connecting to this endpoint must send Jet SQL, not PostgreSQL SQL. See §8 for schema introspection helpers that bridge the gap.
SELECT without FROM: Jet SQL requires a FROM clause. SELECT 1 or SELECT 1, 2 (valid in PostgreSQL/MySQL) may return 0 rows or fail entirely under DAO. Use SELECT … FROM tablename for queries.SELECT TOP N not LIMIT: Jet uses SELECT TOP 10 * FROM t; PostgreSQL's SELECT … LIMIT 10 is not recognised.? (positional), not $1. PG $N syntax is not supported by the Jet engine.& not ||. Jet uses SELECT FirstName & ' ' & LastName.* and % (LIKE) not PostgreSQL's .* regex operators.schema.table qualification. Use bare SELECT * FROM Customers.Now(), IIF(), NZ(), DateValue()) vs PG equivalents. No user-defined functions in SQL; use DAO QueryDef objects (see §5).CREATE TABLE, ALTER TABLE, DROP TABLE) but syntax differs from PG. See Microsoft Jet SQL Reference.BeginTrans/CommitTrans/Rollback in DAO, or BEGIN TRANSACTION / COMMIT / ROLLBACK in Jet SQL. See §7.4 for protocol-level alternatives.| PG Message | Content | DAO Source |
|---|---|---|
RowDescription (0x54) | Int16 column count, then per col: name (string), table OID (0), column attr (int16), type OID (int32), type size (int16), type modifier (int32), format code (int16=0 for text) | rs.Fields(i).Name, rs.Fields(i).Type mapped to PG OID (see §6), rs.Fields(i).Size |
DataRow (0x44) | Int16 column count, then per col: int32 length + value (or -1 for NULL) | rs.Fields(i).Value formatted as text; IsNull(rs.Fields(i).Value) → length = -1 |
CommandComplete (0x43) | Tag string: "SELECT N" or "INSERT 0 M" etc. | rs.RecordCount (for SELECT), database.RecordsAffected (for DML) |
EmptyQueryResponse (0x49) | Sent when the SQL string is empty | — |
ReadyForQuery (0x5A) | 1-byte transaction state: I (idle), T (in transaction), E (failed) | Tracked from BeginTrans/CommitTrans/Rollback state |
PostgreSQL's extended query protocol uses a three-phase model: Parse (prepare a statement), Bind (supply parameters), Execute (run it). DAO supports this through its QueryDef object, which is a saved or temporary parameterized query. JET maps the PG extended protocol onto QueryDef operations.
Statement name maps to a DAO QueryDef object. An unnamed statement ("") creates a temporary QueryDef scoped to the current parse cycle. Named statements persist in a per-connection statement cache — the DAO equivalent of PostgreSQL's prepared statement hash table.
Portal maps to an open DAO Recordset. The portal name identifies an in-progress cursor. Only a single portal is active at a time (matching DAO's limitation — a QueryDef can only have one open Recordset).
DAO QueryDefs support named parameters using the PARAMETERS declaration or positional ? placeholders. JET uses the MariaDB-style positional parameter model (matching ? placeholders) rather than PostgreSQL's $1 syntax, because Jet SQL uses ?:
Unlike PostgreSQL's extended query protocol, which sends parameter types in the Parse message, JET infers parameter types from the SQL (the Jet engine does this internally when the QueryDef is created). The client may still send type OIDs in the Parse message for forward compatibility, but the server ignores them.
| Message | Type | DAO Action |
|---|---|---|
Parse | P | database.CreateQueryDef("") → set .SQL = sql → store in cache |
Bind | B | Set qd.Parameters(i).Value for each bound parameter |
Describe (S) | D | Open a dummy Recordset (WHERE 1=0) to get column metadata without fetching rows |
Describe (P) | D | Return RowDescription for the portal's current result shape |
Execute | E | qd.OpenRecordset() (SELECT) or qd.Execute() (DML) |
Close (S) | C | qd.Close() — release cached QueryDef |
Close (P) | C | rs.Close() — release portal Recordset |
Sync | S | Commit any pending work; send ReadyForQuery |
Flush | H | Flush server output buffer to client (identical to PG) |
The protocol uses PostgreSQL OIDs in RowDescription messages so that standard PG clients can interpret column types. The worker thread maps DAO DataTypeEnum values to the closest PG type OID.
| DAO DataTypeEnum | Value | PG OID | PG Type Name | Notes |
|---|---|---|---|---|
dbBoolean | 1 | 16 | bool | |
dbByte | 2 | 21 | int2 | 0–255 |
dbInteger | 3 | 23 | int4 | ±2^15 (Access Integer = 16-bit) |
dbLong | 4 | 23 | int4 | ±2^31 (Access Long Integer = 32-bit) |
dbCurrency | 5 | 790 | money | Fixed-point, 4 decimal places |
dbSingle | 6 | 700 | float4 | |
dbDouble | 7 | 701 | float8 | |
dbDate | 8 | 1082 | date | |
dbTime | 22 | 1083 | time | |
dbBinary | 9 | 17 | bytea | Hex-encoded in text mode |
dbText | 10 | 25 | text | |
dbMemo | 12 | 25 | text | Memo fields map to text (PG has no 64K limit) |
dbLongBinary | 11 | 17 | bytea | OLE Object / Attachment binary |
dbGUID | 15 | 2950 | uuid | Replication ID / GUID |
dbBigInt | 16 | 20 | int8 | ACE only (Access 2007+) |
dbDecimal | 20 | 1700 | numeric | ACE only |
dbVarBinary | 204 | 17 | bytea | ACE only |
dbDateTimeExt | 101 | 1114 | timestamp | ACE extended datetime |
These messages use the J prefix byte followed by a subtype character. They have no PostgreSQL equivalent but add capabilities relevant to the Jet engine or to database administration.
Inspired by: MariaDB's COM_STATISTICS. Returns server status information.
Inspired by: MariaDB's COM_CHANGE_USER. Resets the connection state and re-authenticates as a different user, without closing the TCP connection.
Inspired by: MariaDB's COM_SET_OPTION with MYSQL_OPTION_MULTI_STATEMENTS_ON. Toggles whether a single Query message may contain multiple SQL statements separated by semicolons.
While PostgreSQL and MariaDB accept BEGIN/COMMIT/ROLLBACK as SQL statements, Jet SQL has its own transaction syntax (BeginTrans/CommitTrans/Rollback in DAO, or BEGIN TRANSACTION in Jet SQL). JET provides these as protocol-level messages for completeness and to avoid dialect confusion.
Inspired by: PostgreSQL's no mid-session database switch (must reconnect) vs. MySQL's USE database. JET provides this as a protocol message since Jet/ACE can have multiple open databases per workspace.
Inspired by: MariaDB's COM_PING. Checks if the connection is alive without executing a query.
Inspired by: MariaDB's compressed protocol (mysql_compress packet header). Toggles zlib compression on the connection.
PostgreSQL clients expect to discover schema by querying information_schema or pg_catalog. Jet/ACE has no equivalent system catalog. However, DAO provides collections that expose the same data. The worker thread intercepts specific SQL patterns and returns results from DAO collections rather than executing the SQL against Jet.
| Client SQL | Worker Action (DAO) |
|---|---|
SELECT * FROM information_schema.tables WHERE table_schema = 'public' | Iterate database.TableDefs; filter out system tables (names starting with MSys or ~); return name, table_type |
SELECT * FROM information_schema.columns WHERE table_name = 'X' | Iterate database.TableDefs("X").Fields; return name, data_type, character_maximum_length, is_nullable, ordinal_position |
SELECT * FROM pg_catalog.pg_tables | Same as information_schema.tables above |
SELECT * FROM pg_catalog.pg_class | Return minimal stub: only entries for user tables, with relname, relkind='r' |
SELECT * FROM pg_catalog.pg_attribute WHERE attrelid = ... | Return column metadata from TableDef.Fields |
SELECT * FROM pg_catalog.pg_type | Return the type mapping table from §6 above as a static result set |
SELECT current_database() | Return the database path from the open Database object |
SELECT version() | Return the jet_version ParameterStatus string |
SHOW search_path / SHOW all / any SHOW | Return ParameterStatus values (PG-compatible SHOW) |
SELECT * FROM MSysObjects WHERE type=1 | Pass through to Jet (direct system table access if permissions allow) |
DAO's QueryDefs collection contains saved SELECT queries, action queries, and parameter queries. These are exposed through information_schema.views and can be queried like tables:
JET uses PostgreSQL's ErrorResponse and NoticeResponse message formats. The field type codes are identical to PG's.
| Field Code | Meaning | Source |
|---|---|---|
S | Severity | "ERROR", "FATAL", "PANIC" |
C | Code (SQLSTATE) | Mapped from Jet error numbers (see below) |
M | Message | Err.Description from DAO Errors collection |
D | Detail | Additional DAO error info (if multiple errors in collection) |
H | Hint | Server-generated hint (e.g. "Check that the table exists") |
P | Position | Character position in the SQL where the error was detected |
R | Routine | DAO method that triggered the error (e.g. "OpenRecordset") |
| Type | Name | Direction | Description |
|---|---|---|---|
| Connection Phase | |||
— | SSLRequest | C→S | 8-byte SSL negotiation request |
— | StartupMessage | C→S | Protocol version + key-value params |
R | AuthenticationRequest | S→C | Auth method + challenge data |
p | AuthenticationResponse | C→S | Password / SASL response |
S | ParameterStatus | S→C | Server parameter key=value |
K | BackendKeyData | S→C | Process ID + secret key |
Z | ReadyForQuery | S→C | Transaction state byte (I/T/E) |
| Simple Query | |||
Q | Query | C→S | SQL string |
T | RowDescription | S→C | Column metadata for result set |
D | DataRow | S→C | One row of data |
C | CommandComplete | S→C | Completion tag |
I | EmptyQueryResponse | S→C | Empty SQL string received |
| Extended Query | |||
P | Parse | C→S | Statement name + SQL + param types |
B | Bind | C→S | Portal + statement + params + formats |
E | Execute | C→S | Portal + max rows |
D | Describe | C→S | Portal or statement descriptor request |
C | Close | C→S | Close portal or statement |
S | Sync | C→S | Commit pending; return to ready state |
H | Flush | C→S | Flush server output buffer |
1 | ParseComplete | S→C | |
2 | BindComplete | S→C | |
3 | CloseComplete | S→C | |
s | PortalSuspended | S→C | Execute reached max_rows before EOF |
n | NoData | S→C | Describe on a non-SELECT statement |
| Error & Notice | |||
E | ErrorResponse | S→C | Error with field codes |
N | NoticeResponse | S→C | Warning/info with field codes |
| Copy (Stub — not supported by Jet) | |||
G | CopyInResponse | S→C | Always returns ErrorResponse ("COPY not supported") |
H | CopyOutResponse | S→C | Always returns ErrorResponse |
| Termination | |||
X | Terminate | C→S | Client disconnecting |
| Cancel | |||
— | CancelRequest | C→S | Sent on separate connection; 16-byte message |
| JET Extensions (J-prefix, two bytes) | |||
JS | JET_STATISTICS | C→S | Request server statistics |
JU | JET_CHANGE_USER | C→S | Switch user mid-session |
JM | JET_MULTI_STATEMENT | C→S | Toggle multi-statement mode |
JB | JET_BEGIN_TRANS | C→S | Begin transaction |
JC | JET_COMMIT | C→S | Commit transaction |
JR | JET_ROLLBACK | C→S | Rollback transaction |
JD | JET_SET_DATABASE | C→S | Switch active database |
JP | JET_PING | C→S | Connection keepalive |
JZ | JET_COMPRESS | C→S | Toggle compression |
poll()/select() for new connections, accept(), spawns worker thread per connection. Never touches DAO.CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), creates Workspace + Database, runs the protocol message loop. One connection = one thread = one COM apartment = one Workspace.CancelRequest messages. Looks up the target backend by PID + secret key, signals the worker thread to abort the current operation.Named statements (from Parse with a non-empty statement name) are stored in a per-connection hash map: name → QueryDef object. The unnamed statement ("") is a single slot reused by each Parse. The cache is cleared on Sync with an error, on JET_CHANGE_USER, and on disconnect.
The ReadyForQuery transaction indicator is derived from DAO's transaction state:
workspace.BeginTrans() has not been called, or the last CommitTrans/Rollback closed the outermost level.BeginTrans called, not yet committed or rolled back. PG clients will not auto-commit in this state.Rollback is permitted until the error is cleared.DAO's DBEngine.Errors collection may contain multiple errors for a single operation. The first error (Errors(0)) is the primary error and maps to the wire protocol's ErrorResponse. Subsequent errors are concatenated into the Detail field. Example:
PostgreSQL's extended query protocol supports Execute with a max_rows parameter to limit rows per fetch. JET adopts this directly — the worker calls rs.MoveNext() up to max_rows times, then sends PortalSuspended. The client can re-Execute to fetch the next batch.
For the simple query protocol, JET sends all rows at once. For very large result sets, the worker should stream: send RowDescription, then send DataRow messages in batches of 100, flushing the socket buffer between batches. This prevents the worker from buffering the entire result set in memory.
dbOpenForwardOnly), sends RowDescription immediately, then iterates rs.MoveNext() → build DataRow → send() directly. The Recordset cursor and the socket write buffer advance in lockstep. No client-visible pagination — just TCP-level backpressure.
| Feature | PG v3 | MariaDB | JET | Notes |
|---|---|---|---|---|
| Message framing | ✓ | ✓ (different) | ✓ (PG style) | Type byte + int32 length |
| SSL/TLS | ✓ | ✓ | ✓ | PG-style SSLRequest |
| SCRAM-SHA-256 | ✓ | — | ✓ | |
| Cleartext password | ✓ | ✓ | ✓ | |
| Simple query | ✓ | ✓ | ✓ | |
| Multi-statement | ✓ (in Query) | ✓ (toggle) | ✓ (both) | Default on; JET_MULTI_STATEMENT toggle |
| Extended query | ✓ | ✓ (different) | ✓ (PG style) | Parse/Bind/Execute → QueryDef |
| Named statements | ✓ | ✓ | ✓ | Cached QueryDefs |
| Portals / cursors | ✓ | — | ✓ (single) | Max 1 portal per connection |
| Binary result format | ✓ | ✓ | — (text only) | DAO provides values as variants; text is simplest |
| COPY | ✓ | — | — | Jet has no COPY equivalent; stub error |
| LISTEN/NOTIFY | ✓ | — | — | Jet has no pub/sub; stub error |
| Pipeline mode | ✓ | — | — | Could be added; not in v1 |
| Cancel | ✓ | ✓ | ✓ | Separate cancel connection |
| Change user | — | ✓ | ✓ | JET_CHANGE_USER |
| Statistics | — | ✓ | ✓ | JET_STATISTICS |
| Ping | — | ✓ | ✓ | JET_PING |
| Mid-session DB switch | — | ✓ (USE) | ✓ | JET_SET_DATABASE |
| Compression | — | ✓ | ✓ | JET_COMPRESS (zlib) |
| information_schema | ✓ | ✓ (different) | ✓ (emulated) | Mapped from DAO collections |
| pg_catalog | ✓ | — | ✓ (stub) | Minimal pg_class/pg_attribute/pg_type |