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}