src/sydra/compat/wire/server.zig
Purpose
Provides a preview PostgreSQL wire-protocol (pgwire) listener suitable for basic compatibility testing with clients like psql.
The server:
- accepts a TCP connection
- performs a minimal startup handshake
- supports simple queries plus a preview extended-query path
- translates SQL → sydraQL and executes it via the regular query pipeline
- writes results as
RowDescription+DataRowmessages
See also
- wire protocol (message framing + startup/response writers)
- wire session (handshake + session config)
- wire re-exports
- SQL → sydraQL translator
- sydraQL execution entrypoint
- Reference: PostgreSQL Compatibility
Public API
pub const ServerConfig
address: []const u8 = "127.0.0.1"port: u16 = 6432session: session_mod.SessionConfig = .{}engine: *engine_mod.Engine
pub fn run(alloc, config) !void
- Listens on
address:portwithreuse_address = true. - Runs an accept loop; each connection is handled synchronously via
handleConnection.
pub fn handleConnection(alloc, connection, session_config, engine) !void
- Wraps the socket in buffered reader/writer states.
- Calls
session_mod.performHandshake. - On success enters
messageLoop.
Frontend message support
messageLoop reads:
type_byte: u8message_length: u32be(includes the 4-byte length field)payload: message_length - 4bytes
It enforces:
message_length >= 4payload_len <= 16 MiB(max_message_size)
Handled message types:
'X'– Terminate: close the connection.'Q'– Simple Query: handled byhandleSimpleQuery(SQL→sydraQL→execute).'P'– Parse: prepares a narrow direct SQL-core statement shape for the preview extended path.'B'– Bind: binds text parameters to a parsed statement and creates a portal.'D'– Describe: describes a prepared statement or portal.'E'– Execute: runs a bound portal, streaming rows or a command tag.'C'– Close: closes a prepared statement or portal.'H'– Flush: drains buffered backend output.'S'– Responds withReadyForQuery('I')(acts as a simple sync/flush).- Anything else:
ErrorResponse("0A000", "message type not implemented")ReadyForQuery('I')
messageLoop dispatch (excerpt)
switch (type_byte) {
'X' => return,
'Q' => {
try handleSimpleQuery(alloc, writer, payload_storage, engine);
},
'P' => {
try handleParseMessage(alloc, writer, payload_storage, engine, &extended_state);
},
'B' => {
try handleBindMessage(alloc, writer, payload_storage, engine, &extended_state);
},
'D' => {
try handleDescribeMessage(writer, payload_storage, &extended_state);
},
'E' => {
try handleExecuteMessage(alloc, writer, payload_storage, &extended_state);
},
'C' => {
try handleCloseMessage(writer, payload_storage, &extended_state);
},
'S' => {
try protocol.writeReadyForQuery(writer, 'I');
},
else => {
try protocol.writeErrorResponse(writer, "ERROR", "0A000", "message type not implemented");
try protocol.writeReadyForQuery(writer, 'I');
},
}
Simple Query execution
fn handleSimpleQuery(alloc, writer, payload, engine) !void
Behavior:
- Trims a trailing NUL byte from
payload(C-string style). - Trims whitespace.
- If empty:
- writes
EmptyQueryResponsethenReadyForQuery('I')
- writes
- Otherwise:
- first attempts the SQL-core prepared path
- on direct-handle success: returns after emitting
execution_mode=sql_core_vm legacy_fallback=false - on fallback to translator:
- emits normalized notices:
execution_mode=translator legacy_fallback=truefallback_reason=...
- also emits the preview-specific notice
sql_prepare_fallback=translator reason=...
- emits normalized notices:
- on direct-handle success: returns after emitting
- calls
translator.translate(alloc, sql)(see SQL → sydraQL translator)- on OOM:
ErrorResponse(FATAL, 53100, "out of memory during translation")
- on OOM:
- on translation success:
- calls
handleSydraqlQuery(…, sydraql)
- calls
- on translation failure:
- writes
ErrorResponse(ERROR, failure.sqlstate, failure.message or "translation failed")
- writes
- first attempts the SQL-core prepared path
- always ends with
ReadyForQuery('I')
SQL → sydraQL translation (excerpt)
const translation = translator.translate(alloc, trimmed) catch |err| switch (err) {
error.OutOfMemory => {
try protocol.writeErrorResponse(writer, "FATAL", "53100", "out of memory during translation");
try protocol.writeReadyForQuery(writer, 'I');
return;
},
};
switch (translation) {
.success => |success| {
defer alloc.free(success.sydraql);
try handleSydraqlQuery(alloc, writer, engine, success.sydraql);
try protocol.writeReadyForQuery(writer, 'I');
return;
},
.failure => |failure| {
const msg = if (failure.message.len == 0) "translation failed" else failure.message;
try protocol.writeErrorResponse(writer, "ERROR", failure.sqlstate, msg);
try protocol.writeReadyForQuery(writer, 'I');
},
}
Extended protocol (preview)
fn handleParseMessage(...) !void
- Parses the frontend
Parsemessage fields (statement_name, SQL text, parameter slots/OIDs). - Rejects SQL text longer than
65536bytes with stable54000program-limit errors before prepare. - Attempts
prepared_query.prepareSqlCoreagainst the current engine. - Stores prepared statement state in
ExtendedQueryState. - On unsupported shapes, returns preview errors such as
0A000and leaves the session usable.
fn handleBindMessage(...) !void
- Resolves the named prepared statement from
ExtendedQueryState. - Accepts text parameters only and binds them positionally.
- Creates or replaces a named portal.
fn handleDescribeMessage(...) !void
- Describes either a prepared statement (
'S') or portal ('P'). - Uses the same default column/type mapping as the simple-query path.
fn handleExecuteMessage(...) !void
- Executes a bound portal, streaming
DataRowmessages until completion or suspension. - Emits
CommandCompletefor completed portals andPortalSuspendedwhenmax_rowsis hit.
fn handleCloseMessage(...) !void
- Closes a prepared statement or portal and releases the associated state.
SydraQL execution + result encoding
fn handleSydraqlQuery(alloc, writer, engine, sydraql) !void
Execution:
- Calls
query_exec.executeto create anExecutionCursor. - Streams:
RowDescription(even for zero columns; thencolumns.lenis0)DataRowfor each row returned bycursor.next()
Diagnostics:
- On direct sydraQL execution failures, the current stable pgwire contract is:
42601for syntax errors22023for validation failures and ambiguous selectors0A000for compiler-deferred unsupported shapesXX000for shadow mismatches and uncategorized runtime failures
- Selector
tag_filtersyntax on theFROMselector remains outside thev0.4.0preview contract. The supported path isWHERE tag.<k> ...; compiler fallback reportsunsupported_tag_filterwhen that older selector form is encountered. - Collects operator stats via
cursor.collectOperatorStats. - Computes:
rows_emitted(from stream count)rows_scanned(sum of operatorrows_outwhere operator name is"scan", case-insensitive)stream_msfrom wall timeplan_msfromcursor.stats.{parse,validate,optimize,physical,pipeline}_us
- Emits
NoticeResponsemessages:schema=[{name:\"...\",type:\"...\",nullable:true}, ...](for non-empty schemas)trace_id=...(whencursor.stats.trace_idis present)execution_mode=... legacy_fallback=...fallback_reason=...(when execution fell back). Current stable values mirror the compiler taxonomy:unsupported_statementunsupported_fillunsupported_tag_filterunsupported_groupingunsupported_aggregateunsupported_projectionunsupported_orderingunsupported_predicateunsupported_expressionunsupported_functionseries_not_foundambiguous_selectorshadow_mismatch
operator=... rows_out=... elapsed_ms=...for each operator stat
- Completes with:
CommandCompletetagSELECT rows=… scanned=… stream_ms=… plan_ms=… [trace_id=…]ReadyForQuery('I')
fn writeRowDescription(writer, columns) !void
Writes pgwire RowDescription ('T') using:
- the column name (
plan.ColumnInfo.name) - placeholder table/attribute identifiers (
0) - a single “default type” mapping for every column (
query_functions.pgTypeInfo(Type.init(.value, true)); see src/sydra/query/functions.zig)
fn writeDataRow(writer, values, row_buffer, value_buffer) !void
Writes pgwire DataRow ('D') in text format for every value:
- Each value is preceded by a 4-byte
i32belength. - Null values use length
-1.
fn formatValue(value, buf) !?[]const u8
Text formatting rules:
null→null(caller encodes-1)boolean→"t"or"f"integer→ decimal stringfloat→ decimal string via{d}string→ byte slice as-is
Other internal helpers
trimNullTerminatortrims a trailing0byte from query payloads.readU32reads big-endian lengths.readCStringparses NUL-terminated strings from a buffer.parseAddressparses IPv4 or IPv6.anyWriteradapts astd.Io.Writerintostd.Io.AnyWriter.formatSelectTagformats theCommandCompletetag (with optionaltrace_id).