beck_core/
pg.rs

1//! `pg_catalog`, as read models over the schema the program already derives.
2//!
3//! [`docs/23-incremental-views-report.md`](../../../../../docs/23-incremental-views-report.md)
4//! §23.19 held this open as "the correct long-run answer is a small read-only emulation of
5//! `pg_class` and friends", and [`docs/08`](../../../../../docs/08-roadmap.md)'s Phase 3 exit row
6//! carried it as the one thing a DBA still could not do: `psql`'s `\d` is a join over four
7//! catalogue relations, and those relations did not exist.
8//!
9//! # What this is
10//!
11//! Every relation here is a [`crate::read::Table`] like any other, with a
12//! [`crate::read::Source::Pg`] saying where its rows come from — which is *this module*, from the
13//! [`Schema`] the compiler derived. So `\d` is answered by the same parser, the same
14//! [`crate::plan::Op::Join`] and the same scan that answer `select * from todos`, and there is no
15//! branch anywhere in [`beck_rt::pgwire`](../../beck_rt/pgwire/index.html) that knows what a
16//! backslash command is. A catalogue that were a special case in the wire protocol would be a
17//! second query path to keep true; this one cannot disagree with the schema because it *is* the
18//! schema, read through a different set of column names.
19//!
20//! # What it is not
21//!
22//! It is not PostgreSQL's catalogue. There are no indexes, no constraints, no triggers, no
23//! sequences, no functions and no roles, because a read model has none of those things — so the
24//! relations that would describe them are here with their columns and **no rows**, which is the
25//! true answer rather than an absent one: `\d todos` asks about policies and publications on its
26//! way to printing a table, and "none" is what it needs to hear.
27//!
28//! A relation that is not here at all is refused **by name** ([`Rel::missing`]), because a client
29//! that is told "there is no `pg_catalog.pg_proc`" knows what happened and a client handed an empty
30//! answer does not.
31//!
32//! # The four decisions in the rows
33//!
34//! * **Every read model is `relkind = 'r'`** — an ordinary table.
35//!   [`docs/05`](../../../../../docs/05-tier-lowering.md) §5.3's promise is that a tool "sees
36//!   materialized views as ordinary tables", and a maintained arrangement answered as `'m'` would
37//!   send `psql` looking for a view definition this has no SQL to give it. What a table is derived
38//!   *from* is in [`Schema::CATALOGUE`], which is the table that answers that question.
39//! * **One namespace for the program, one for the catalogue.** The read models are in `public` and
40//!   these relations are in `pg_catalog`, which is what makes `\d` list the program's tables and
41//!   not its own: `psql` filters on `nspname <> 'pg_catalog'`, and that filter has to have
42//!   something to filter.
43//! * **The oids are positions, not identities.** A relation's oid is its index in a list that is
44//!   built from the schema and cannot change while the process runs. Nothing persists one.
45//! * **Owner and access method are constants**, because there is nobody to own a read model and no
46//!   index to choose a method for.
47//!
48//! Every column `psql` names is here; a column it does not name is not, so this file is short for
49//! the same reason the SQL is a subset.
50
51use std::sync::Arc;
52
53use crate::core::{Fields, Value};
54use crate::read::{Cardinality, Cell, Column, Datum, Schema, SqlTy, Table};
55
56/// The namespace a program's read models are in.
57pub const PUBLIC: &str = "public";
58/// The namespace these relations are in, and the one `psql` hides from `\d`.
59pub const CATALOG: &str = "pg_catalog";
60
61/// The oid of the `pg_catalog` namespace, and of `public` — PostgreSQL's own, because a client
62/// that hard-codes one hard-codes these.
63const NSP_CATALOG: i64 = 11;
64const NSP_PUBLIC: i64 = 2200;
65
66/// Where relation oids start. Above every oid PostgreSQL reserves for its own catalogue, which is
67/// where a real server's first user table lands too.
68const FIRST_OID: i64 = 16384;
69
70/// The one role every read model is owned by. There is no authentication on this port
71/// ([`adr/0020`](../../../../../docs/adr/0020-the-read-model-speaks-pgwire-by-hand.md)), so there
72/// is no user to be the owner and this is the name `pg_get_userbyid` answers with.
73pub const OWNER: &str = "beck";
74
75/// One relation of the emulated catalogue.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
77pub enum Rel {
78    Class,
79    Namespace,
80    Attribute,
81    Type,
82    Database,
83    /// The relations below have columns and no rows: a read model has no such object, and a query
84    /// that joins one is answered with the no rows that is the truth rather than refused.
85    Am,
86    Attrdef,
87    Collation,
88    Inherits,
89    Policy,
90    Publication,
91    PublicationNamespace,
92    PublicationRel,
93    StatisticExt,
94}
95
96/// Every relation, in the order [`relations`] builds them and therefore in oid order.
97pub const ALL: &[Rel] = &[
98    Rel::Class,
99    Rel::Namespace,
100    Rel::Attribute,
101    Rel::Type,
102    Rel::Database,
103    Rel::Am,
104    Rel::Attrdef,
105    Rel::Collation,
106    Rel::Inherits,
107    Rel::Policy,
108    Rel::Publication,
109    Rel::PublicationNamespace,
110    Rel::PublicationRel,
111    Rel::StatisticExt,
112];
113
114use SqlTy::{Bigint, Boolean, Text};
115
116/// A column of a catalogue relation: its name, its type, and whether this catalogue ever writes a
117/// NULL into it.
118///
119/// Nullability is a fact worth stating rather than a default, because a **join key must not be
120/// nullable**: this SQL's join is the index's own equality, where a null would match a null
121/// ([`crate::query`] refuses one by name). Every column `psql` joins on is an object identifier
122/// this module always writes, and every column that stands for something a read model does not
123/// have — an ACL, a stored expression, a locale — is a NULL and says so.
124type Col = (&'static str, SqlTy, bool);
125
126const SET: bool = false;
127const NULLABLE: bool = true;
128
129impl Rel {
130    pub fn name(self) -> &'static str {
131        match self {
132            Rel::Class => "pg_class",
133            Rel::Namespace => "pg_namespace",
134            Rel::Attribute => "pg_attribute",
135            Rel::Type => "pg_type",
136            Rel::Database => "pg_database",
137            Rel::Am => "pg_am",
138            Rel::Attrdef => "pg_attrdef",
139            Rel::Collation => "pg_collation",
140            Rel::Inherits => "pg_inherits",
141            Rel::Policy => "pg_policy",
142            Rel::Publication => "pg_publication",
143            Rel::PublicationNamespace => "pg_publication_namespace",
144            Rel::PublicationRel => "pg_publication_rel",
145            Rel::StatisticExt => "pg_statistic_ext",
146        }
147    }
148
149    /// The columns, in `pg_catalog`'s own order and spelling.
150    ///
151    /// An `oid`-shaped column is [`SqlTy::Bigint`] rather than a type of its own: PostgreSQL's
152    /// `oid` is a 32-bit unsigned integer, every comparison `psql` writes against one is against a
153    /// decimal literal, and a fifth SQL type would have to be a type OID a driver knows.
154    /// A `"char"` column — `relkind`, `relpersistence`, `attidentity` — is [`SqlTy::Text`] of one
155    /// character, which is what it compares as.
156    pub fn columns(self) -> &'static [Col] {
157        match self {
158            Rel::Class => &[
159                ("oid", Bigint, SET),
160                ("relname", Text, SET),
161                ("relnamespace", Bigint, SET),
162                ("relkind", Text, SET),
163                ("relowner", Bigint, SET),
164                ("relam", Bigint, SET),
165                ("reltablespace", Bigint, SET),
166                ("reltoastrelid", Bigint, SET),
167                ("reloftype", Bigint, SET),
168                ("relnatts", Bigint, SET),
169                ("reltuples", Bigint, SET),
170                ("relchecks", Bigint, SET),
171                ("relhasindex", Boolean, SET),
172                ("relhasrules", Boolean, SET),
173                ("relhastriggers", Boolean, SET),
174                ("relhassubclass", Boolean, SET),
175                ("relrowsecurity", Boolean, SET),
176                ("relforcerowsecurity", Boolean, SET),
177                ("relispartition", Boolean, SET),
178                ("relpersistence", Text, SET),
179                ("relreplident", Text, SET),
180                ("reloptions", Text, NULLABLE),
181                ("relpartbound", Text, NULLABLE),
182                ("relacl", Text, NULLABLE),
183            ],
184            Rel::Namespace => &[
185                ("oid", Bigint, SET),
186                ("nspname", Text, SET),
187                ("nspowner", Bigint, SET),
188                ("nspacl", Text, NULLABLE),
189            ],
190            Rel::Attribute => &[
191                ("attrelid", Bigint, SET),
192                ("attname", Text, SET),
193                ("atttypid", Bigint, SET),
194                ("atttypmod", Bigint, SET),
195                ("attnum", Bigint, SET),
196                ("attnotnull", Boolean, SET),
197                ("atthasdef", Boolean, SET),
198                ("attisdropped", Boolean, SET),
199                ("attislocal", Boolean, SET),
200                ("attcollation", Bigint, SET),
201                ("attstattarget", Bigint, SET),
202                ("attidentity", Text, SET),
203                ("attgenerated", Text, SET),
204                ("attcompression", Text, SET),
205            ],
206            Rel::Type => &[
207                ("oid", Bigint, SET),
208                ("typname", Text, SET),
209                ("typnamespace", Bigint, SET),
210                ("typtype", Text, SET),
211                ("typelem", Bigint, SET),
212                ("typcollation", Bigint, SET),
213            ],
214            Rel::Database => &[
215                ("oid", Bigint, SET),
216                ("datname", Text, SET),
217                ("datdba", Bigint, SET),
218                ("encoding", Bigint, SET),
219                ("datlocprovider", Text, SET),
220                ("datcollate", Text, SET),
221                ("datctype", Text, SET),
222                ("daticulocale", Text, NULLABLE),
223                ("datallowconn", Boolean, SET),
224                ("datistemplate", Boolean, SET),
225                ("datacl", Text, NULLABLE),
226            ],
227            Rel::Am => &[
228                ("oid", Bigint, SET),
229                ("amname", Text, SET),
230                ("amhandler", Bigint, NULLABLE),
231                ("amtype", Text, SET),
232            ],
233            Rel::Attrdef => &[
234                ("oid", Bigint, SET),
235                ("adrelid", Bigint, SET),
236                ("adnum", Bigint, SET),
237                ("adbin", Text, NULLABLE),
238            ],
239            Rel::Collation => &[
240                ("oid", Bigint, SET),
241                ("collname", Text, SET),
242                ("collnamespace", Bigint, SET),
243                ("collcollate", Text, NULLABLE),
244                ("collctype", Text, NULLABLE),
245            ],
246            Rel::Inherits => &[
247                ("inhrelid", Bigint, SET),
248                ("inhparent", Bigint, SET),
249                ("inhseqno", Bigint, SET),
250                ("inhdetachpending", Boolean, SET),
251            ],
252            Rel::Policy => &[
253                ("oid", Bigint, SET),
254                ("polname", Text, SET),
255                ("polrelid", Bigint, SET),
256                ("polcmd", Text, SET),
257                ("polpermissive", Boolean, SET),
258                ("polroles", Text, NULLABLE),
259                ("polqual", Text, NULLABLE),
260                ("polwithcheck", Text, NULLABLE),
261            ],
262            Rel::Publication => &[
263                ("oid", Bigint, SET),
264                ("pubname", Text, SET),
265                ("pubowner", Bigint, SET),
266                ("puballtables", Boolean, SET),
267            ],
268            Rel::PublicationNamespace => &[
269                ("oid", Bigint, SET),
270                ("pnpubid", Bigint, SET),
271                ("pnnspid", Bigint, SET),
272            ],
273            Rel::PublicationRel => &[
274                ("oid", Bigint, SET),
275                ("prpubid", Bigint, SET),
276                ("prrelid", Bigint, SET),
277                ("prqual", Text, NULLABLE),
278                ("prattrs", Text, NULLABLE),
279            ],
280            Rel::StatisticExt => &[
281                ("oid", Bigint, SET),
282                ("stxrelid", Bigint, SET),
283                ("stxnamespace", Bigint, SET),
284                ("stxname", Text, SET),
285                ("stxowner", Bigint, SET),
286                ("stxkind", Text, NULLABLE),
287                ("stxstattarget", Bigint, SET),
288            ],
289        }
290    }
291
292    /// What a client is told when it names a `pg_catalog` relation that is not here.
293    ///
294    /// By name, and with the list, because the alternative — an empty answer — is a client that
295    /// believes the program has no functions rather than one that knows this catalogue has no
296    /// `pg_proc`.
297    pub fn missing(name: &str) -> String {
298        format!(
299            "\"pg_catalog.{name}\" is not one of the catalogue relations this read model has. \
300             There is {}. `\\d`, `\\d <table>`, `\\dt`, `\\dn` and `\\l` are the backslash \
301             commands they answer; anything else asks for an object a read model does not have",
302            ALL.iter()
303                .map(|r| format!("\"{}\"", r.name()))
304                .collect::<Vec<_>>()
305                .join(", ")
306        )
307    }
308}
309
310/// The catalogue's relations as tables, in oid order.
311pub fn relations() -> Vec<Table> {
312    ALL.iter()
313        .map(|rel| Table {
314            name: Arc::from(rel.name()),
315            columns: rel
316                .columns()
317                .iter()
318                .map(|(n, ty, nullable)| Column {
319                    name: Arc::from(*n),
320                    ty: *ty,
321                    nullable: *nullable,
322                })
323                .collect(),
324            source: crate::read::Source::Pg(*rel),
325            cardinality: Cardinality::Many,
326            element: Arc::from(rel.name()),
327        })
328        .collect()
329}
330
331/// The oid of a relation: its position in the schema's own list, then in the catalogue's.
332///
333/// A function rather than a field because it is derived from the same order twice — once to build
334/// `pg_class` and once for `pg_attribute` to point at it — and two lists that had to agree by
335/// inspection is the thing this project keeps refusing to write.
336fn oids(schema: &Schema) -> Vec<(i64, &Table, i64)> {
337    let mut out = Vec::new();
338    let mut next = FIRST_OID;
339    for t in &schema.tables {
340        out.push((next, t, NSP_PUBLIC));
341        next += 1;
342    }
343    for t in &schema.pg {
344        out.push((next, t, NSP_CATALOG));
345        next += 1;
346    }
347    out
348}
349
350/// The type oid of a column, which is the OID that goes on the wire for it too
351/// ([`SqlTy::oid`]) — one mapping, so `format_type` cannot disagree with `RowDescription`.
352fn type_oid(ty: SqlTy) -> i64 {
353    ty.oid() as i64
354}
355
356/// One relation's rows, derived from the schema.
357///
358/// `O(tables + columns)` for every relation, and nothing is cached: the whole catalogue of a
359/// 40-table program is a few thousand small values, built once per query that reads it and thrown
360/// away with the answer. The alternative — a cache — would be a second copy of the schema to keep
361/// true, which is the thing this module exists to not be.
362pub fn rows(rel: Rel, schema: &Schema) -> Vec<Value> {
363    let relations = oids(schema);
364    match rel {
365        Rel::Class => relations
366            .iter()
367            .map(|(oid, t, nsp)| {
368                record(
369                    rel,
370                    [
371                        ("oid", Value::Int(*oid)),
372                        ("relname", Value::text(t.name.to_string())),
373                        ("relnamespace", Value::Int(*nsp)),
374                        // See the module docs: a read model is an ordinary table, whatever
375                        // maintains it.
376                        ("relkind", Value::text("r".into())),
377                        ("relowner", Value::Int(10)),
378                        ("relam", Value::Int(0)),
379                        ("reltablespace", Value::Int(0)),
380                        ("reltoastrelid", Value::Int(0)),
381                        ("reloftype", Value::Int(0)),
382                        ("relnatts", Value::Int(t.columns.len() as i64)),
383                        // -1 is "never analysed", which is the truth: nothing counts these rows
384                        // until somebody asks.
385                        ("reltuples", Value::Int(-1)),
386                        ("relchecks", Value::Int(0)),
387                        ("relhasindex", Value::Bool(false)),
388                        ("relhasrules", Value::Bool(false)),
389                        ("relhastriggers", Value::Bool(false)),
390                        ("relhassubclass", Value::Bool(false)),
391                        ("relrowsecurity", Value::Bool(false)),
392                        ("relforcerowsecurity", Value::Bool(false)),
393                        ("relispartition", Value::Bool(false)),
394                        ("relpersistence", Value::text("p".into())),
395                        ("relreplident", Value::text("d".into())),
396                        ("reloptions", Value::Unit),
397                        ("relpartbound", Value::Unit),
398                        ("relacl", Value::Unit),
399                    ],
400                )
401            })
402            .collect(),
403        Rel::Namespace => [(NSP_CATALOG, CATALOG), (NSP_PUBLIC, PUBLIC)]
404            .iter()
405            .map(|(oid, name)| {
406                record(
407                    rel,
408                    [
409                        ("oid", Value::Int(*oid)),
410                        ("nspname", Value::text((*name).into())),
411                        ("nspowner", Value::Int(10)),
412                        ("nspacl", Value::Unit),
413                    ],
414                )
415            })
416            .collect(),
417        Rel::Attribute => relations
418            .iter()
419            .flat_map(|(oid, t, _)| {
420                t.columns.iter().enumerate().map(move |(i, c)| {
421                    record(
422                        rel,
423                        [
424                            ("attrelid", Value::Int(*oid)),
425                            ("attname", Value::text(c.name.to_string())),
426                            ("atttypid", Value::Int(type_oid(c.ty))),
427                            // No type modifier: none of the four types has one.
428                            ("atttypmod", Value::Int(-1)),
429                            ("attnum", Value::Int(i as i64 + 1)),
430                            ("attnotnull", Value::Bool(!c.nullable)),
431                            ("atthasdef", Value::Bool(false)),
432                            ("attisdropped", Value::Bool(false)),
433                            ("attislocal", Value::Bool(true)),
434                            ("attcollation", Value::Int(0)),
435                            ("attstattarget", Value::Int(-1)),
436                            ("attidentity", Value::text(String::new())),
437                            ("attgenerated", Value::text(String::new())),
438                            ("attcompression", Value::text(String::new())),
439                        ],
440                    )
441                })
442            })
443            .collect(),
444        // The four types a Beck scalar maps onto, under the names PostgreSQL's own catalogue
445        // spells them with — `format_type` is what turns one into the name a person reads.
446        Rel::Type => [
447            (SqlTy::Boolean, "bool"),
448            (SqlTy::Bigint, "int8"),
449            (SqlTy::Text, "text"),
450            (SqlTy::Double, "float8"),
451        ]
452        .iter()
453        .map(|(ty, name)| {
454            record(
455                rel,
456                [
457                    ("oid", Value::Int(type_oid(*ty))),
458                    ("typname", Value::text((*name).into())),
459                    ("typnamespace", Value::Int(NSP_CATALOG)),
460                    ("typtype", Value::text("b".into())),
461                    ("typelem", Value::Int(0)),
462                    ("typcollation", Value::Int(0)),
463                ],
464            )
465        })
466        .collect(),
467        // One database, because a process serves one program's state.
468        Rel::Database => vec![record(
469            rel,
470            [
471                ("oid", Value::Int(FIRST_OID - 1)),
472                ("datname", Value::text(OWNER.into())),
473                ("datdba", Value::Int(10)),
474                // 6 is UTF8 in PostgreSQL's encoding table, which is the encoding this wire
475                // announces in its startup parameters.
476                ("encoding", Value::Int(6)),
477                ("datlocprovider", Value::text("c".into())),
478                ("datcollate", Value::text("C".into())),
479                ("datctype", Value::text("C".into())),
480                ("daticulocale", Value::Unit),
481                ("datallowconn", Value::Bool(true)),
482                ("datistemplate", Value::Bool(false)),
483                ("datacl", Value::Unit),
484            ],
485        )],
486        // The relations that describe what a read model does not have.
487        Rel::Am
488        | Rel::Attrdef
489        | Rel::Collation
490        | Rel::Inherits
491        | Rel::Policy
492        | Rel::Publication
493        | Rel::PublicationNamespace
494        | Rel::PublicationRel
495        | Rel::StatisticExt => Vec::new(),
496    }
497}
498
499fn record<const N: usize>(rel: Rel, fields: [(&str, Value); N]) -> Value {
500    debug_assert_eq!(
501        N,
502        rel.columns().len(),
503        "{} builds a row of {N} fields for {} columns",
504        rel.name(),
505        rel.columns().len()
506    );
507    let fields: Fields = fields
508        .into_iter()
509        .map(|(k, v)| (Arc::from(k), v))
510        .collect::<Vec<_>>()
511        .into_iter()
512        .collect();
513    Value::data(rel.name(), None, fields)
514}
515
516// -------------------------------------------------------------------------------------------
517// The functions a catalogue query calls
518// -------------------------------------------------------------------------------------------
519
520/// A `pg_catalog` function, applied to the arguments a row produced.
521///
522/// `None` means this catalogue has no function of that name, and the caller refuses it **by name**
523/// — the same rule as [`Rel::missing`], and for the same reason: `psql` writes these calls into
524/// queries it expects to work, so one that silently answered NULL would produce a `\d` that is
525/// wrong rather than one that says what it could not do.
526///
527/// Every one of them is a constant or a lookup. There is nothing to compute because there is
528/// nothing behind them: no roles, no search path, no defaults, no ACLs.
529pub fn call(name: &str, args: &[Cell]) -> Option<Result<Cell, String>> {
530    let text = |s: &str| Some(Ok(Some(Datum::Text(s.to_string()))));
531    let null = || Some(Ok(None));
532    let arg = |i: usize| args.get(i).cloned().flatten();
533    match name {
534        // There is one owner and one search path, so these are the same answer for every row.
535        "pg_get_userbyid" => text(OWNER),
536        "pg_table_is_visible" | "pg_type_is_visible" | "pg_function_is_visible" => {
537            Some(Ok(Some(Datum::Boolean(true))))
538        }
539        // Nothing is published and nothing is replicated.
540        "pg_relation_is_publishable" => Some(Ok(Some(Datum::Boolean(false)))),
541        // A column's type as a person reads it, from the same OID that goes on the wire for it.
542        "format_type" => Some(Ok(match arg(0) {
543            Some(Datum::Bigint(oid)) => [SqlTy::Boolean, SqlTy::Bigint, SqlTy::Text, SqlTy::Double]
544                .iter()
545                .find(|t| type_oid(**t) == oid)
546                .map(|t| Datum::Text(t.name().to_string())),
547            _ => None,
548        })),
549        // The stored form of an expression: a default, a partition bound, a policy's predicate.
550        // A read model has none of the three, and every row that could carry one is a row that
551        // does not exist.
552        "pg_get_expr" | "pg_get_constraintdef" | "pg_get_indexdef" | "pg_get_partkeydef" => null(),
553        "pg_encoding_to_char" => match arg(0) {
554            Some(Datum::Bigint(6)) => text("UTF8"),
555            _ => null(),
556        },
557        // An array joined into a string. Every array-shaped column here is NULL, and NULL is what
558        // PostgreSQL answers for one; an array with elements in it would be a fifth type.
559        "array_to_string" => match arg(0) {
560            None => null(),
561            Some(_) => Some(Err(
562                "`array_to_string` has nothing to join here: the catalogue's array-shaped columns \
563                 are all null, because a read model has no ACL, no policy roles and no statistics \
564                 kinds"
565                    .to_string(),
566            )),
567        },
568        "current_database" | "current_catalog" => text(OWNER),
569        "current_schema" => text(PUBLIC),
570        "current_user" | "session_user" | "user" => text(OWNER),
571        "version" => text(&crate::read::version()),
572        _ => None,
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    /// Every relation builds rows whose fields are exactly the columns it declares.
581    ///
582    /// The negative half is what this is for: a column added to [`Rel::columns`] and forgotten in
583    /// [`rows`] would be a column `psql` reads as NULL — a `\d` that prints a table with no
584    /// nullability rather than one that fails.
585    #[test]
586    fn every_row_has_every_column_of_its_relation() {
587        let schema = Schema {
588            tables: Vec::new(),
589            pg: relations(),
590        };
591        for rel in ALL {
592            let names: Vec<&str> = rel.columns().iter().map(|(n, ..)| *n).collect();
593            for row in rows(*rel, &schema) {
594                let Value::Data(d) = &row else {
595                    panic!("{} builds a row that is not a record", rel.name())
596                };
597                let mut got: Vec<&str> = d.fields.iter().map(|(k, _)| k.as_ref()).collect();
598                let mut want = names.clone();
599                got.sort_unstable();
600                want.sort_unstable();
601                assert_eq!(got, want, "{}", rel.name());
602            }
603        }
604    }
605
606    #[test]
607    fn a_relation_that_is_not_here_is_refused_by_name() {
608        let m = Rel::missing("pg_proc");
609        assert!(m.contains("pg_catalog.pg_proc"), "{m}");
610        assert!(m.contains("pg_class"), "{m}");
611    }
612}