Design: Named Prepared Statements #77
Replies: 3 comments
-
DecisionsQ1 — Q2 — Q3 — Future note — session in callbacks for retry-on-failure: Both |
Beta Was this translation helpful? Give feedback.
-
|
Erratum: The Tests section includes |
Beta Was this translation helpful? Give feedback.
-
|
Implemented in PR #78. All design decisions from the previous comment were applied as specified. The implementation includes the full public API ( |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Overview
The driver currently supports two query protocols:
SimpleQuery): Sends a SQL string directly. Can contain multiple statements.PreparedQuery): Parses, binds, and executes a parameterized single-statement query in one shot. The unnamed statement is destroyed by the next Parse or simple Query.Named prepared statements add a third mode: pre-prepared named statements. The SQL is parsed once with
session.prepare(), then executed multiple times with different parameters viasession.execute(), and eventually cleaned up withsession.close_statement(). The server retains the parsed and planned query, eliminating the Parse step on repeated executions.Named statements are independent of the unnamed statement. Simple queries and unnamed PreparedQuery executions destroy the unnamed statement but have no effect on named statements. The three protocols can be freely interleaved.
Public API
New types
NamedPreparedQuery-- a val class representing an execution of a previously prepared named statement:Unlike
PreparedQuery,NamedPreparedQuerydoes not carry the SQL string -- the SQL was provided duringsession.prepare(). Thenamefield identifies which server-side statement to execute.PrepareReceiver-- callback interface for prepare operations:Updated union type
This is a breaking change. Any code that pattern-matches on
Querymust add a branch forNamedPreparedQuery. The Pony compiler enforces exhaustive matching, so all affected code is caught at compile time.New Session behaviors
Usage example
Internal Changes
Queue refactoring
The current queue
Array[(Query, ResultReceiver)]is expanded to support three operation types:The queue becomes
Array[_QueueItem]. All operations execute serially through the same FIFO queue, preserving ordering guarantees. This means a sequence likeprepare("s1") -> execute(NamedPreparedQuery("s1")) -> close_statement("s1") -> prepare("s1")is guaranteed to execute in order.New
_SessionStatemethodsprepareandclose_statementare added to_SessionStateand implemented on each concrete state class. This follows the existing pattern whereexecuteis implemented directly on each state class (not defaulted through the trait hierarchy), because the error type differs per state.prepareper state:_SessionUnopened.prepare(...)callsreceiver.pg_prepare_failed(name, SessionNeverOpened)_SessionClosed.prepare(...)callsreceiver.pg_prepare_failed(name, SessionClosed)_SessionConnected.prepare(...)callsreceiver.pg_prepare_failed(name, SessionNotAuthenticated)_SessionLoggedIn.prepare(...)queues a_QueuedPrepareand callstry_run_queryclose_statementper state:_SessionUnopened.close_statement(...)-- silent no-op_SessionClosed.close_statement(...)-- silent no-op_SessionConnected.close_statement(...)-- silent no-op_SessionLoggedIn.close_statement(...)-- queues a_QueuedCloseStatementand callstry_run_queryNew query sub-states
_PrepareInFlight-- handles the prepare cycle:Wire protocol sent:
Parse(name, sql, []) + Describe(statement, name) + SyncExpected server response:
ParseComplete + ParameterDescription + (RowDescription | NoData) + ReadyForQueryError case:
ErrorResponse + ReadyForQueryMessage routing through
_ResponseMessageParser:ParseComplete-- falls through (silently consumed, not routed)ParameterDescription-- falls through (silently consumed, not routed)RowDescription-- routed toon_row_descriptionNoData-- falls through (silently consumed, not routed)ErrorResponse-- routed toon_error_responseReadyForQuery-- routed toon_ready_for_queryState behavior:
on_row_description-- received from Describe(statement); ignored (not cached in this version)on_error_response-- sets an error flag and notifies receiver viapg_prepare_failedon_ready_for_query-- if no error was received, notifies receiver viapg_statement_prepared; dequeues item; transitions to_QueryReady(if idle) or_QueryNotReadyon_data_row,on_command_complete,on_empty_query_response-- trigger shutdown (protocol anomaly; these should never arrive during a prepare cycle)try_run_query-- no-op (prepare is already in flight)_CloseStatementInFlight-- handles the close cycle:Wire protocol sent:
Close(statement, name) + SyncExpected server response:
CloseComplete + ReadyForQueryError case:
ErrorResponse + ReadyForQuery(the protocol spec says closing a non-existent object is not an error, so this path may be unreachable in practice for Close, but other server-side errors are theoretically possible)Message routing:
CloseComplete-- falls through (silently consumed, not routed)ReadyForQuery-- routed toon_ready_for_queryErrorResponse-- routed toon_error_responseState behavior:
on_ready_for_query-- dequeues item; transitions to_QueryReadyor_QueryNotReadyon_error_response-- silently absorbed (fire-and-forget semantics; errors from close are ignored); the subsequentReadyForQuerywill dequeue and transition normallytry_run_query-- no-opNamed query execution in
_QueryReady_QueryReady.try_run_query()is updated to handle all three queue item types:Named query execution reuses
_ExtendedQueryInFlightfor response handling because the server response sequence is identical to unnamed extended query execution:BindComplete + (RowDescription | NoData) + DataRow* + CommandComplete + ReadyForQuery. Only the wire bytes sent differ (no Parse step, Bind references the named statement).Existing in-flight state updates
The queue type change from
Array[(Query, ResultReceiver)]toArray[_QueueItem]requires updating_SimpleQueryInFlightand_ExtendedQueryInFlight. Both states currently access the queue with tuple destructuring:This changes to unwrapping the
_QueueItemunion:Affected methods in both states:
on_command_complete,on_empty_query_response,on_error_response,on_ready_for_query(forshift()). This is a mechanical change -- the logic within each method is unchanged.New frontend messages
Shutdown handling
_SessionLoggedIn.on_shutdownis updated to drain all queue item types:Breaking Changes
Queryunion type expanded:type Query is (SimpleQuery | PreparedQuery | NamedPreparedQuery). All pattern matches onQuerymust add aNamedPreparedQuerybranch. The Pony compiler enforces exhaustive matching, so all affected code is caught at compile time. This affects both public API consumers and internal test code (e.g.,_AllSuccessQueryRunningClient.pg_query_failedhas a match onQueryto extract the query string for error messages).The internal queue representation change from
Array[(Query, ResultReceiver)]toArray[_QueueItem]has no public API impact beyond the Query union change.Tests
Build and run commands:
make ssl=3.0.x-- build and run all testsmake unit-tests ssl=3.0.x-- unit tests only (no postgres needed)make integration-tests ssl=3.0.x-- integration tests (requires running PostgreSQL)Unit tests
_TestFrontendMessageDescribeStatement-- verify wire format of Describe(statement) message_TestFrontendMessageCloseStatement-- verify wire format of Close(statement) messageIntegration tests
PreparedStatement/Prepare-- prepare a statement, verifypg_statement_preparedcallbackPreparedStatement/PrepareAndExecute-- prepare, then execute with NamedPreparedQuery, verify resultsPreparedStatement/PrepareAndExecuteMultiple-- prepare once, execute twice with different params, verify both resultsPreparedStatement/PrepareAndClose-- prepare, close, then attempt execution, verify server errorPreparedStatement/PrepareFails-- prepare with invalid SQL, verifypg_prepare_failedcallbackPreparedStatement/PrepareAfterClose-- prepare, close, re-prepare same name, verify successPreparedStatement/CloseNonexistent-- close a statement that was never prepared, verify no errorPreparedStatement/PrepareDuplicateName-- prepare same name twice without closing, verify second prepare gets a server error viapg_prepare_failedPreparedStatement/MixedWithSimpleAndPrepared-- interleave named, unnamed, and simple queries in sequenceMock server tests
PreparedStatement/ShutdownDrainsPrepareQueue-- mock server that authenticates but never becomes ready; verify pending prepare operations getSessionClosedfailures on shutdownWhat's Deferred
Statement metadata caching: During prepare, the server sends ParameterDescription and RowDescription. These are currently received but not stored. Future work could cache them per statement name to: (a) skip Describe(portal) during execution, (b) validate parameter counts/types client-side before sending.
Client-side statement tracking: The session does not track which statement names are currently prepared. Invalid operations (executing a non-existent statement, re-preparing without closing) produce server-side errors delivered through the normal error callbacks. Future work could add a local registry for faster client-side validation and better error messages.
PrepareReceiver metadata: The PrepareReceiver callback includes only the session and name. Future versions could include the ParameterDescription (parameter OIDs) and RowDescription (column names/types) from the prepare response.
Open Questions
close_statementcallback: Shouldclose_statementbe fire-and-forget (current design), or should it accept a callback? Arguments for fire-and-forget: simpler API, queue ordering already guarantees sequencing with subsequent operations, closing non-existent statements isn't an error per protocol. Arguments for callback: user gets explicit confirmation that the name is available for reuse.NamedPreparedQuerynaming: IsNamedPreparedQuerythe right name? Alternatives considered:BoundQuery(emphasizes the bind-only nature),NamedQuery(shorter but ambiguous).NamedPreparedQueryis verbose but maps directly to the PostgreSQL concept.PrepareReceiversession parameter: The current design passessession: Sessioninpg_statement_preparedbut not inpg_prepare_failed. This is asymmetric within the interface. Options: (A) includesessionin both callbacks -- internally consistent, useful for retry-on-failure, but inconsistent withResultReceiverwhich omits session from both callbacks; (B) omitsessionfrom both -- consistent withResultReceiver, but less ergonomic since the user typically wants to execute queries immediately after prepare; (C) keep current asymmetry -- pragmatic (session is useful on success, less so on failure). Note:ResultReceiverhas an existing TODO about whether to add session to its callbacks (result_receiver.pony:1).Beta Was this translation helpful? Give feedback.
All reactions