Skip to main content

mz_pgtest/
lib.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! pgtest is a Postgres wire protocol tester using datadriven test files. It
11//! can be used to send [specific
12//! messages](https://www.postgresql.org/docs/current/protocol-message-formats.html)
13//! to any Postgres-compatible server and record received messages.
14//!
15//! The following datadriven directives are supported. They support a
16//! `conn=name` argument to specify a non-default connection.
17//! - `send`: Sends input messages to the server. Arguments, if needed, are
18//! specified using JSON. Refer to the associated types to see supported
19//! arguments. Arguments can be omitted to use defaults.
20//! - `until`: Waits until input messages have been received from the server.
21//! Additional messages are accumulated and returned as well.
22//! - `replace`: Registers a substitution that is applied to the output of every
23//! later `until`, on any connection, to mask values that vary between runs. The
24//! input's first line is a regex and its second line the replacement, which may
25//! use the `$1` capture-group syntax of the `regex` crate.
26//!
27//! The first time a `conn=name` argument is specified, `cluster=name` can also
28//! be specified to set the sessions cluster on initial connection.
29//!
30//! During debugging, set the environment variable `PGTEST_VERBOSE=1` to see
31//! messages sent and received.
32//!
33//! Supported `send` types:
34//! - [`Query`]
35//! - [`Parse`]
36//! - [`Describe`]
37//! - [`Bind`]
38//! - [`Execute`]
39//! - `Sync`
40//!
41//! Supported `until` arguments:
42//! - `no_error_fields` causes `ErrorResponse` messages to have empty contents.
43//! Useful when none of our fields match Postgres. For example `until
44//! no_error_fields`.
45//! - `err_field_typs` specifies the set of error message fields
46//! ([reference](https://www.postgresql.org/docs/current/protocol-error-fields.html)).
47//! The default is `CMS` (code, message, severity). For example: `until
48//! err_field_typs=SC` would return the severity and code fields in any
49//! ErrorResponse message.
50//! - `parameter_status` names the parameters whose `ParameterStatus` messages
51//! are recorded. All others are skipped. For example `until
52//! parameter_status=application_name`.
53//!
54//! For example, to execute a simple prepared statement:
55//! ```pgtest
56//! send
57//! Parse {"query": "SELECT $1::text, 1 + $2::int4"}
58//! Bind {"values": ["blah", "4"]}
59//! Execute
60//! Sync
61//! ----
62//!
63//! until
64//! ReadyForQuery
65//! ----
66//! ParseComplete
67//! BindComplete
68//! DataRow {"fields":["blah","5"]}
69//! CommandComplete {"tag":"SELECT 1"}
70//! ReadyForQuery {"status":"I"}
71//! ```
72//!
73//! # Usage while writing tests
74//!
75//! The expected way to use this while writing tests is to generate output from
76//! a postgres server. Use the `pgtest-mz` directory if our output differs
77//! incompatibly from postgres. Write your test, excluding any lines after the
78//! `----` of the `until` directive. For example:
79//! ```pgtest
80//! send
81//! Query {"query": "SELECT 1"}
82//! ----
83//!
84//! until
85//! ReadyForQuery
86//! ----
87//! ```
88//! Then run the pgtest binary, enabling rewrites and pointing it at postgres:
89//! ```shell
90//! REWRITE=1 cargo run --bin mz-pgtest -- test/pgtest/test.pt --addr localhost:5432 --user postgres
91//! ```
92//! This will generate the expected output for the `until` directive. Now rerun
93//! against a running Materialize server:
94//! ```shell
95//! cargo run --bin mz-pgtest -- test/pgtest/test.pt
96//! ```
97
98use std::collections::{BTreeMap, BTreeSet};
99use std::fmt;
100use std::io::{ErrorKind, Read, Write};
101use std::net::TcpStream;
102use std::time::{Duration, Instant};
103
104use anyhow::{anyhow, bail};
105use bytes::{BufMut, BytesMut};
106use mz_ore::collections::CollectionExt;
107use postgres_protocol::IsNull;
108use postgres_protocol::message::backend::Message;
109use postgres_protocol::message::frontend;
110use regex::Regex;
111use serde::{Deserialize, Serialize};
112use tokio_postgres::fallible_iterator::FallibleIterator;
113
114struct PgConn {
115    stream: TcpStream,
116    recv_buf: BytesMut,
117    send_buf: BytesMut,
118    timeout: Duration,
119    verbose: bool,
120}
121
122impl PgConn {
123    fn new<'a>(
124        addr: &str,
125        user: &'a str,
126        timeout: Duration,
127        verbose: bool,
128        mut options: Vec<(&'a str, &'a str)>,
129    ) -> anyhow::Result<Self> {
130        let mut conn = Self {
131            stream: TcpStream::connect(addr)?,
132            recv_buf: BytesMut::new(),
133            send_buf: BytesMut::new(),
134            timeout,
135            verbose,
136        };
137
138        conn.stream.set_read_timeout(Some(timeout))?;
139        options.insert(0, ("user", user));
140        options.insert(0, ("welcome_message", "off"));
141        conn.send(|buf| frontend::startup_message(options, buf).unwrap())?;
142        match conn.recv()?.1 {
143            Message::AuthenticationOk => {}
144            _ => bail!("expected AuthenticationOk"),
145        };
146        conn.until(
147            vec!["ReadyForQuery"],
148            vec!['C', 'S', 'M'],
149            BTreeSet::new(),
150            &BTreeSet::new(),
151        )?;
152        Ok(conn)
153    }
154
155    fn send<F: FnOnce(&mut BytesMut)>(&mut self, f: F) -> anyhow::Result<()> {
156        self.send_buf.clear();
157        f(&mut self.send_buf);
158        self.stream.write_all(&self.send_buf)?;
159        Ok(())
160    }
161    fn until(
162        &mut self,
163        until: Vec<&str>,
164        err_field_typs: Vec<char>,
165        ignore: BTreeSet<String>,
166        parameter_status: &BTreeSet<String>,
167    ) -> anyhow::Result<Vec<String>> {
168        let mut msgs = Vec::with_capacity(until.len());
169        for expect in until {
170            loop {
171                let (ch, msg) = match self.recv() {
172                    Ok((ch, msg)) => (ch, msg),
173                    Err(err) => bail!("{}: waiting for {}, saw {:#?}", err, expect, msgs),
174                };
175                let (typ, args) = match msg {
176                    Message::ReadyForQuery(body) => (
177                        "ReadyForQuery",
178                        serde_json::to_string(&ReadyForQuery {
179                            status: char::from(body.status()).to_string(),
180                        })?,
181                    ),
182                    Message::RowDescription(body) => (
183                        "RowDescription",
184                        serde_json::to_string(&RowDescription {
185                            fields: body
186                                .fields()
187                                .map(|f| {
188                                    Ok(Field {
189                                        name: f.name().to_string(),
190                                    })
191                                })
192                                .collect()
193                                .unwrap(),
194                        })?,
195                    ),
196                    Message::DataRow(body) => {
197                        let buf = body.buffer();
198                        (
199                            "DataRow",
200                            serde_json::to_string(&DataRow {
201                                fields: body
202                                    .ranges()
203                                    .map(|range| {
204                                        match range {
205                                            Some(range) => {
206                                                // Attempt to convert to a String. If not utf8, print as array of bytes instead.
207                                                Ok(String::from_utf8(
208                                                    buf[range.start..range.end].to_vec(),
209                                                )
210                                                .unwrap_or_else(|_| {
211                                                    format!(
212                                                        "{:?}",
213                                                        buf[range.start..range.end].to_vec()
214                                                    )
215                                                }))
216                                            }
217                                            None => Ok("NULL".into()),
218                                        }
219                                    })
220                                    .collect()
221                                    .unwrap(),
222                            })?,
223                        )
224                    }
225                    Message::CommandComplete(body) => (
226                        "CommandComplete",
227                        serde_json::to_string(&CommandComplete {
228                            tag: body.tag().unwrap().to_string(),
229                        })?,
230                    ),
231                    Message::ParseComplete => ("ParseComplete", "".to_string()),
232                    Message::BindComplete => ("BindComplete", "".to_string()),
233                    Message::PortalSuspended => ("PortalSuspended", "".to_string()),
234                    Message::ErrorResponse(body) => (
235                        "ErrorResponse",
236                        serde_json::to_string(&ErrorResponse {
237                            fields: body
238                                .fields()
239                                .filter_map(|f| {
240                                    let typ = char::from(f.type_());
241                                    if err_field_typs.contains(&typ) {
242                                        Ok(Some(ErrorField {
243                                            typ,
244                                            value: String::from_utf8_lossy(f.value_bytes())
245                                                .into_owned(),
246                                        }))
247                                    } else {
248                                        Ok(None)
249                                    }
250                                })
251                                .collect()
252                                .unwrap(),
253                        })?,
254                    ),
255                    Message::NoticeResponse(body) => (
256                        "NoticeResponse",
257                        serde_json::to_string(&ErrorResponse {
258                            fields: body
259                                .fields()
260                                .filter_map(|f| {
261                                    let typ = char::from(f.type_());
262                                    if err_field_typs.contains(&typ) {
263                                        Ok(Some(ErrorField {
264                                            typ,
265                                            value: String::from_utf8_lossy(f.value_bytes())
266                                                .into_owned(),
267                                        }))
268                                    } else {
269                                        Ok(None)
270                                    }
271                                })
272                                .collect()
273                                .unwrap(),
274                        })?,
275                    ),
276                    Message::CopyOutResponse(body) => (
277                        "CopyOut",
278                        serde_json::to_string(&CopyOut {
279                            format: format_name(body.format()),
280                            column_formats: body
281                                .column_formats()
282                                .map(|format| Ok(format_name(format)))
283                                .collect()
284                                .unwrap(),
285                        })?,
286                    ),
287                    Message::CopyInResponse(body) => (
288                        "CopyIn",
289                        serde_json::to_string(&CopyOut {
290                            format: format_name(body.format()),
291                            column_formats: body
292                                .column_formats()
293                                .map(|format| Ok(format_name(format)))
294                                .collect()
295                                .unwrap(),
296                        })?,
297                    ),
298                    Message::CopyData(body) => (
299                        "CopyData",
300                        serde_json::to_string(
301                            &std::str::from_utf8(body.data())
302                                .map(|s| s.to_string())
303                                .unwrap_or_else(|_| format!("{:?}", body.data())),
304                        )?,
305                    ),
306                    Message::CopyDone => ("CopyDone", "".to_string()),
307                    Message::ParameterDescription(body) => (
308                        "ParameterDescription",
309                        serde_json::to_string(&ParameterDescription {
310                            parameters: body.parameters().collect().unwrap(),
311                        })?,
312                    ),
313                    Message::ParameterStatus(body) => {
314                        let name = body.name()?;
315                        if !parameter_status.contains(name) {
316                            continue;
317                        }
318                        (
319                            "ParameterStatus",
320                            serde_json::to_string(&ParameterStatus {
321                                name: name.to_string(),
322                                value: body.value()?.to_string(),
323                            })?,
324                        )
325                    }
326                    Message::NoData => ("NoData", "".to_string()),
327                    Message::EmptyQueryResponse => ("EmptyQueryResponse", "".to_string()),
328                    _ => ("UNKNOWN", format!("'{}'", ch)),
329                };
330                if self.verbose {
331                    println!("RECV {}: {:?}", ch, typ);
332                }
333                if ignore.contains(typ) {
334                    continue;
335                }
336                let mut s = typ.to_string();
337                if !args.is_empty() {
338                    s.push(' ');
339                    s.push_str(&args);
340                }
341                msgs.push(s);
342                if expect == typ {
343                    break;
344                }
345            }
346        }
347        Ok(msgs)
348    }
349    /// Returns the PostgreSQL message format and the `Message`.
350    ///
351    /// An error is returned if a new message is not received within the timeout.
352    pub fn recv(&mut self) -> anyhow::Result<(char, Message)> {
353        let mut buf = [0; 1024];
354        let until = Instant::now();
355        loop {
356            if until.elapsed() > self.timeout {
357                bail!("timeout after {:?} waiting for new message", self.timeout);
358            }
359            let mut ch: char = '0';
360            if self.recv_buf.len() > 0 {
361                ch = char::from(self.recv_buf[0]);
362            }
363            if let Some(msg) = Message::parse(&mut self.recv_buf)? {
364                return Ok((ch, msg));
365            };
366            // If there was no message, read more bytes.
367            let sz = match self.stream.read(&mut buf) {
368                Ok(n) => n,
369                // According to the `read` docs, this is a non-fatal retryable error.
370                // https://doc.rust-lang.org/std/io/trait.Read.html#errors
371                Err(e) if e.kind() == ErrorKind::Interrupted => continue,
372                Err(e) => return Err(anyhow!(e)),
373            };
374            self.recv_buf.extend_from_slice(&buf[..sz]);
375        }
376    }
377}
378
379const DEFAULT_CONN: &str = "";
380
381pub struct PgTest {
382    addr: String,
383    user: String,
384    timeout: Duration,
385    conns: BTreeMap<String, PgConn>,
386    verbose: bool,
387}
388
389impl PgTest {
390    pub fn new(addr: String, user: String, timeout: Duration) -> anyhow::Result<Self> {
391        let verbose = std::env::var_os("PGTEST_VERBOSE").is_some();
392        let conn = PgConn::new(&addr, &user, timeout.clone(), verbose, vec![])?;
393        let mut conns = BTreeMap::new();
394        conns.insert(DEFAULT_CONN.to_string(), conn);
395
396        Ok(PgTest {
397            addr,
398            user,
399            timeout,
400            conns,
401            verbose,
402        })
403    }
404
405    // Returns the named connection. If this is the first time that connection is
406    // seen, sends options.
407    fn get_conn(
408        &mut self,
409        name: Option<String>,
410        options: Vec<(&str, &str)>,
411    ) -> anyhow::Result<&mut PgConn> {
412        let name = name.unwrap_or_else(|| DEFAULT_CONN.to_string());
413        if !self.conns.contains_key(&name) {
414            let conn = PgConn::new(
415                &self.addr,
416                &self.user,
417                self.timeout.clone(),
418                self.verbose,
419                options,
420            )?;
421            self.conns.insert(name.clone(), conn);
422        }
423        Ok(self.conns.get_mut(&name).expect("must exist"))
424    }
425
426    pub fn send<F: Fn(&mut BytesMut)>(
427        &mut self,
428        conn: Option<String>,
429        options: Vec<(&str, &str)>,
430        f: F,
431    ) -> anyhow::Result<()> {
432        let conn = self.get_conn(conn, options)?;
433        conn.send(f)
434    }
435
436    pub fn until(
437        &mut self,
438        conn: Option<String>,
439        options: Vec<(&str, &str)>,
440        until: Vec<&str>,
441        err_field_typs: Vec<char>,
442        ignore: BTreeSet<String>,
443        parameter_status: &BTreeSet<String>,
444    ) -> anyhow::Result<Vec<String>> {
445        let conn = self.get_conn(conn, options)?;
446        conn.until(until, err_field_typs, ignore, parameter_status)
447    }
448}
449
450// Backend messages
451
452#[derive(Serialize)]
453pub struct ReadyForQuery {
454    pub status: String,
455}
456
457#[derive(Serialize)]
458pub struct RowDescription {
459    pub fields: Vec<Field>,
460}
461
462#[derive(Serialize)]
463pub struct Field {
464    pub name: String,
465}
466
467#[derive(Serialize)]
468pub struct DataRow {
469    pub fields: Vec<String>,
470}
471
472#[derive(Serialize)]
473pub struct CopyOut {
474    pub format: String,
475    pub column_formats: Vec<String>,
476}
477
478#[derive(Serialize)]
479pub struct ParameterDescription {
480    parameters: Vec<u32>,
481}
482
483#[derive(Serialize)]
484pub struct CommandComplete {
485    pub tag: String,
486}
487
488#[derive(Serialize)]
489pub struct ParameterStatus {
490    pub name: String,
491    pub value: String,
492}
493
494#[derive(Serialize)]
495pub struct ErrorResponse {
496    pub fields: Vec<ErrorField>,
497}
498
499#[derive(Serialize)]
500pub struct ErrorField {
501    pub typ: char,
502    pub value: String,
503}
504
505impl Drop for PgTest {
506    fn drop(&mut self) {
507        for conn in self.conns.values_mut() {
508            let _ = conn.send(frontend::terminate);
509        }
510    }
511}
512
513fn format_name<T>(format: T) -> String
514where
515    T: Copy + TryInto<u16> + fmt::Display,
516{
517    match format.try_into() {
518        Ok(0) => "text".to_string(),
519        Ok(1) => "binary".to_string(),
520        _ => format!("unknown: {}", format),
521    }
522}
523
524pub fn walk(addr: String, user: String, timeout: Duration, dir: &str) {
525    datadriven::walk(dir, |tf| run_test(tf, addr.clone(), user.clone(), timeout));
526}
527
528pub fn run_test(tf: &mut datadriven::TestFile, addr: String, user: String, timeout: Duration) {
529    let mut pgt = PgTest::new(addr, user, timeout).unwrap();
530    let mut replacements: Vec<(Regex, String)> = Vec::new();
531    tf.run(|tc| -> String {
532        let lines = tc.input.lines();
533        let mut args = tc.args.clone();
534        let conn: Option<String> = args
535            .remove("conn")
536            .map(|args| Some(args.into_first()))
537            .unwrap_or(None);
538        let mut options: Vec<(&str, &str)> = Vec::new();
539        let cluster = args.remove("cluster");
540        if let Some(cluster) = &cluster {
541            let cluster = cluster.into_first();
542            options.push(("cluster", cluster.as_str()));
543        }
544        match tc.directive.as_str() {
545            "send" => {
546                for line in lines {
547                    if pgt.verbose {
548                        println!("SEND {}", line);
549                    }
550                    let mut line = line.splitn(2, ' ');
551                    let typ = line.next().unwrap_or("");
552                    let args = line.next().unwrap_or("{}");
553                    pgt.send(conn.clone(), options.clone(), |buf| match typ {
554                        "Query" => {
555                            let v: Query = serde_json::from_str(args).unwrap();
556                            frontend::query(&v.query, buf).unwrap();
557                        }
558                        "Parse" => {
559                            let v: Parse = serde_json::from_str(args).unwrap();
560                            frontend::parse(
561                                &v.name.unwrap_or_else(|| "".into()),
562                                &v.query,
563                                vec![],
564                                buf,
565                            )
566                            .unwrap();
567                        }
568                        "Sync" => frontend::sync(buf),
569                        "Bind" => {
570                            let v: Bind = serde_json::from_str(args).unwrap();
571                            // Parameter values as raw bytes. `binary_values`
572                            // supplies them verbatim, which (together with
573                            // `param_formats`) is what lets a test exercise the
574                            // binary parameter format; otherwise `values` are
575                            // sent as UTF-8 text, matching historical behavior.
576                            let values: Vec<Vec<u8>> = match v.binary_values {
577                                Some(binary_values) => binary_values,
578                                None => v
579                                    .values
580                                    .unwrap_or_default()
581                                    .into_iter()
582                                    .map(String::into_bytes)
583                                    .collect(),
584                            };
585                            if frontend::bind(
586                                &v.portal.unwrap_or_else(|| "".into()),
587                                &v.statement.unwrap_or_else(|| "".into()),
588                                v.param_formats.unwrap_or_default(), // formats
589                                values,                              // values
590                                |bytes: Vec<u8>, buf| {
591                                    buf.put_slice(&bytes);
592                                    Ok(IsNull::No)
593                                }, // serializer
594                                v.result_formats.unwrap_or_default(),
595                                buf,
596                            )
597                            .is_err()
598                            {
599                                panic!("bind error");
600                            }
601                        }
602                        "Describe" => {
603                            let v: Describe = serde_json::from_str(args).unwrap();
604                            frontend::describe(
605                                v.variant.unwrap_or_else(|| "S".into()).as_bytes()[0],
606                                &v.name.unwrap_or_else(|| "".into()),
607                                buf,
608                            )
609                            .unwrap();
610                        }
611                        "Execute" => {
612                            let v: Execute = serde_json::from_str(args).unwrap();
613                            frontend::execute(
614                                &v.portal.unwrap_or_else(|| "".into()),
615                                v.max_rows.unwrap_or(0),
616                                buf,
617                            )
618                            .unwrap();
619                        }
620                        "CopyData" => {
621                            let v: String = serde_json::from_str(args).unwrap();
622                            frontend::CopyData::new(v.as_bytes()).unwrap().write(buf);
623                        }
624                        "CopyDone" => {
625                            frontend::copy_done(buf);
626                        }
627                        "CopyFail" => {
628                            let v: String = serde_json::from_str(args).unwrap();
629                            frontend::copy_fail(&v, buf).unwrap();
630                        }
631                        _ => panic!("unknown message type {}", typ),
632                    })
633                    .unwrap();
634                }
635                "".to_string()
636            }
637            "until" => {
638                // Our error field values don't always match postgres. Default to reporting
639                // the error code (C) and message (M), but allow the user to specify which ones
640                // they want.
641                let err_field_typs = if let Some(_) = args.remove("no_error_fields") {
642                    vec![]
643                } else {
644                    match args.remove("err_field_typs") {
645                        Some(typs) => typs.join("").chars().collect(),
646                        None => vec!['C', 'S', 'M'],
647                    }
648                };
649                let mut ignore = BTreeSet::new();
650                if let Some(values) = args.remove("ignore") {
651                    for v in values {
652                        ignore.insert(v);
653                    }
654                }
655                let parameter_status: BTreeSet<String> = args
656                    .remove("parameter_status")
657                    .unwrap_or_default()
658                    .into_iter()
659                    .collect();
660                if !args.is_empty() {
661                    panic!("extra until arguments: {:?}", args);
662                }
663                let mut out = pgt
664                    .until(
665                        conn,
666                        options,
667                        lines.collect(),
668                        err_field_typs,
669                        ignore,
670                        &parameter_status,
671                    )
672                    .unwrap()
673                    .join("\n");
674                for (regex, replacement) in &replacements {
675                    out = regex.replace_all(&out, replacement.as_str()).into_owned();
676                }
677                format!("{}\n", out)
678            }
679            "replace" => {
680                let [pattern, replacement] = lines.collect::<Vec<_>>()[..] else {
681                    panic!(
682                        "replace takes two lines, a regex and its replacement: {:?}",
683                        tc.input
684                    );
685                };
686                replacements.push((Regex::new(pattern).unwrap(), replacement.to_string()));
687                String::new()
688            }
689            _ => panic!("unknown directive {}", tc.input),
690        }
691    })
692}
693
694// Frontend messages
695
696#[derive(Deserialize)]
697#[serde(deny_unknown_fields)]
698pub struct Query {
699    pub query: String,
700}
701
702#[derive(Deserialize)]
703#[serde(deny_unknown_fields)]
704pub struct Parse {
705    pub name: Option<String>,
706    pub query: String,
707}
708
709#[derive(Deserialize)]
710#[serde(deny_unknown_fields)]
711pub struct Bind {
712    pub portal: Option<String>,
713    pub statement: Option<String>,
714    pub values: Option<Vec<String>>,
715    /// Parameter format codes (0 = text, 1 = binary). Empty/absent means all
716    /// parameters use the text format, as before.
717    pub param_formats: Option<Vec<i16>>,
718    /// Raw parameter values, sent verbatim. Use this instead of `values` to put
719    /// arbitrary bytes (e.g. a binary-format parameter) on the wire.
720    pub binary_values: Option<Vec<Vec<u8>>>,
721    pub result_formats: Option<Vec<i16>>,
722}
723
724#[derive(Deserialize)]
725#[serde(deny_unknown_fields)]
726pub struct Execute {
727    pub portal: Option<String>,
728    pub max_rows: Option<i32>,
729}
730
731#[derive(Deserialize)]
732#[serde(deny_unknown_fields)]
733pub struct Describe {
734    pub variant: Option<String>,
735    pub name: Option<String>,
736}