Skip to main content

corium_pgwire/
lib.rs

1//! A `PostgreSQL` wire-protocol front end for Corium SQL.
2//!
3//! [`serve`] accepts `PostgreSQL` client connections. Reads run through
4//! [`corium_sql::SqlSession`] against an immutable [`corium_db::Db`] value
5//! obtained from a [`DbCatalog`]. Supported DML is planned into ordinary
6//! Corium transaction forms and committed through the catalog.
7//!
8//! One server exposes every database the catalog offers (subject to the
9//! catalog's own whitelist). A connection selects its database with the
10//! standard startup `database` parameter and can switch at any time with
11//! `USE <database>`; `SHOW DATABASES` lists what is available. The catalog is
12//! expected to open and cache databases lazily and share them across
13//! connections.
14//!
15//! Both simple and extended query sub-protocols are supported, including
16//! typed bound inputs and text or binary results. Mutations are
17//! autocommit-only: explicit transaction blocks allow reads but reject writes
18//! until atomic multi-statement transactions are implemented.
19
20mod protocol;
21mod types;
22
23use std::collections::{BTreeMap, HashMap};
24use std::future::Future;
25use std::sync::Arc;
26
27use corium_core::EntityId;
28use corium_db::Db;
29use corium_query::edn::Edn;
30use corium_sql::{MutationKind, SqlColumn, SqlError, SqlSession, SqlType};
31use thiserror::Error;
32use tokio::io::{AsyncRead, AsyncWrite};
33use tokio::net::TcpListener;
34
35use protocol::{BackendWriter, ErrorFields, FieldDescription, Frontend, FrontendReader};
36
37/// A database the catalog cannot hand back.
38#[derive(Debug, Error)]
39pub enum CatalogError {
40    /// No such database, or it is not permitted by the catalog's whitelist.
41    #[error("database {0:?} is not available")]
42    NotFound(String),
43    /// The database exists but could not be reached or opened.
44    #[error("{0}")]
45    Unavailable(String),
46    /// This catalog intentionally exposes snapshots without a write path.
47    #[error("database {0:?} is read-only through this catalog")]
48    ReadOnly(String),
49    /// A conditional transaction lost a concurrency race.
50    #[error("{0}")]
51    Conflict(String),
52    /// The transactor rejected validly transported transaction data.
53    #[error("{0}")]
54    Rejected(String),
55    /// The configured Corium principal is not allowed to transact.
56    #[error("{0}")]
57    Denied(String),
58    /// The remote component does not support the requested write capability
59    /// or protocol version.
60    #[error("{0}")]
61    Unsupported(String),
62}
63
64/// Result of a catalog transaction, synchronized through its committed basis.
65pub struct CatalogTxResult {
66    /// Database value including the committed transaction.
67    pub db_after: Db,
68    /// Tempids allocated by the transaction.
69    pub tempids: BTreeMap<String, EntityId>,
70}
71
72/// Supplies the databases the server exposes.
73///
74/// Implementations are expected to open databases lazily and cache them so a
75/// database is shared across all client connections. [`db`](DbCatalog::db)
76/// returns a fresh immutable snapshot each call, the same way the `corium sql`
77/// shell captures a current `Db` per statement.
78#[async_trait::async_trait]
79pub trait DbCatalog: Send + Sync + 'static {
80    /// Names of the databases clients may connect to.
81    ///
82    /// # Errors
83    /// Returns [`CatalogError`] when the catalog cannot be enumerated.
84    async fn list(&self) -> Result<Vec<String>, CatalogError>;
85
86    /// A current snapshot of the database named `name`.
87    ///
88    /// # Errors
89    /// Returns [`CatalogError::NotFound`] when the database is unknown or not
90    /// permitted, and [`CatalogError::Unavailable`] when it cannot be opened.
91    async fn db(&self, name: &str) -> Result<Db, CatalogError>;
92
93    /// Commits forms only if `expected_basis_t` is still current.
94    ///
95    /// The default keeps read-only catalog implementations source-compatible.
96    async fn transact(
97        &self,
98        name: &str,
99        _expected_basis_t: u64,
100        _forms: Vec<Edn>,
101    ) -> Result<CatalogTxResult, CatalogError> {
102        Err(CatalogError::ReadOnly(name.to_owned()))
103    }
104}
105
106/// Server-wide configuration for the `PostgreSQL` front end.
107#[derive(Clone, Debug)]
108pub struct PgWireConfig {
109    /// If set, clients must send this cleartext password to connect. When
110    /// `None`, connections are trusted.
111    pub password: Option<String>,
112    /// `server_version` reported to clients in a `ParameterStatus` message.
113    pub server_version: String,
114}
115
116impl Default for PgWireConfig {
117    fn default() -> Self {
118        Self {
119            password: None,
120            server_version: concat!("16.0 (corium ", env!("CARGO_PKG_VERSION"), ")").to_owned(),
121        }
122    }
123}
124
125/// Serves the `PostgreSQL` wire protocol until `shutdown` resolves.
126///
127/// Each accepted connection is handled on its own task; per-connection
128/// failures are logged and do not stop the server.
129///
130/// # Errors
131/// Returns an error only if accepting a connection fails fatally.
132pub async fn serve<C, F>(
133    listener: TcpListener,
134    catalog: Arc<C>,
135    config: PgWireConfig,
136    shutdown: F,
137) -> std::io::Result<()>
138where
139    C: DbCatalog,
140    F: Future<Output = ()>,
141{
142    let config = Arc::new(config);
143    tokio::pin!(shutdown);
144    loop {
145        tokio::select! {
146            () = &mut shutdown => return Ok(()),
147            accepted = listener.accept() => {
148                let (stream, peer) = accepted?;
149                let catalog = Arc::clone(&catalog);
150                let config = Arc::clone(&config);
151                tokio::spawn(async move {
152                    let (read, write) = stream.into_split();
153                    let mut session = ConnectionSession::new(
154                        FrontendReader::new(read),
155                        BackendWriter::new(write),
156                        catalog,
157                        config,
158                    );
159                    if let Err(error) = session.run().await {
160                        tracing::debug!(%peer, %error, "pgwire connection closed");
161                    }
162                });
163            }
164        }
165    }
166}
167
168/// A bound portal: the SQL to run and the database it was bound against.
169struct Portal {
170    sql: String,
171    database: Option<String>,
172    params: Vec<corium_sql::SqlValue>,
173    result_formats: Vec<i16>,
174}
175
176struct PreparedStatement {
177    sql: String,
178    parameter_types: Vec<i32>,
179}
180
181/// How a statement should be handled before it ever reaches `SqlSession`.
182enum Statement {
183    /// A stateless control statement accepted as a no-op with this tag.
184    Control(&'static str),
185    /// Start an explicit transaction block. Reads are allowed, but writes are
186    /// rejected until true multi-statement transactions are implemented.
187    Begin,
188    /// End an explicit transaction block.
189    Commit,
190    /// Abandon an explicit transaction block.
191    Rollback,
192    /// `USE <database>` — switch the connection's active database.
193    Use(String),
194    /// `SHOW DATABASES` — list the catalog.
195    ShowDatabases,
196    /// An ordinary read-only query for `SqlSession`.
197    Query,
198    /// `INSERT`, `UPDATE`, or `DELETE`.
199    Mutation,
200}
201
202/// A failure while dispatching one statement.
203enum Dispatch {
204    /// No database is selected for a query.
205    NoDatabase,
206    /// The catalog could not provide the database.
207    Catalog(CatalogError),
208    /// `SqlSession` rejected or failed the query.
209    Sql(SqlError),
210}
211
212/// The per-connection protocol state machine.
213struct ConnectionSession<R, W, C> {
214    reader: FrontendReader<R>,
215    writer: BackendWriter<W>,
216    catalog: Arc<C>,
217    config: Arc<PgWireConfig>,
218    /// The connection's active database, chosen at startup or by `USE`.
219    current_db: Option<String>,
220    statements: HashMap<String, PreparedStatement>,
221    portals: HashMap<String, Portal>,
222    /// Set after an extended-protocol error; frontend messages are ignored
223    /// until the next `Sync`.
224    failed: bool,
225    /// Whether the client has entered an explicit transaction block.
226    ///
227    /// Corium SQL mutations are autocommit-only for now. Tracking the block
228    /// prevents a `BEGIN; INSERT ...; COMMIT` sequence from appearing atomic
229    /// while actually committing the insert immediately.
230    in_transaction: bool,
231    /// Whether an error has aborted the current explicit transaction.
232    transaction_failed: bool,
233    /// The immutable database value pinned by the first read in the current
234    /// explicit transaction.
235    transaction_db: Option<(String, Db)>,
236}
237
238impl<R, W, C> ConnectionSession<R, W, C>
239where
240    R: AsyncRead + Unpin,
241    W: AsyncWrite + Unpin,
242    C: DbCatalog,
243{
244    fn new(
245        reader: FrontendReader<R>,
246        writer: BackendWriter<W>,
247        catalog: Arc<C>,
248        config: Arc<PgWireConfig>,
249    ) -> Self {
250        Self {
251            reader,
252            writer,
253            catalog,
254            config,
255            current_db: None,
256            statements: HashMap::new(),
257            portals: HashMap::new(),
258            failed: false,
259            in_transaction: false,
260            transaction_failed: false,
261            transaction_db: None,
262        }
263    }
264
265    async fn run(&mut self) -> std::io::Result<()> {
266        let startup = self.reader.read_startup(&mut self.writer).await?;
267        if !self.authenticate().await? {
268            return Ok(());
269        }
270        // The database is validated lazily on first use, so a client may
271        // connect with an unknown default and then `USE` a real database.
272        self.current_db = startup
273            .get("database")
274            .filter(|value| !value.is_empty())
275            .map(str::to_owned);
276        self.send_ready_banner(startup.get("application_name").unwrap_or(""))
277            .await?;
278
279        while let Some(message) = self.reader.read_message().await? {
280            match message {
281                Frontend::Query(sql) => {
282                    // After an extended-protocol error the backend ignores
283                    // every message until the next `Sync`.
284                    if !self.failed {
285                        self.simple_query(&sql).await?;
286                        self.writer.flush().await?;
287                    }
288                }
289                Frontend::Parse {
290                    name,
291                    query,
292                    parameter_types,
293                } => self.handle_parse(name, query, parameter_types),
294                Frontend::Bind {
295                    portal,
296                    statement,
297                    parameter_formats,
298                    parameters,
299                    result_formats,
300                } => self.handle_bind(
301                    &portal,
302                    &statement,
303                    &parameter_formats,
304                    &parameters,
305                    &result_formats,
306                ),
307                Frontend::Describe { kind, name } => self.handle_describe(kind, &name).await?,
308                Frontend::Execute { portal } => self.handle_execute(&portal).await?,
309                Frontend::Close { kind, name } => {
310                    if !self.failed {
311                        if kind == b'S' {
312                            self.statements.remove(&name);
313                        } else {
314                            self.portals.remove(&name);
315                        }
316                        self.writer.close_complete();
317                    }
318                }
319                Frontend::Sync => {
320                    self.failed = false;
321                    self.writer.ready_for_query(self.ready_status());
322                    self.writer.flush().await?;
323                }
324                Frontend::Flush => self.writer.flush().await?,
325                Frontend::Password(_) => {}
326                Frontend::Terminate => break,
327            }
328        }
329        Ok(())
330    }
331
332    /// Runs the authentication exchange. Returns `false` if the connection
333    /// should be closed (bad password).
334    async fn authenticate(&mut self) -> std::io::Result<bool> {
335        let Some(expected) = self.config.password.clone() else {
336            self.writer.authentication_ok();
337            return Ok(true);
338        };
339        self.writer.authentication_cleartext_password();
340        self.writer.flush().await?;
341        match self.reader.read_message().await? {
342            Some(Frontend::Password(supplied)) if supplied == expected => {
343                self.writer.authentication_ok();
344                Ok(true)
345            }
346            _ => {
347                self.writer.error_response(&ErrorFields {
348                    code: "28P01",
349                    message: "password authentication failed",
350                });
351                self.writer.flush().await?;
352                Ok(false)
353            }
354        }
355    }
356
357    /// Sends the post-authentication parameter status, key data, and the
358    /// first `ReadyForQuery`.
359    async fn send_ready_banner(&mut self, application_name: &str) -> std::io::Result<()> {
360        self.writer
361            .parameter_status("server_version", &self.config.server_version);
362        self.writer.parameter_status("server_encoding", "UTF8");
363        self.writer.parameter_status("client_encoding", "UTF8");
364        self.writer.parameter_status("DateStyle", "ISO, MDY");
365        self.writer.parameter_status("TimeZone", "UTC");
366        self.writer.parameter_status("integer_datetimes", "on");
367        self.writer
368            .parameter_status("standard_conforming_strings", "on");
369        self.writer
370            .parameter_status("application_name", application_name);
371        self.writer.backend_key_data(0, 0);
372        self.writer.ready_for_query(b'I');
373        self.writer.flush().await
374    }
375
376    /// Handles a simple-query message: run each statement, stopping at the
377    /// first error, then report `ReadyForQuery`.
378    async fn simple_query(&mut self, sql: &str) -> std::io::Result<()> {
379        let statements = split_statements(sql);
380        if statements.is_empty() {
381            self.writer.empty_query_response();
382            self.writer.ready_for_query(self.ready_status());
383            return Ok(());
384        }
385        for statement in statements {
386            if !self.run_simple_statement(&statement).await? {
387                break;
388            }
389        }
390        self.writer.ready_for_query(self.ready_status());
391        Ok(())
392    }
393
394    /// Runs one simple-protocol statement. Returns `false` when an error was
395    /// reported and the rest of the query string should be abandoned.
396    async fn run_simple_statement(&mut self, sql: &str) -> std::io::Result<bool> {
397        let statement = classify(sql);
398        if self.transaction_failed && !matches!(&statement, Statement::Rollback) {
399            self.report_transaction_aborted();
400            return Ok(false);
401        }
402        match statement {
403            Statement::Control(tag) => {
404                self.writer.command_complete(tag);
405                Ok(true)
406            }
407            Statement::Begin => {
408                self.begin_transaction();
409                self.writer.command_complete("BEGIN");
410                Ok(true)
411            }
412            Statement::Commit => {
413                self.end_transaction();
414                self.writer.command_complete("COMMIT");
415                Ok(true)
416            }
417            Statement::Rollback => {
418                self.end_transaction();
419                self.writer.command_complete("ROLLBACK");
420                Ok(true)
421            }
422            Statement::Use(name) => match self.use_database(&name).await {
423                Ok(()) => {
424                    self.writer.command_complete("USE");
425                    Ok(true)
426                }
427                Err(error) => {
428                    self.report_dispatch(&error);
429                    Ok(false)
430                }
431            },
432            Statement::ShowDatabases => match self.show_databases(true, &[]).await {
433                Ok(()) => Ok(true),
434                Err(error) => {
435                    self.report_dispatch(&error);
436                    Ok(false)
437                }
438            },
439            Statement::Query => {
440                let db = match self.snapshot(None).await {
441                    Ok(db) => db,
442                    Err(error) => {
443                        self.report_dispatch(&error);
444                        return Ok(false);
445                    }
446                };
447                match self.run_statement(&db, sql, &[], true, &[]).await {
448                    Ok(rows) => {
449                        self.writer.command_complete(&command_tag(sql, rows));
450                        Ok(true)
451                    }
452                    Err(error) => {
453                        self.report_dispatch(&Dispatch::Sql(error));
454                        Ok(false)
455                    }
456                }
457            }
458            Statement::Mutation => {
459                if self.in_transaction {
460                    self.report_dispatch(&explicit_transaction_write_error());
461                    return Ok(false);
462                }
463                let Some(database) = self.current_db.clone() else {
464                    self.report_dispatch(&Dispatch::NoDatabase);
465                    return Ok(false);
466                };
467                let db = match self.snapshot(Some(&database)).await {
468                    Ok(db) => db,
469                    Err(error) => {
470                        self.report_dispatch(&error);
471                        return Ok(false);
472                    }
473                };
474                match self.run_mutation(&database, &db, sql, &[], true, &[]).await {
475                    Ok((kind, rows)) => {
476                        self.writer
477                            .command_complete(&mutation_command_tag(kind, rows));
478                        Ok(true)
479                    }
480                    Err(error) => {
481                        self.report_dispatch(&error);
482                        Ok(false)
483                    }
484                }
485            }
486        }
487    }
488
489    fn handle_parse(&mut self, name: String, query: String, mut parameter_types: Vec<i32>) {
490        if self.failed {
491            return;
492        }
493        let inferred_count = placeholder_count(&query);
494        if parameter_types.len() > inferred_count {
495            self.fail_extended("08P01", "too many parameter types in Parse");
496            return;
497        }
498        parameter_types.resize(inferred_count, 0);
499        self.statements.insert(
500            name,
501            PreparedStatement {
502                sql: query,
503                parameter_types,
504            },
505        );
506        self.writer.parse_complete();
507    }
508
509    fn handle_bind(
510        &mut self,
511        portal: &str,
512        statement: &str,
513        parameter_formats: &[i16],
514        parameters: &[Option<Vec<u8>>],
515        result_formats: &[i16],
516    ) {
517        if self.failed {
518            return;
519        }
520        if result_formats.iter().any(|format| !matches!(format, 0 | 1)) {
521            self.fail_extended("08P01", "result format code must be zero or one");
522            return;
523        }
524        let Some(prepared) = self.statements.get(statement) else {
525            self.fail_extended("26000", "prepared statement does not exist");
526            return;
527        };
528        if parameters.len() != prepared.parameter_types.len() {
529            self.fail_extended("08P01", "bound parameter count does not match Parse");
530            return;
531        }
532        let formats = match expand_formats(parameter_formats, parameters.len()) {
533            Ok(formats) => formats,
534            Err(message) => {
535                self.fail_extended("08P01", message);
536                return;
537            }
538        };
539        let params = prepared
540            .parameter_types
541            .iter()
542            .zip(formats)
543            .zip(parameters)
544            .map(|((oid, format), value)| types::decode_parameter(*oid, format, value.as_deref()))
545            .collect::<Result<Vec<_>, _>>();
546        let params = match params {
547            Ok(params) => params,
548            Err(message) => {
549                self.fail_extended("22P02", &message);
550                return;
551            }
552        };
553        self.portals.insert(
554            portal.to_owned(),
555            Portal {
556                sql: prepared.sql.clone(),
557                database: self.current_db.clone(),
558                params,
559                result_formats: result_formats.to_vec(),
560            },
561        );
562        self.writer.bind_complete();
563    }
564
565    async fn handle_describe(&mut self, kind: u8, name: &str) -> std::io::Result<()> {
566        if self.failed {
567            return Ok(());
568        }
569        // `Describe` of a prepared statement first reports its parameters.
570        let (sql, database, params, result_formats) = if kind == b'S' {
571            let Some((sql, parameter_types)) = self
572                .statements
573                .get(name)
574                .map(|statement| (statement.sql.clone(), statement.parameter_types.clone()))
575            else {
576                self.fail_extended("26000", "prepared statement does not exist");
577                return Ok(());
578            };
579            self.writer.parameter_description(&parameter_types);
580            let params = parameter_types
581                .into_iter()
582                .map(types::describe_parameter)
583                .collect::<Result<Vec<_>, _>>();
584            let params = match params {
585                Ok(params) => params,
586                Err(message) => {
587                    self.fail_extended("0A000", &message);
588                    return Ok(());
589                }
590            };
591            (sql, self.current_db.clone(), params, Vec::new())
592        } else {
593            let Some(portal) = self.portals.get(name) else {
594                self.fail_extended("34000", "portal does not exist");
595                return Ok(());
596            };
597            (
598                portal.sql.clone(),
599                portal.database.clone(),
600                portal.params.clone(),
601                portal.result_formats.clone(),
602            )
603        };
604        match classify(&sql) {
605            Statement::ShowDatabases => {
606                self.write_row_description(&[database_field()], &result_formats);
607            }
608            Statement::Query if !sql.trim().is_empty() => {
609                let db = match self.snapshot(database.as_deref()).await {
610                    Ok(db) => db,
611                    Err(error) => {
612                        self.fail_dispatch(&error);
613                        return Ok(());
614                    }
615                };
616                match self.describe_columns(&db, &sql, &params).await {
617                    Ok(fields) => {
618                        self.write_row_description(&fields, &result_formats);
619                    }
620                    Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
621                }
622            }
623            Statement::Mutation => {
624                let db = match self.snapshot(database.as_deref()).await {
625                    Ok(db) => db,
626                    Err(error) => {
627                        self.fail_dispatch(&error);
628                        return Ok(());
629                    }
630                };
631                match SqlSession::new(&db) {
632                    Ok(session) => match session.mutation_columns(&sql, &params).await {
633                        Ok(Some(columns)) if columns.is_empty() => self.writer.no_data(),
634                        Ok(Some(columns)) => {
635                            self.write_row_description(
636                                &columns.iter().map(field_of).collect::<Vec<_>>(),
637                                &result_formats,
638                            );
639                        }
640                        Ok(None) => self.writer.no_data(),
641                        Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
642                    },
643                    Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
644                }
645            }
646            _ => self.writer.no_data(),
647        }
648        Ok(())
649    }
650
651    async fn handle_execute(&mut self, portal: &str) -> std::io::Result<()> {
652        if self.failed {
653            return Ok(());
654        }
655        let Some((sql, database, params, result_formats)) =
656            self.portals.get(portal).map(|portal| {
657                (
658                    portal.sql.clone(),
659                    portal.database.clone(),
660                    portal.params.clone(),
661                    portal.result_formats.clone(),
662                )
663            })
664        else {
665            self.fail_extended("34000", "portal does not exist");
666            return Ok(());
667        };
668        if sql.trim().is_empty() {
669            self.writer.empty_query_response();
670            return Ok(());
671        }
672        let statement = classify(&sql);
673        if self.transaction_failed && !matches!(&statement, Statement::Rollback) {
674            self.fail_extended(
675                "25P02",
676                "current transaction is aborted; commands ignored until end of transaction block",
677            );
678            return Ok(());
679        }
680        match statement {
681            Statement::Control(tag) => self.writer.command_complete(tag),
682            Statement::Begin => {
683                self.begin_transaction();
684                self.writer.command_complete("BEGIN");
685            }
686            Statement::Commit => {
687                self.end_transaction();
688                self.writer.command_complete("COMMIT");
689            }
690            Statement::Rollback => {
691                self.end_transaction();
692                self.writer.command_complete("ROLLBACK");
693            }
694            Statement::Use(name) => match self.use_database(&name).await {
695                Ok(()) => self.writer.command_complete("USE"),
696                Err(error) => self.fail_dispatch(&error),
697            },
698            Statement::ShowDatabases => {
699                if let Err(error) = self.show_databases(false, &result_formats).await {
700                    self.fail_dispatch(&error);
701                }
702            }
703            Statement::Query => {
704                let db = match self.snapshot(database.as_deref()).await {
705                    Ok(db) => db,
706                    Err(error) => {
707                        self.fail_dispatch(&error);
708                        return Ok(());
709                    }
710                };
711                match self
712                    .run_statement(&db, &sql, &params, false, &result_formats)
713                    .await
714                {
715                    Ok(rows) => self.writer.command_complete(&command_tag(&sql, rows)),
716                    Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
717                }
718            }
719            Statement::Mutation => {
720                if self.in_transaction {
721                    self.fail_dispatch(&explicit_transaction_write_error());
722                    return Ok(());
723                }
724                let Some(database) = database.or_else(|| self.current_db.clone()) else {
725                    self.fail_dispatch(&Dispatch::NoDatabase);
726                    return Ok(());
727                };
728                let db = match self.snapshot(Some(&database)).await {
729                    Ok(db) => db,
730                    Err(error) => {
731                        self.fail_dispatch(&error);
732                        return Ok(());
733                    }
734                };
735                match self
736                    .run_mutation(&database, &db, &sql, &params, false, &result_formats)
737                    .await
738                {
739                    Ok((kind, rows)) => self
740                        .writer
741                        .command_complete(&mutation_command_tag(kind, rows)),
742                    Err(error) => self.fail_dispatch(&error),
743                }
744            }
745        }
746        Ok(())
747    }
748
749    /// Validates and activates `name` as the connection's database, warming
750    /// the catalog cache in the process.
751    async fn use_database(&mut self, name: &str) -> Result<(), Dispatch> {
752        self.snapshot(Some(name)).await?;
753        self.current_db = Some(name.to_owned());
754        Ok(())
755    }
756
757    /// Emits a one-column `database` result listing the catalog.
758    async fn show_databases(
759        &mut self,
760        with_row_description: bool,
761        result_formats: &[i16],
762    ) -> Result<(), Dispatch> {
763        let names = self.catalog.list().await.map_err(Dispatch::Catalog)?;
764        if with_row_description {
765            self.write_row_description(&[database_field()], result_formats);
766        }
767        for name in &names {
768            self.writer.data_row(&[Some(name.clone().into_bytes())]);
769        }
770        self.writer.command_complete("SHOW");
771        Ok(())
772    }
773
774    /// Resolves an immutable snapshot for `database`, falling back to the
775    /// connection's active database.
776    async fn snapshot(&mut self, database: Option<&str>) -> Result<Db, Dispatch> {
777        let name = database
778            .map(str::to_owned)
779            .or_else(|| self.current_db.clone())
780            .ok_or(Dispatch::NoDatabase)?;
781        if self.in_transaction {
782            if let Some((pinned_name, pinned)) = &self.transaction_db {
783                if pinned_name != &name {
784                    return Err(Dispatch::Sql(SqlError::Mutation(
785                        "cannot switch databases inside an explicit transaction".into(),
786                    )));
787                }
788                return Ok(pinned.clone());
789            }
790            let db = self.catalog.db(&name).await.map_err(Dispatch::Catalog)?;
791            self.transaction_db = Some((name, db.clone()));
792            Ok(db)
793        } else {
794            self.catalog.db(&name).await.map_err(Dispatch::Catalog)
795        }
796    }
797
798    fn ready_status(&self) -> u8 {
799        if self.transaction_failed {
800            b'E'
801        } else if self.in_transaction {
802            b'T'
803        } else {
804            b'I'
805        }
806    }
807
808    /// Plans a query and returns its result columns without streaming rows.
809    async fn describe_columns(
810        &self,
811        db: &Db,
812        sql: &str,
813        params: &[corium_sql::SqlValue],
814    ) -> Result<Vec<FieldDescription>, SqlError> {
815        let session = SqlSession::new(db)?;
816        let query = session.query_params(sql, params).await?;
817        Ok(query.columns().iter().map(field_of).collect())
818    }
819
820    /// Runs one statement, optionally emitting a `RowDescription` first, then
821    /// streaming its rows as `DataRow` messages. Returns the row count.
822    async fn run_statement(
823        &mut self,
824        db: &Db,
825        sql: &str,
826        params: &[corium_sql::SqlValue],
827        with_row_description: bool,
828        result_formats: &[i16],
829    ) -> Result<usize, SqlError> {
830        let session = SqlSession::new(db)?;
831        let mut query = session.query_params(sql, params).await?;
832        let columns = query.columns().to_vec();
833        let formats = expand_result_formats(result_formats, columns.len())
834            .map_err(|message| SqlError::Mutation(message.into()))?;
835        if with_row_description {
836            let fields = columns.iter().map(field_of).collect::<Vec<_>>();
837            self.writer.row_description_with_formats(&fields, &formats);
838        }
839        let mut count = 0usize;
840        while let Some(row) = query.next_row().await? {
841            let values = row
842                .iter()
843                .zip(&columns)
844                .zip(&formats)
845                .map(|((value, column), format)| {
846                    types::encode_result(value, &column.data_type, *format)
847                        .map_err(SqlError::Mutation)
848                })
849                .collect::<Result<Vec<_>, _>>()?;
850            self.writer.data_row(&values);
851            count += 1;
852            // Bound peak memory on large results by flushing periodically.
853            if count.is_multiple_of(1024) {
854                self.writer
855                    .flush()
856                    .await
857                    .map_err(|error| SqlError::Schema(error.to_string()))?;
858            }
859        }
860        Ok(count)
861    }
862
863    async fn run_mutation(
864        &mut self,
865        database: &str,
866        db: &Db,
867        sql: &str,
868        params: &[corium_sql::SqlValue],
869        with_row_description: bool,
870        result_formats: &[i16],
871    ) -> Result<(MutationKind, usize), Dispatch> {
872        let session = SqlSession::new(db).map_err(Dispatch::Sql)?;
873        let mutation = session
874            .mutation_params(sql, params)
875            .await
876            .map_err(Dispatch::Sql)?
877            .ok_or_else(|| Dispatch::Sql(SqlError::Mutation("expected a mutation".into())))?;
878        let (db_after, tempids) = if mutation.is_empty() {
879            (db.clone(), BTreeMap::new())
880        } else {
881            let result = self
882                .catalog
883                .transact(
884                    database,
885                    mutation.expected_basis_t(),
886                    mutation.forms().to_vec(),
887                )
888                .await
889                .map_err(Dispatch::Catalog)?;
890            (result.db_after, result.tempids)
891        };
892        let returned = mutation
893            .finish(&db_after, &tempids)
894            .await
895            .map_err(Dispatch::Sql)?;
896        let formats = expand_result_formats(result_formats, returned.columns.len())
897            .map_err(|message| Dispatch::Sql(SqlError::Mutation(message.into())))?;
898        if with_row_description && !returned.columns.is_empty() {
899            self.writer.row_description_with_formats(
900                &returned.columns.iter().map(field_of).collect::<Vec<_>>(),
901                &formats,
902            );
903        }
904        for row in returned.rows {
905            let values = row
906                .iter()
907                .zip(&returned.columns)
908                .zip(&formats)
909                .map(|((value, column), format)| {
910                    types::encode_result(value, &column.data_type, *format)
911                        .map_err(|message| Dispatch::Sql(SqlError::Mutation(message)))
912                })
913                .collect::<Result<Vec<_>, _>>()?;
914            self.writer.data_row(&values);
915        }
916        Ok((mutation.kind(), mutation.affected()))
917    }
918
919    /// Emits an `ErrorResponse` for a simple-query dispatch failure.
920    fn report_dispatch(&mut self, error: &Dispatch) {
921        let (code, message) = dispatch_error_fields(error);
922        self.writer.error_response(&ErrorFields {
923            code,
924            message: &message,
925        });
926        if self.in_transaction {
927            self.transaction_failed = true;
928        }
929    }
930
931    /// Emits an `ErrorResponse` and enters the skip-until-`Sync` state.
932    fn fail_dispatch(&mut self, error: &Dispatch) {
933        self.report_dispatch(error);
934        self.failed = true;
935    }
936
937    /// Emits an `ErrorResponse` with an explicit code and enters the
938    /// skip-until-`Sync` state.
939    fn fail_extended(&mut self, code: &str, message: &str) {
940        self.writer.error_response(&ErrorFields { code, message });
941        self.failed = true;
942        if self.in_transaction {
943            self.transaction_failed = true;
944        }
945    }
946
947    fn report_transaction_aborted(&mut self) {
948        self.writer.error_response(&ErrorFields {
949            code: "25P02",
950            message: "current transaction is aborted; commands ignored until end of transaction block",
951        });
952    }
953
954    fn begin_transaction(&mut self) {
955        self.in_transaction = true;
956        self.transaction_failed = false;
957        self.transaction_db = None;
958    }
959
960    fn end_transaction(&mut self) {
961        self.in_transaction = false;
962        self.transaction_failed = false;
963        self.transaction_db = None;
964    }
965
966    fn write_row_description(&mut self, fields: &[FieldDescription], requested: &[i16]) {
967        match expand_result_formats(requested, fields.len()) {
968            Ok(formats) => self.writer.row_description_with_formats(fields, &formats),
969            Err(message) => self.fail_extended("08P01", message),
970        }
971    }
972}
973
974/// The `RowDescription` field for the `SHOW DATABASES` result column.
975fn database_field() -> FieldDescription {
976    let type_oid = types::type_oid(&SqlType::Text);
977    FieldDescription {
978        name: "database".to_owned(),
979        type_oid,
980        type_len: types::type_len(type_oid),
981    }
982}
983
984/// Builds a `RowDescription` field from a result column.
985fn field_of(column: &SqlColumn) -> FieldDescription {
986    let type_oid = types::type_oid(&column.data_type);
987    FieldDescription {
988        name: column.name.clone(),
989        type_oid,
990        type_len: types::type_len(type_oid),
991    }
992}
993
994/// The `SQLSTATE` code and message an error is reported with.
995fn dispatch_error_fields(error: &Dispatch) -> (&'static str, String) {
996    match error {
997        Dispatch::NoDatabase => (
998            "3D000",
999            "no database selected; run \"USE <database>\" first".to_owned(),
1000        ),
1001        Dispatch::Catalog(error @ CatalogError::NotFound(_)) => ("3D000", error.to_string()),
1002        Dispatch::Catalog(error @ CatalogError::Unavailable(_)) => ("08006", error.to_string()),
1003        Dispatch::Catalog(error @ CatalogError::ReadOnly(_)) => ("25006", error.to_string()),
1004        Dispatch::Catalog(error @ CatalogError::Conflict(_)) => ("40001", error.to_string()),
1005        Dispatch::Catalog(error @ CatalogError::Rejected(_)) => ("23000", error.to_string()),
1006        Dispatch::Catalog(error @ CatalogError::Denied(_)) => ("42501", error.to_string()),
1007        Dispatch::Catalog(error @ CatalogError::Unsupported(_)) => ("0A000", error.to_string()),
1008        Dispatch::Sql(error) => (sqlstate_for(error), error.to_string()),
1009    }
1010}
1011
1012fn explicit_transaction_write_error() -> Dispatch {
1013    Dispatch::Sql(SqlError::Mutation(
1014        "writes inside explicit transaction blocks are not supported; use autocommit".to_owned(),
1015    ))
1016}
1017
1018/// Chooses a `SQLSTATE` code for a SQL error.
1019fn sqlstate_for(error: &SqlError) -> &'static str {
1020    match error {
1021        // Missing table / projection problems.
1022        SqlError::Schema(_) => "42P01",
1023        SqlError::Mutation(_) => "0A000",
1024        // Parse, plan, and execution failures.
1025        SqlError::Parser(_) | SqlError::DataFusion(_) | SqlError::Arrow(_) => "42601",
1026    }
1027}
1028
1029/// The `CommandComplete` tag for a statement that returned `rows` rows.
1030fn command_tag(sql: &str, rows: usize) -> String {
1031    if first_keyword(sql).eq_ignore_ascii_case("explain") {
1032        "EXPLAIN".to_owned()
1033    } else {
1034        format!("SELECT {rows}")
1035    }
1036}
1037
1038fn mutation_command_tag(kind: MutationKind, rows: usize) -> String {
1039    match kind {
1040        MutationKind::Insert => format!("INSERT 0 {rows}"),
1041        MutationKind::Update => format!("UPDATE {rows}"),
1042        MutationKind::Delete => format!("DELETE {rows}"),
1043    }
1044}
1045
1046fn expand_formats(formats: &[i16], count: usize) -> Result<Vec<i16>, &'static str> {
1047    match formats {
1048        [] => Ok(vec![0; count]),
1049        [format] => Ok(vec![*format; count]),
1050        formats if formats.len() == count => Ok(formats.to_vec()),
1051        _ => Err("parameter format count must be zero, one, or the parameter count"),
1052    }
1053}
1054
1055fn expand_result_formats(formats: &[i16], count: usize) -> Result<Vec<i16>, &'static str> {
1056    match formats {
1057        [] => Ok(vec![0; count]),
1058        [format] => Ok(vec![*format; count]),
1059        formats if formats.len() == count => Ok(formats.to_vec()),
1060        _ => Err("result format count must be zero, one, or the result column count"),
1061    }
1062}
1063
1064/// Returns the largest `PostgreSQL` positional placeholder (`$1`, `$2`, ...).
1065/// Strings, identifiers, comments, and dollar-quoted bodies are skipped so
1066/// their contents do not affect the protocol parameter count.
1067fn placeholder_count(sql: &str) -> usize {
1068    #[derive(Clone, Copy)]
1069    enum State {
1070        Normal,
1071        Single { backslash_escapes: bool },
1072        Double,
1073        LineComment,
1074        BlockComment(usize),
1075    }
1076    let bytes = sql.as_bytes();
1077    let mut state = State::Normal;
1078    let mut count = 0usize;
1079    let mut index = 0usize;
1080    while index < bytes.len() {
1081        match state {
1082            State::Normal if bytes[index..].starts_with(b"--") => {
1083                state = State::LineComment;
1084                index += 1;
1085            }
1086            State::Normal if bytes[index..].starts_with(b"/*") => {
1087                state = State::BlockComment(1);
1088                index += 1;
1089            }
1090            State::Normal if bytes[index] == b'\'' => {
1091                let backslash_escapes = index > 0
1092                    && matches!(bytes[index - 1], b'e' | b'E')
1093                    && (index < 2
1094                        || !matches!(
1095                            bytes[index - 2],
1096                            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_'
1097                        ));
1098                state = State::Single { backslash_escapes };
1099            }
1100            State::Normal if bytes[index] == b'"' => state = State::Double,
1101            State::Normal if bytes[index] == b'$' => {
1102                let start = index + 1;
1103                let mut end = start;
1104                while bytes.get(end).is_some_and(u8::is_ascii_digit) {
1105                    end += 1;
1106                }
1107                if end > start
1108                    && let Ok(number) = sql[start..end].parse::<usize>()
1109                {
1110                    count = count.max(number);
1111                    index = end - 1;
1112                } else if let Some(delimiter_end) = dollar_quote_end(bytes, index) {
1113                    let delimiter = &bytes[index..delimiter_end];
1114                    let body_start = delimiter_end;
1115                    if let Some(offset) = find_bytes(&bytes[body_start..], delimiter) {
1116                        index = body_start + offset + delimiter.len() - 1;
1117                    } else {
1118                        index = bytes.len();
1119                    }
1120                }
1121            }
1122            State::Single {
1123                backslash_escapes: true,
1124            } if bytes[index] == b'\\' => {
1125                index += usize::from(index + 1 < bytes.len());
1126            }
1127            State::Single { .. } if bytes[index] == b'\'' => {
1128                if bytes.get(index + 1) == Some(&b'\'') {
1129                    index += 1;
1130                } else {
1131                    state = State::Normal;
1132                }
1133            }
1134            State::Double if bytes[index] == b'"' => {
1135                if bytes.get(index + 1) == Some(&b'"') {
1136                    index += 1;
1137                } else {
1138                    state = State::Normal;
1139                }
1140            }
1141            State::LineComment if matches!(bytes[index], b'\r' | b'\n') => {
1142                state = State::Normal;
1143            }
1144            State::BlockComment(depth) if bytes[index..].starts_with(b"/*") => {
1145                state = State::BlockComment(depth + 1);
1146                index += 1;
1147            }
1148            State::BlockComment(depth) if bytes[index..].starts_with(b"*/") => {
1149                state = if depth == 1 {
1150                    State::Normal
1151                } else {
1152                    State::BlockComment(depth - 1)
1153                };
1154                index += 1;
1155            }
1156            State::Normal
1157            | State::Single { .. }
1158            | State::Double
1159            | State::LineComment
1160            | State::BlockComment(_) => {}
1161        }
1162        index += 1;
1163    }
1164    count
1165}
1166
1167fn dollar_quote_end(bytes: &[u8], start: usize) -> Option<usize> {
1168    let mut end = start + 1;
1169    if bytes.get(end) == Some(&b'$') {
1170        return Some(end + 1);
1171    }
1172    if !bytes
1173        .get(end)
1174        .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
1175    {
1176        return None;
1177    }
1178    end += 1;
1179    while bytes
1180        .get(end)
1181        .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
1182    {
1183        end += 1;
1184    }
1185    (bytes.get(end) == Some(&b'$')).then_some(end + 1)
1186}
1187
1188fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1189    haystack
1190        .windows(needle.len())
1191        .position(|window| window == needle)
1192}
1193
1194/// Classifies a statement so `USE`, `SHOW DATABASES`, and no-op control
1195/// statements are handled before reaching `SqlSession`.
1196fn classify(sql: &str) -> Statement {
1197    let mut words = sql.split_whitespace();
1198    let first = words.next().unwrap_or("").to_ascii_uppercase();
1199    match first.as_str() {
1200        "USE" => parse_use_target(sql).map_or(Statement::Query, Statement::Use),
1201        "SHOW"
1202            if words
1203                .next()
1204                .is_some_and(|word| word.eq_ignore_ascii_case("databases")) =>
1205        {
1206            Statement::ShowDatabases
1207        }
1208        "BEGIN" | "START" => Statement::Begin,
1209        "COMMIT" | "END" => Statement::Commit,
1210        "ROLLBACK" | "ABORT" => Statement::Rollback,
1211        "SET" => Statement::Control("SET"),
1212        "RESET" => Statement::Control("RESET"),
1213        "DISCARD" => Statement::Control("DISCARD ALL"),
1214        "INSERT" | "UPDATE" | "DELETE" => Statement::Mutation,
1215        _ => Statement::Query,
1216    }
1217}
1218
1219/// Extracts the database name from a `USE <database>` statement.
1220fn parse_use_target(sql: &str) -> Option<String> {
1221    let trimmed = sql.trim();
1222    // `classify` matched `USE` as the first word, so the keyword is exactly
1223    // the first three bytes.
1224    let rest = trimmed.get(3..)?.trim().trim_end_matches(';').trim();
1225    if rest.is_empty() {
1226        return None;
1227    }
1228    Some(unquote(rest))
1229}
1230
1231/// Strips one layer of SQL single- or double-quoting, else takes the first
1232/// whitespace-delimited token.
1233fn unquote(value: &str) -> String {
1234    if let Some(inner) = value
1235        .strip_prefix('"')
1236        .and_then(|rest| rest.strip_suffix('"'))
1237    {
1238        inner.replace("\"\"", "\"")
1239    } else if let Some(inner) = value
1240        .strip_prefix('\'')
1241        .and_then(|rest| rest.strip_suffix('\''))
1242    {
1243        inner.replace("''", "'")
1244    } else {
1245        value.split_whitespace().next().unwrap_or("").to_owned()
1246    }
1247}
1248
1249/// The first whitespace-delimited token of a statement.
1250fn first_keyword(sql: &str) -> &str {
1251    sql.split_whitespace().next().unwrap_or("")
1252}
1253
1254/// Splits a query string into individual statements, respecting single- and
1255/// double-quoted strings and SQL comments. A trailing statement without a
1256/// terminating semicolon is included.
1257fn split_statements(input: &str) -> Vec<String> {
1258    #[derive(Clone, Copy, Eq, PartialEq)]
1259    enum State {
1260        Normal,
1261        SingleQuote,
1262        DoubleQuote,
1263        LineComment,
1264        BlockComment,
1265    }
1266    let bytes = input.as_bytes();
1267    let mut state = State::Normal;
1268    let mut statements = Vec::new();
1269    let mut start = 0;
1270    let mut index = 0;
1271    while index < bytes.len() {
1272        let byte = bytes[index];
1273        let next = bytes.get(index + 1).copied();
1274        match state {
1275            State::Normal => match (byte, next) {
1276                (b'\'', _) => state = State::SingleQuote,
1277                (b'"', _) => state = State::DoubleQuote,
1278                (b'-', Some(b'-')) => {
1279                    state = State::LineComment;
1280                    index += 1;
1281                }
1282                (b'/', Some(b'*')) => {
1283                    state = State::BlockComment;
1284                    index += 1;
1285                }
1286                (b';', _) => {
1287                    let statement = input[start..index].trim();
1288                    if has_sql_content(statement) {
1289                        statements.push(statement.to_owned());
1290                    }
1291                    start = index + 1;
1292                }
1293                _ => {}
1294            },
1295            State::SingleQuote => {
1296                if byte == b'\'' {
1297                    if next == Some(b'\'') {
1298                        index += 1;
1299                    } else {
1300                        state = State::Normal;
1301                    }
1302                }
1303            }
1304            State::DoubleQuote => {
1305                if byte == b'"' {
1306                    if next == Some(b'"') {
1307                        index += 1;
1308                    } else {
1309                        state = State::Normal;
1310                    }
1311                }
1312            }
1313            State::LineComment if byte == b'\n' => state = State::Normal,
1314            State::BlockComment if byte == b'*' && next == Some(b'/') => {
1315                state = State::Normal;
1316                index += 1;
1317            }
1318            State::LineComment | State::BlockComment => {}
1319        }
1320        index += 1;
1321    }
1322    let remainder = input[start..].trim();
1323    if has_sql_content(remainder) {
1324        statements.push(remainder.to_owned());
1325    }
1326    statements
1327}
1328
1329/// Whether a statement fragment holds anything other than whitespace and SQL
1330/// comments. A fragment made only of comments is an empty query, not a
1331/// statement to execute.
1332fn has_sql_content(segment: &str) -> bool {
1333    let bytes = segment.as_bytes();
1334    let mut index = 0;
1335    while index < bytes.len() {
1336        let byte = bytes[index];
1337        match (byte, bytes.get(index + 1).copied()) {
1338            (b'-', Some(b'-')) => {
1339                index += 2;
1340                while index < bytes.len() && bytes[index] != b'\n' {
1341                    index += 1;
1342                }
1343            }
1344            (b'/', Some(b'*')) => {
1345                index += 2;
1346                while index < bytes.len()
1347                    && !(bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/'))
1348                {
1349                    index += 1;
1350                }
1351                index += 2;
1352            }
1353            _ if byte.is_ascii_whitespace() => index += 1,
1354            _ => return true,
1355        }
1356    }
1357    false
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362    use super::*;
1363
1364    #[test]
1365    fn statement_splitter_handles_quotes_and_trailing_statement() {
1366        let statements = split_statements("SELECT ';'; SELECT \"a;b\"; SELECT 3");
1367        assert_eq!(statements, vec!["SELECT ';'", "SELECT \"a;b\"", "SELECT 3"]);
1368    }
1369
1370    #[test]
1371    fn empty_query_splits_to_nothing() {
1372        assert!(split_statements("   ;  -- comment\n").is_empty());
1373    }
1374
1375    #[test]
1376    fn control_statements_are_recognized_case_insensitively() {
1377        assert!(matches!(classify("begin"), Statement::Begin));
1378        assert!(matches!(
1379            classify("  SET client_encoding TO 'UTF8'"),
1380            Statement::Control("SET")
1381        ));
1382        assert!(matches!(classify("COMMIT"), Statement::Commit));
1383        assert!(matches!(classify("SELECT 1"), Statement::Query));
1384    }
1385
1386    #[test]
1387    fn use_and_show_are_recognized() {
1388        assert!(matches!(
1389            classify("show databases"),
1390            Statement::ShowDatabases
1391        ));
1392        match classify("USE \"my-db\"") {
1393            Statement::Use(name) => assert_eq!(name, "my-db"),
1394            _ => panic!("expected USE"),
1395        }
1396        match classify("use people;") {
1397            Statement::Use(name) => assert_eq!(name, "people"),
1398            _ => panic!("expected USE"),
1399        }
1400        // A bare `USE` with no target is left to fail as an ordinary query.
1401        assert!(matches!(classify("USE"), Statement::Query));
1402    }
1403
1404    #[test]
1405    fn command_tag_counts_selects_and_names_explains() {
1406        assert_eq!(command_tag("SELECT * FROM t", 7), "SELECT 7");
1407        assert_eq!(command_tag("EXPLAIN SELECT 1", 3), "EXPLAIN");
1408    }
1409
1410    #[test]
1411    fn placeholder_count_skips_every_postgres_quoting_form() {
1412        let sql = r#"
1413            SELECT $2, '$99', E'escaped \' $98', "$97",
1414                   $$ body $96 $$, $tag$ body $95 $tag$
1415            -- $94
1416            /* $93 /* nested $92 */ still comment */
1417            WHERE value = $7
1418        "#;
1419        assert_eq!(placeholder_count(sql), 7);
1420    }
1421}