Skip to main content

corium_pgwire/
lib.rs

1//! A `PostgreSQL` wire-protocol front end for read-only Corium SQL.
2//!
3//! [`serve`] accepts `PostgreSQL` client connections and answers their queries
4//! by running them through [`corium_sql::SqlSession`] against an immutable
5//! [`corium_db::Db`] value obtained from a [`DbCatalog`]. Because every query
6//! goes through `SqlSession`, the same read-only guarantee holds: DDL, DML,
7//! and session-mutating statements are rejected.
8//!
9//! One server exposes every database the catalog offers (subject to the
10//! catalog's own whitelist). A connection selects its database with the
11//! standard startup `database` parameter and can switch at any time with
12//! `USE <database>`; `SHOW DATABASES` lists what is available. The catalog is
13//! expected to open and cache databases lazily and share them across
14//! connections.
15//!
16//! Both the simple and extended query sub-protocols are supported, in the
17//! text wire format. Bound parameters and the binary format are not
18//! supported and are reported as errors. A handful of stateless control
19//! statements (`BEGIN`, `COMMIT`, `ROLLBACK`, `SET`, `RESET`, `DISCARD`) are
20//! accepted as no-ops so ordinary clients and drivers connect cleanly.
21
22mod protocol;
23mod types;
24
25use std::collections::HashMap;
26use std::future::Future;
27use std::sync::Arc;
28
29use corium_db::Db;
30use corium_sql::{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}
47
48/// Supplies the databases the server exposes.
49///
50/// Implementations are expected to open databases lazily and cache them so a
51/// database is shared across all client connections. [`db`](DbCatalog::db)
52/// returns a fresh immutable snapshot each call, the same way the `corium sql`
53/// shell captures a current `Db` per statement.
54#[async_trait::async_trait]
55pub trait DbCatalog: Send + Sync + 'static {
56    /// Names of the databases clients may connect to.
57    ///
58    /// # Errors
59    /// Returns [`CatalogError`] when the catalog cannot be enumerated.
60    async fn list(&self) -> Result<Vec<String>, CatalogError>;
61
62    /// A current snapshot of the database named `name`.
63    ///
64    /// # Errors
65    /// Returns [`CatalogError::NotFound`] when the database is unknown or not
66    /// permitted, and [`CatalogError::Unavailable`] when it cannot be opened.
67    async fn db(&self, name: &str) -> Result<Db, CatalogError>;
68}
69
70/// Server-wide configuration for the `PostgreSQL` front end.
71#[derive(Clone, Debug)]
72pub struct PgWireConfig {
73    /// If set, clients must send this cleartext password to connect. When
74    /// `None`, connections are trusted.
75    pub password: Option<String>,
76    /// `server_version` reported to clients in a `ParameterStatus` message.
77    pub server_version: String,
78}
79
80impl Default for PgWireConfig {
81    fn default() -> Self {
82        Self {
83            password: None,
84            server_version: concat!("16.0 (corium ", env!("CARGO_PKG_VERSION"), ")").to_owned(),
85        }
86    }
87}
88
89/// Serves the `PostgreSQL` wire protocol until `shutdown` resolves.
90///
91/// Each accepted connection is handled on its own task; per-connection
92/// failures are logged and do not stop the server.
93///
94/// # Errors
95/// Returns an error only if accepting a connection fails fatally.
96pub async fn serve<C, F>(
97    listener: TcpListener,
98    catalog: Arc<C>,
99    config: PgWireConfig,
100    shutdown: F,
101) -> std::io::Result<()>
102where
103    C: DbCatalog,
104    F: Future<Output = ()>,
105{
106    let config = Arc::new(config);
107    tokio::pin!(shutdown);
108    loop {
109        tokio::select! {
110            () = &mut shutdown => return Ok(()),
111            accepted = listener.accept() => {
112                let (stream, peer) = accepted?;
113                let catalog = Arc::clone(&catalog);
114                let config = Arc::clone(&config);
115                tokio::spawn(async move {
116                    let (read, write) = stream.into_split();
117                    let mut session = ConnectionSession::new(
118                        FrontendReader::new(read),
119                        BackendWriter::new(write),
120                        catalog,
121                        config,
122                    );
123                    if let Err(error) = session.run().await {
124                        tracing::debug!(%peer, %error, "pgwire connection closed");
125                    }
126                });
127            }
128        }
129    }
130}
131
132/// A bound portal: the SQL to run and the database it was bound against.
133struct Portal {
134    sql: String,
135    database: Option<String>,
136}
137
138/// How a statement should be handled before it ever reaches `SqlSession`.
139enum Statement {
140    /// A stateless control statement accepted as a no-op with this tag.
141    Control(&'static str),
142    /// `USE <database>` — switch the connection's active database.
143    Use(String),
144    /// `SHOW DATABASES` — list the catalog.
145    ShowDatabases,
146    /// An ordinary read-only query for `SqlSession`.
147    Query,
148}
149
150/// A failure while dispatching one statement.
151enum Dispatch {
152    /// No database is selected for a query.
153    NoDatabase,
154    /// The catalog could not provide the database.
155    Catalog(CatalogError),
156    /// `SqlSession` rejected or failed the query.
157    Sql(SqlError),
158}
159
160/// The per-connection protocol state machine.
161struct ConnectionSession<R, W, C> {
162    reader: FrontendReader<R>,
163    writer: BackendWriter<W>,
164    catalog: Arc<C>,
165    config: Arc<PgWireConfig>,
166    /// The connection's active database, chosen at startup or by `USE`.
167    current_db: Option<String>,
168    statements: HashMap<String, String>,
169    portals: HashMap<String, Portal>,
170    /// Set after an extended-protocol error; frontend messages are ignored
171    /// until the next `Sync`.
172    failed: bool,
173}
174
175impl<R, W, C> ConnectionSession<R, W, C>
176where
177    R: AsyncRead + Unpin,
178    W: AsyncWrite + Unpin,
179    C: DbCatalog,
180{
181    fn new(
182        reader: FrontendReader<R>,
183        writer: BackendWriter<W>,
184        catalog: Arc<C>,
185        config: Arc<PgWireConfig>,
186    ) -> Self {
187        Self {
188            reader,
189            writer,
190            catalog,
191            config,
192            current_db: None,
193            statements: HashMap::new(),
194            portals: HashMap::new(),
195            failed: false,
196        }
197    }
198
199    async fn run(&mut self) -> std::io::Result<()> {
200        let startup = self.reader.read_startup(&mut self.writer).await?;
201        if !self.authenticate().await? {
202            return Ok(());
203        }
204        // The database is validated lazily on first use, so a client may
205        // connect with an unknown default and then `USE` a real database.
206        self.current_db = startup
207            .get("database")
208            .filter(|value| !value.is_empty())
209            .map(str::to_owned);
210        self.send_ready_banner(startup.get("application_name").unwrap_or(""))
211            .await?;
212
213        while let Some(message) = self.reader.read_message().await? {
214            match message {
215                Frontend::Query(sql) => {
216                    // After an extended-protocol error the backend ignores
217                    // every message until the next `Sync`.
218                    if !self.failed {
219                        self.simple_query(&sql).await?;
220                        self.writer.flush().await?;
221                    }
222                }
223                Frontend::Parse {
224                    name,
225                    query,
226                    parameter_count,
227                } => self.handle_parse(name, query, parameter_count),
228                Frontend::Bind {
229                    portal,
230                    statement,
231                    parameter_count,
232                    result_formats,
233                } => self.handle_bind(&portal, &statement, parameter_count, &result_formats),
234                Frontend::Describe { kind, name } => self.handle_describe(kind, &name).await?,
235                Frontend::Execute { portal } => self.handle_execute(&portal).await?,
236                Frontend::Close { kind, name } => {
237                    if !self.failed {
238                        if kind == b'S' {
239                            self.statements.remove(&name);
240                        } else {
241                            self.portals.remove(&name);
242                        }
243                        self.writer.close_complete();
244                    }
245                }
246                Frontend::Sync => {
247                    self.failed = false;
248                    self.writer.ready_for_query(b'I');
249                    self.writer.flush().await?;
250                }
251                Frontend::Flush => self.writer.flush().await?,
252                Frontend::Password(_) => {}
253                Frontend::Terminate => break,
254            }
255        }
256        Ok(())
257    }
258
259    /// Runs the authentication exchange. Returns `false` if the connection
260    /// should be closed (bad password).
261    async fn authenticate(&mut self) -> std::io::Result<bool> {
262        let Some(expected) = self.config.password.clone() else {
263            self.writer.authentication_ok();
264            return Ok(true);
265        };
266        self.writer.authentication_cleartext_password();
267        self.writer.flush().await?;
268        match self.reader.read_message().await? {
269            Some(Frontend::Password(supplied)) if supplied == expected => {
270                self.writer.authentication_ok();
271                Ok(true)
272            }
273            _ => {
274                self.writer.error_response(&ErrorFields {
275                    code: "28P01",
276                    message: "password authentication failed",
277                });
278                self.writer.flush().await?;
279                Ok(false)
280            }
281        }
282    }
283
284    /// Sends the post-authentication parameter status, key data, and the
285    /// first `ReadyForQuery`.
286    async fn send_ready_banner(&mut self, application_name: &str) -> std::io::Result<()> {
287        self.writer
288            .parameter_status("server_version", &self.config.server_version);
289        self.writer.parameter_status("server_encoding", "UTF8");
290        self.writer.parameter_status("client_encoding", "UTF8");
291        self.writer.parameter_status("DateStyle", "ISO, MDY");
292        self.writer.parameter_status("TimeZone", "UTC");
293        self.writer.parameter_status("integer_datetimes", "on");
294        self.writer
295            .parameter_status("standard_conforming_strings", "on");
296        self.writer
297            .parameter_status("application_name", application_name);
298        self.writer.backend_key_data(0, 0);
299        self.writer.ready_for_query(b'I');
300        self.writer.flush().await
301    }
302
303    /// Handles a simple-query message: run each statement, stopping at the
304    /// first error, then report `ReadyForQuery`.
305    async fn simple_query(&mut self, sql: &str) -> std::io::Result<()> {
306        let statements = split_statements(sql);
307        if statements.is_empty() {
308            self.writer.empty_query_response();
309            self.writer.ready_for_query(b'I');
310            return Ok(());
311        }
312        for statement in statements {
313            if !self.run_simple_statement(&statement).await? {
314                break;
315            }
316        }
317        self.writer.ready_for_query(b'I');
318        Ok(())
319    }
320
321    /// Runs one simple-protocol statement. Returns `false` when an error was
322    /// reported and the rest of the query string should be abandoned.
323    async fn run_simple_statement(&mut self, sql: &str) -> std::io::Result<bool> {
324        match classify(sql) {
325            Statement::Control(tag) => {
326                self.writer.command_complete(tag);
327                Ok(true)
328            }
329            Statement::Use(name) => match self.use_database(&name).await {
330                Ok(()) => {
331                    self.writer.command_complete("USE");
332                    Ok(true)
333                }
334                Err(error) => {
335                    self.report_dispatch(&error);
336                    Ok(false)
337                }
338            },
339            Statement::ShowDatabases => match self.show_databases().await {
340                Ok(()) => Ok(true),
341                Err(error) => {
342                    self.report_dispatch(&error);
343                    Ok(false)
344                }
345            },
346            Statement::Query => {
347                let db = match self.snapshot(None).await {
348                    Ok(db) => db,
349                    Err(error) => {
350                        self.report_dispatch(&error);
351                        return Ok(false);
352                    }
353                };
354                match self.run_statement(&db, sql, true).await {
355                    Ok(rows) => {
356                        self.writer.command_complete(&command_tag(sql, rows));
357                        Ok(true)
358                    }
359                    Err(error) => {
360                        self.report_dispatch(&Dispatch::Sql(error));
361                        Ok(false)
362                    }
363                }
364            }
365        }
366    }
367
368    fn handle_parse(&mut self, name: String, query: String, parameter_count: usize) {
369        if self.failed {
370            return;
371        }
372        if parameter_count > 0 {
373            self.fail_extended("0A000", "bound parameters are not supported");
374            return;
375        }
376        self.statements.insert(name, query);
377        self.writer.parse_complete();
378    }
379
380    fn handle_bind(
381        &mut self,
382        portal: &str,
383        statement: &str,
384        parameter_count: usize,
385        result_formats: &[i16],
386    ) {
387        if self.failed {
388            return;
389        }
390        if parameter_count > 0 {
391            self.fail_extended("0A000", "bound parameters are not supported");
392            return;
393        }
394        if result_formats.contains(&1) {
395            self.fail_extended("0A000", "binary result format is not supported");
396            return;
397        }
398        let Some(sql) = self.statements.get(statement) else {
399            self.fail_extended("26000", "prepared statement does not exist");
400            return;
401        };
402        self.portals.insert(
403            portal.to_owned(),
404            Portal {
405                sql: sql.clone(),
406                database: self.current_db.clone(),
407            },
408        );
409        self.writer.bind_complete();
410    }
411
412    async fn handle_describe(&mut self, kind: u8, name: &str) -> std::io::Result<()> {
413        if self.failed {
414            return Ok(());
415        }
416        // `Describe` of a prepared statement first reports its parameters.
417        let (sql, database) = if kind == b'S' {
418            self.writer.parameter_description_empty();
419            let Some(sql) = self.statements.get(name) else {
420                self.fail_extended("26000", "prepared statement does not exist");
421                return Ok(());
422            };
423            (sql.clone(), self.current_db.clone())
424        } else {
425            let Some(portal) = self.portals.get(name) else {
426                self.fail_extended("34000", "portal does not exist");
427                return Ok(());
428            };
429            (portal.sql.clone(), portal.database.clone())
430        };
431        match classify(&sql) {
432            Statement::ShowDatabases => self.writer.row_description(&[database_field()]),
433            Statement::Query if !sql.trim().is_empty() => {
434                let db = match self.snapshot(database.as_deref()).await {
435                    Ok(db) => db,
436                    Err(error) => {
437                        self.fail_dispatch(&error);
438                        return Ok(());
439                    }
440                };
441                match self.describe_columns(&db, &sql).await {
442                    Ok(fields) => self.writer.row_description(&fields),
443                    Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
444                }
445            }
446            _ => self.writer.no_data(),
447        }
448        Ok(())
449    }
450
451    async fn handle_execute(&mut self, portal: &str) -> std::io::Result<()> {
452        if self.failed {
453            return Ok(());
454        }
455        let Some((sql, database)) = self
456            .portals
457            .get(portal)
458            .map(|portal| (portal.sql.clone(), portal.database.clone()))
459        else {
460            self.fail_extended("34000", "portal does not exist");
461            return Ok(());
462        };
463        if sql.trim().is_empty() {
464            self.writer.empty_query_response();
465            return Ok(());
466        }
467        match classify(&sql) {
468            Statement::Control(tag) => self.writer.command_complete(tag),
469            Statement::Use(name) => match self.use_database(&name).await {
470                Ok(()) => self.writer.command_complete("USE"),
471                Err(error) => self.fail_dispatch(&error),
472            },
473            Statement::ShowDatabases => {
474                if let Err(error) = self.show_databases().await {
475                    self.fail_dispatch(&error);
476                }
477            }
478            Statement::Query => {
479                let db = match self.snapshot(database.as_deref()).await {
480                    Ok(db) => db,
481                    Err(error) => {
482                        self.fail_dispatch(&error);
483                        return Ok(());
484                    }
485                };
486                match self.run_statement(&db, &sql, false).await {
487                    Ok(rows) => self.writer.command_complete(&command_tag(&sql, rows)),
488                    Err(error) => self.fail_dispatch(&Dispatch::Sql(error)),
489                }
490            }
491        }
492        Ok(())
493    }
494
495    /// Validates and activates `name` as the connection's database, warming
496    /// the catalog cache in the process.
497    async fn use_database(&mut self, name: &str) -> Result<(), Dispatch> {
498        self.snapshot(Some(name)).await?;
499        self.current_db = Some(name.to_owned());
500        Ok(())
501    }
502
503    /// Emits a one-column `database` result listing the catalog.
504    async fn show_databases(&mut self) -> Result<(), Dispatch> {
505        let names = self.catalog.list().await.map_err(Dispatch::Catalog)?;
506        self.writer.row_description(&[database_field()]);
507        for name in &names {
508            self.writer.data_row(&[Some(name.clone().into_bytes())]);
509        }
510        self.writer.command_complete("SHOW");
511        Ok(())
512    }
513
514    /// Resolves an immutable snapshot for `database`, falling back to the
515    /// connection's active database.
516    async fn snapshot(&self, database: Option<&str>) -> Result<Db, Dispatch> {
517        let name = database
518            .map(str::to_owned)
519            .or_else(|| self.current_db.clone())
520            .ok_or(Dispatch::NoDatabase)?;
521        self.catalog.db(&name).await.map_err(Dispatch::Catalog)
522    }
523
524    /// Plans a query and returns its result columns without streaming rows.
525    async fn describe_columns(
526        &self,
527        db: &Db,
528        sql: &str,
529    ) -> Result<Vec<FieldDescription>, SqlError> {
530        let session = SqlSession::new(db)?;
531        let query = session.query(sql).await?;
532        Ok(query.columns().iter().map(field_of).collect())
533    }
534
535    /// Runs one statement, optionally emitting a `RowDescription` first, then
536    /// streaming its rows as `DataRow` messages. Returns the row count.
537    async fn run_statement(
538        &mut self,
539        db: &Db,
540        sql: &str,
541        with_row_description: bool,
542    ) -> Result<usize, SqlError> {
543        let session = SqlSession::new(db)?;
544        let mut query = session.query(sql).await?;
545        if with_row_description {
546            let fields = query.columns().iter().map(field_of).collect::<Vec<_>>();
547            self.writer.row_description(&fields);
548        }
549        let mut count = 0usize;
550        while let Some(row) = query.next_row().await? {
551            let values = row.iter().map(types::encode_value).collect::<Vec<_>>();
552            self.writer.data_row(&values);
553            count += 1;
554            // Bound peak memory on large results by flushing periodically.
555            if count.is_multiple_of(1024) {
556                self.writer
557                    .flush()
558                    .await
559                    .map_err(|error| SqlError::Schema(error.to_string()))?;
560            }
561        }
562        Ok(count)
563    }
564
565    /// Emits an `ErrorResponse` for a simple-query dispatch failure.
566    fn report_dispatch(&mut self, error: &Dispatch) {
567        let (code, message) = dispatch_error_fields(error);
568        self.writer.error_response(&ErrorFields {
569            code,
570            message: &message,
571        });
572    }
573
574    /// Emits an `ErrorResponse` and enters the skip-until-`Sync` state.
575    fn fail_dispatch(&mut self, error: &Dispatch) {
576        self.report_dispatch(error);
577        self.failed = true;
578    }
579
580    /// Emits an `ErrorResponse` with an explicit code and enters the
581    /// skip-until-`Sync` state.
582    fn fail_extended(&mut self, code: &str, message: &str) {
583        self.writer.error_response(&ErrorFields { code, message });
584        self.failed = true;
585    }
586}
587
588/// The `RowDescription` field for the `SHOW DATABASES` result column.
589fn database_field() -> FieldDescription {
590    let type_oid = types::type_oid(&SqlType::Text);
591    FieldDescription {
592        name: "database".to_owned(),
593        type_oid,
594        type_len: types::type_len(type_oid),
595    }
596}
597
598/// Builds a `RowDescription` field from a result column.
599fn field_of(column: &SqlColumn) -> FieldDescription {
600    let type_oid = types::type_oid(&column.data_type);
601    FieldDescription {
602        name: column.name.clone(),
603        type_oid,
604        type_len: types::type_len(type_oid),
605    }
606}
607
608/// The `SQLSTATE` code and message an error is reported with.
609fn dispatch_error_fields(error: &Dispatch) -> (&'static str, String) {
610    match error {
611        Dispatch::NoDatabase => (
612            "3D000",
613            "no database selected; run \"USE <database>\" first".to_owned(),
614        ),
615        Dispatch::Catalog(error @ CatalogError::NotFound(_)) => ("3D000", error.to_string()),
616        Dispatch::Catalog(error @ CatalogError::Unavailable(_)) => ("08006", error.to_string()),
617        Dispatch::Sql(error) => (sqlstate_for(error), error.to_string()),
618    }
619}
620
621/// Chooses a `SQLSTATE` code for a SQL error.
622fn sqlstate_for(error: &SqlError) -> &'static str {
623    match error {
624        // Missing table / projection problems.
625        SqlError::Schema(_) => "42P01",
626        // Parse, plan, and execution failures.
627        SqlError::DataFusion(_) | SqlError::Arrow(_) => "42601",
628    }
629}
630
631/// The `CommandComplete` tag for a statement that returned `rows` rows.
632fn command_tag(sql: &str, rows: usize) -> String {
633    if first_keyword(sql).eq_ignore_ascii_case("explain") {
634        "EXPLAIN".to_owned()
635    } else {
636        format!("SELECT {rows}")
637    }
638}
639
640/// Classifies a statement so `USE`, `SHOW DATABASES`, and no-op control
641/// statements are handled before reaching `SqlSession`.
642fn classify(sql: &str) -> Statement {
643    let mut words = sql.split_whitespace();
644    let first = words.next().unwrap_or("").to_ascii_uppercase();
645    match first.as_str() {
646        "USE" => parse_use_target(sql).map_or(Statement::Query, Statement::Use),
647        "SHOW"
648            if words
649                .next()
650                .is_some_and(|word| word.eq_ignore_ascii_case("databases")) =>
651        {
652            Statement::ShowDatabases
653        }
654        "BEGIN" | "START" => Statement::Control("BEGIN"),
655        "COMMIT" | "END" => Statement::Control("COMMIT"),
656        "ROLLBACK" | "ABORT" => Statement::Control("ROLLBACK"),
657        "SET" => Statement::Control("SET"),
658        "RESET" => Statement::Control("RESET"),
659        "DISCARD" => Statement::Control("DISCARD ALL"),
660        _ => Statement::Query,
661    }
662}
663
664/// Extracts the database name from a `USE <database>` statement.
665fn parse_use_target(sql: &str) -> Option<String> {
666    let trimmed = sql.trim();
667    // `classify` matched `USE` as the first word, so the keyword is exactly
668    // the first three bytes.
669    let rest = trimmed.get(3..)?.trim().trim_end_matches(';').trim();
670    if rest.is_empty() {
671        return None;
672    }
673    Some(unquote(rest))
674}
675
676/// Strips one layer of SQL single- or double-quoting, else takes the first
677/// whitespace-delimited token.
678fn unquote(value: &str) -> String {
679    if let Some(inner) = value
680        .strip_prefix('"')
681        .and_then(|rest| rest.strip_suffix('"'))
682    {
683        inner.replace("\"\"", "\"")
684    } else if let Some(inner) = value
685        .strip_prefix('\'')
686        .and_then(|rest| rest.strip_suffix('\''))
687    {
688        inner.replace("''", "'")
689    } else {
690        value.split_whitespace().next().unwrap_or("").to_owned()
691    }
692}
693
694/// The first whitespace-delimited token of a statement.
695fn first_keyword(sql: &str) -> &str {
696    sql.split_whitespace().next().unwrap_or("")
697}
698
699/// Splits a query string into individual statements, respecting single- and
700/// double-quoted strings and SQL comments. A trailing statement without a
701/// terminating semicolon is included.
702fn split_statements(input: &str) -> Vec<String> {
703    #[derive(Clone, Copy, Eq, PartialEq)]
704    enum State {
705        Normal,
706        SingleQuote,
707        DoubleQuote,
708        LineComment,
709        BlockComment,
710    }
711    let bytes = input.as_bytes();
712    let mut state = State::Normal;
713    let mut statements = Vec::new();
714    let mut start = 0;
715    let mut index = 0;
716    while index < bytes.len() {
717        let byte = bytes[index];
718        let next = bytes.get(index + 1).copied();
719        match state {
720            State::Normal => match (byte, next) {
721                (b'\'', _) => state = State::SingleQuote,
722                (b'"', _) => state = State::DoubleQuote,
723                (b'-', Some(b'-')) => {
724                    state = State::LineComment;
725                    index += 1;
726                }
727                (b'/', Some(b'*')) => {
728                    state = State::BlockComment;
729                    index += 1;
730                }
731                (b';', _) => {
732                    let statement = input[start..index].trim();
733                    if has_sql_content(statement) {
734                        statements.push(statement.to_owned());
735                    }
736                    start = index + 1;
737                }
738                _ => {}
739            },
740            State::SingleQuote => {
741                if byte == b'\'' {
742                    if next == Some(b'\'') {
743                        index += 1;
744                    } else {
745                        state = State::Normal;
746                    }
747                }
748            }
749            State::DoubleQuote => {
750                if byte == b'"' {
751                    if next == Some(b'"') {
752                        index += 1;
753                    } else {
754                        state = State::Normal;
755                    }
756                }
757            }
758            State::LineComment if byte == b'\n' => state = State::Normal,
759            State::BlockComment if byte == b'*' && next == Some(b'/') => {
760                state = State::Normal;
761                index += 1;
762            }
763            State::LineComment | State::BlockComment => {}
764        }
765        index += 1;
766    }
767    let remainder = input[start..].trim();
768    if has_sql_content(remainder) {
769        statements.push(remainder.to_owned());
770    }
771    statements
772}
773
774/// Whether a statement fragment holds anything other than whitespace and SQL
775/// comments. A fragment made only of comments is an empty query, not a
776/// statement to execute.
777fn has_sql_content(segment: &str) -> bool {
778    let bytes = segment.as_bytes();
779    let mut index = 0;
780    while index < bytes.len() {
781        let byte = bytes[index];
782        match (byte, bytes.get(index + 1).copied()) {
783            (b'-', Some(b'-')) => {
784                index += 2;
785                while index < bytes.len() && bytes[index] != b'\n' {
786                    index += 1;
787                }
788            }
789            (b'/', Some(b'*')) => {
790                index += 2;
791                while index < bytes.len()
792                    && !(bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/'))
793                {
794                    index += 1;
795                }
796                index += 2;
797            }
798            _ if byte.is_ascii_whitespace() => index += 1,
799            _ => return true,
800        }
801    }
802    false
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    #[test]
810    fn statement_splitter_handles_quotes_and_trailing_statement() {
811        let statements = split_statements("SELECT ';'; SELECT \"a;b\"; SELECT 3");
812        assert_eq!(statements, vec!["SELECT ';'", "SELECT \"a;b\"", "SELECT 3"]);
813    }
814
815    #[test]
816    fn empty_query_splits_to_nothing() {
817        assert!(split_statements("   ;  -- comment\n").is_empty());
818    }
819
820    #[test]
821    fn control_statements_are_recognized_case_insensitively() {
822        assert!(matches!(classify("begin"), Statement::Control("BEGIN")));
823        assert!(matches!(
824            classify("  SET client_encoding TO 'UTF8'"),
825            Statement::Control("SET")
826        ));
827        assert!(matches!(classify("COMMIT"), Statement::Control("COMMIT")));
828        assert!(matches!(classify("SELECT 1"), Statement::Query));
829    }
830
831    #[test]
832    fn use_and_show_are_recognized() {
833        assert!(matches!(
834            classify("show databases"),
835            Statement::ShowDatabases
836        ));
837        match classify("USE \"my-db\"") {
838            Statement::Use(name) => assert_eq!(name, "my-db"),
839            _ => panic!("expected USE"),
840        }
841        match classify("use people;") {
842            Statement::Use(name) => assert_eq!(name, "people"),
843            _ => panic!("expected USE"),
844        }
845        // A bare `USE` with no target is left to fail as an ordinary query.
846        assert!(matches!(classify("USE"), Statement::Query));
847    }
848
849    #[test]
850    fn command_tag_counts_selects_and_names_explains() {
851        assert_eq!(command_tag("SELECT * FROM t", 7), "SELECT 7");
852        assert_eq!(command_tag("EXPLAIN SELECT 1", 3), "EXPLAIN");
853    }
854}