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