Skip to main content

nedb_engine/
pgcatalog.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! `pg_catalog` and `information_schema` as REAL QUERYABLE TABLES.
6//!
7//! # Why this exists
8//!
9//! `\dt` in psql and the schema browser in DBeaver came back **empty**. That
10//! is an evaluator's first ten minutes, and an empty table list does not read
11//! as "unsupported" — it reads as "this database is broken" or "my data is
12//! gone".
13//!
14//! # Why it is built this way
15//!
16//! The cheap implementation is to recognise psql's exact query text and answer
17//! it from a fixed table. Several pgwire-compatible engines do that. It is the
18//! wrong choice here for a specific reason: **it breaks silently.** psql
19//! changes its catalogue queries between versions, and when the pattern stops
20//! matching, the result is an empty table list — indistinguishable from a
21//! database that genuinely has no tables. That is the exact class of
22//! confidently-wrong answer this engine has spent its life removing.
23//!
24//! So the catalogue is a set of real tables, synthesised from the live
25//! database, and queried through the ordinary predicate path
26//! (`nql::query_rows`). `WHERE`, `ORDER BY`, `LIMIT` and the `~`/`!~`
27//! operators all work on them because they are the same operators, not a
28//! second implementation.
29//!
30//! # What a schemaless engine can honestly report
31//!
32//! NEDB has no schema, so the catalogue is *derived*, and that derivation is
33//! stated rather than hidden:
34//!
35//! * a **collection** is a table in `pg_class` / `information_schema.tables`;
36//! * a **field observed in a sampled document** is a column in
37//!   `pg_attribute` / `information_schema.columns`, typed the way the pgwire
38//!   layer types it on the wire;
39//! * everything Postgres tracks that NEDB does not have — owners, tablespaces,
40//!   ACLs, statistics — reports a fixed, plainly-wrong-if-you-look value
41//!   (`10`, `0`, empty) rather than a fabricated plausible one.
42//!
43//! Column order and the sampled field set come from the same code the wire
44//! protocol uses, so a client is never told about a column the data does not
45//! produce.
46
47use std::sync::Arc;
48
49use serde_json::{json, Map, Value};
50
51use crate::db::Db;
52
53/// The fixed OID NEDB reports for anything Postgres owns and NEDB does not.
54///
55/// `10` is Postgres's own `bootstrap superuser` OID. Reporting a real-looking
56/// owner is less misleading than reporting `0`, which some clients render as a
57/// missing row rather than an unknown one.
58const OWNER_OID: i64 = 10;
59
60/// The one schema NEDB presents. A collection has no namespace of its own, so
61/// inventing several would be inventing structure.
62const PUBLIC_NS_OID: i64 = 2200; // Postgres's own oid for `public`
63const CATALOG_NS_OID: i64 = 11;
64const INFO_NS_OID: i64 = 13000;
65
66/// How many documents to sample when deriving a table's columns.
67///
68/// Bounded, because `\d` on a large collection must not turn into a scan. It
69/// is a sample, and the module doc says so — a field that appears only outside
70/// it is absent from the catalogue, which is the honest failure for a store
71/// with no declared schema.
72const COLUMN_SAMPLE: usize = 200;
73
74/// Is `table` a catalogue relation this module serves?
75///
76/// Matched on the BARE name, because the pgwire layer strips the schema
77/// qualification before it gets here (`pg_catalog.pg_class` → `pg_class`).
78/// An `information_schema.` prefix is kept on those names by the caller, since
79/// `tables` and `columns` are words a user could plausibly name a collection.
80pub fn is_catalog(table: &str) -> bool {
81    matches!(
82        table,
83        "pg_class" | "pg_namespace" | "pg_attribute" | "pg_type" | "pg_database"
84            | "pg_am" | "pg_roles" | "pg_user" | "pg_settings" | "pg_index"
85            | "pg_description" | "pg_constraint" | "pg_tablespace"
86            | "information_schema.tables"
87            | "information_schema.columns"
88            | "information_schema.schemata"
89            | "information_schema.key_column_usage"
90            | "information_schema.table_constraints"
91    ) || EMPTY_CATALOG.contains(&table)
92}
93
94/// Postgres system relations psql's `\d` family reads that NEDB has no
95/// counterpart for: no policies, defaults, collations, inheritance,
96/// publications, triggers, rules, large objects, extended statistics,
97/// enums, procedures, operators, extensions, foreign servers, text search
98/// or event triggers.
99///
100/// Each is a real relation here that is EMPTY, which is the truthful answer
101/// — `\dRp` on a fresh Postgres lists no publications either. An unknown
102/// bare name is still an error; these are the names psql 17 actually writes,
103/// verified by running every backslash command against the binary.
104const EMPTY_CATALOG: &[&str] = &[
105    "pg_policy", "pg_attrdef", "pg_collation", "pg_inherits", "pg_publication",
106    "pg_publication_rel", "pg_publication_namespace", "pg_subscription",
107    "pg_subscription_rel", "pg_largeobject_metadata", "pg_statistic_ext",
108    "pg_statistic_ext_data", "pg_trigger", "pg_rewrite", "pg_event_trigger",
109    "pg_enum", "pg_range", "pg_proc", "pg_aggregate", "pg_language",
110    "pg_operator", "pg_opclass", "pg_opfamily", "pg_amop", "pg_amproc", "pg_cast",
111    "pg_conversion", "pg_extension", "pg_available_extensions",
112    "pg_available_extension_versions", "pg_foreign_data_wrapper",
113    "pg_foreign_server", "pg_foreign_table", "pg_user_mapping", "pg_user_mappings",
114    "pg_default_acl", "pg_partitioned_table", "pg_ts_config", "pg_ts_config_map",
115    "pg_ts_dict", "pg_ts_parser", "pg_ts_template", "pg_seclabel", "pg_shdescription",
116    "pg_auth_members", "pg_shseclabel", "pg_replication_origin", "pg_sequence",
117    "pg_stat_user_tables", "pg_stat_all_tables", "pg_stats", "pg_statistic",
118    "pg_depend", "pg_shdepend", "pg_init_privs", "pg_parameter_acl",
119    "pg_transform", "pg_group", "pg_shadow", "pg_locks", "pg_stat_activity",
120    "pg_prepared_statements", "pg_cursors", "pg_timezone_names", "pg_timezone_abbrevs",
121    "information_schema.views", "information_schema.routines",
122    "information_schema.sequences", "information_schema.referential_constraints",
123    "information_schema.constraint_column_usage", "information_schema.triggers",
124    "information_schema.domains", "information_schema.column_privileges",
125    "information_schema.table_privileges", "information_schema.check_constraints",
126];
127
128/// A stable synthetic OID for a name.
129///
130/// Postgres clients use an OID to correlate rows between catalogue tables
131/// (`pg_attribute.attrelid` → `pg_class.oid`), so it has to be *consistent*
132/// within a connection, not globally meaningful. Derived from the name so the
133/// same collection gets the same OID on every query without any state to keep.
134/// Offset above Postgres's own reserved range so a synthetic OID cannot
135/// collide with a real one a client has hard-coded.
136fn oid_for(name: &str) -> i64 {
137    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
138    for b in name.as_bytes() {
139        h ^= *b as u64;
140        h = h.wrapping_mul(0x100_0000_01b3);
141    }
142    // Keep it comfortably inside i32 (Postgres OIDs are 32-bit unsigned) and
143    // above the reserved floor.
144    16_384 + (h % 2_000_000_000) as i64
145}
146
147/// The collections in the database, sorted so a listing is stable run to run.
148fn collections(db: Option<&Arc<Db>>) -> Vec<String> {
149    let mut out = match db {
150        Some(db) => db.id_index.collections(),
151        None => vec![],
152    };
153    // Internal bookkeeping is not a user table. `__links__` holds relation
154    // edges; showing it in `\dt` would invite someone to query or trust it.
155    out.retain(|c| !c.starts_with("__") && !c.is_empty());
156    out.sort();
157    out
158}
159
160/// Field name → wire type, derived from a bounded sample of the collection.
161///
162/// Uses the pgwire layer's own typing so the catalogue cannot disagree with
163/// what the wire actually sends: a column reported as `bigint` here is a
164/// column the protocol advertises as `int8`.
165fn columns_of(db: Option<&Arc<Db>>, coll: &str) -> Vec<(String, i32)> {
166    let db = match db {
167        Some(db) => db,
168        None => return vec![],
169    };
170    let rows = match crate::nql::query(db, &format!("FROM {} LIMIT {}", coll, COLUMN_SAMPLE)) {
171        Ok((rows, _)) => rows,
172        Err(_) => return vec![],
173    };
174    let mut names: Vec<String> = vec![];
175    for r in &rows {
176        if let Value::Object(m) = r {
177            for k in m.keys() {
178                if !names.iter().any(|n| n == k) {
179                    names.push(k.clone());
180                }
181            }
182        }
183    }
184    names.sort();
185    names
186        .into_iter()
187        .map(|n| {
188            let oid = crate::pgwire::oid_for_column(&rows, &n);
189            (n, oid)
190        })
191        .collect()
192}
193
194/// The Postgres type name for an OID we hand out — the `data_type` a client
195/// reads in `information_schema.columns`.
196/// Public alias so `format_type()` in the SQL engine names a type the same
197/// way `information_schema.columns` does.
198pub fn type_name_pub(oid: i32) -> &'static str {
199    type_name(oid)
200}
201
202fn type_name(oid: i32) -> &'static str {
203    match oid {
204        16 => "boolean",
205        20 => "bigint",
206        21 => "smallint",
207        23 => "integer",
208        700 => "real",
209        701 => "double precision",
210        1043 => "character varying",
211        _ => "text",
212    }
213}
214
215fn row(pairs: Vec<(&str, Value)>) -> Value {
216    let mut m = Map::new();
217    for (k, v) in pairs {
218        m.insert(k.to_string(), v);
219    }
220    Value::Object(m)
221}
222
223/// The rows of a catalogue relation, or `None` if it is not one.
224///
225/// Every column a real Postgres exposes is present where a client might read
226/// it, because a missing column is a hard error in the middle of someone
227/// else's generated SQL — far worse than a column reporting a fixed value.
228pub fn rows(table: &str, db: Option<&Arc<Db>>) -> Option<Vec<Value>> {
229    let colls = collections(db);
230
231    Some(match table {
232        // ── pg_namespace ────────────────────────────────────────────────────
233        // What `\dn` reads. The one table that needs no JOIN, which is why it
234        // was the first milestone.
235        "pg_namespace" | "information_schema.schemata" => {
236            let is_info = table.starts_with("information_schema");
237            [("public", PUBLIC_NS_OID), ("pg_catalog", CATALOG_NS_OID),
238             ("information_schema", INFO_NS_OID)]
239                .iter()
240                .map(|(name, oid)| {
241                    if is_info {
242                        row(vec![
243                            ("catalog_name", json!("nedb")),
244                            ("schema_name", json!(name)),
245                            ("schema_owner", json!("nedb")),
246                            ("default_character_set_catalog", Value::Null),
247                            ("default_character_set_schema", Value::Null),
248                            ("default_character_set_name", Value::Null),
249                            ("sql_path", Value::Null),
250                        ])
251                    } else {
252                        row(vec![
253                            ("oid", json!(oid)),
254                            ("nspname", json!(name)),
255                            ("nspowner", json!(OWNER_OID)),
256                            ("nspacl", Value::Null),
257                        ])
258                    }
259                })
260                .collect()
261        }
262
263        // ── pg_class — one row per collection ───────────────────────────────
264        "pg_class" => colls
265            .iter()
266            .map(|c| {
267                row(vec![
268                    ("oid", json!(oid_for(c))),
269                    ("relname", json!(c)),
270                    ("relnamespace", json!(PUBLIC_NS_OID)),
271                    // 'r' = ordinary table. A collection is writable and has
272                    // rows, so any other relkind would be a lie.
273                    ("relkind", json!("r")),
274                    ("relowner", json!(OWNER_OID)),
275                    ("relam", json!(2)),          // heap
276                    ("reltuples", json!(-1.0)),   // -1 = never analysed, which is true
277                    ("relpages", json!(0)),
278                    ("relhasindex", json!(false)),
279                    ("relpersistence", json!("p")),
280                    ("reltablespace", json!(0)),
281                    ("relispartition", json!(false)),
282                    ("reltoastrelid", json!(0)),
283                    ("relnatts", json!(columns_of(db, c).len() as i64)),
284                    // What `\d <table>` reads to decide which further
285                    // queries to send. Every "has" is false and every count is
286                    // zero because NEDB has none of these — and each false
287                    // spares psql a query against an empty relation.
288                    ("relacl", Value::Null),
289                    ("relchecks", json!(0)),
290                    ("relhasrules", json!(false)),
291                    ("relhastriggers", json!(false)),
292                    ("relhassubclass", json!(false)),
293                    ("relrowsecurity", json!(false)),
294                    ("relforcerowsecurity", json!(false)),
295                    ("relispopulated", json!(true)),
296                    ("relreplident", json!("d")),
297                    ("reloftype", json!(0)),
298                    ("relpartbound", Value::Null),
299                    ("reloptions", Value::Null),
300                    ("relfilenode", json!(oid_for(c))),
301                    ("reltype", json!(0)),
302                    ("relofoid", json!(0)),
303                    // `tableoid` is the OID of pg_class itself in Postgres.
304                    ("tableoid", json!(1259)),
305                ])
306            })
307            .collect(),
308
309        // ── pg_attribute — one row per observed field ───────────────────────
310        "pg_attribute" => colls
311            .iter()
312            .flat_map(|c| {
313                let rel = oid_for(c);
314                columns_of(db, c)
315                    .into_iter()
316                    .enumerate()
317                    .map(move |(i, (name, oid))| {
318                        row(vec![
319                            ("attrelid", json!(rel)),
320                            ("attname", json!(name)),
321                            // 1-based, as Postgres numbers them.
322                            ("attnum", json!(i as i64 + 1)),
323                            ("atttypid", json!(oid as i64)),
324                            ("attlen", json!(-1)),
325                            ("atttypmod", json!(-1)),
326                            // Nothing is NOT NULL in a schemaless store: any
327                            // document may omit any field.
328                            ("attnotnull", json!(false)),
329                            ("atthasdef", json!(false)),
330                            ("attisdropped", json!(false)),
331                            ("attidentity", json!("")),
332                            ("attgenerated", json!("")),
333                            ("attacl", Value::Null),
334                            ("attcollation", json!(0)),
335                            ("attstattarget", Value::Null),
336                            ("attstorage", json!("x")),
337                            ("attcompression", json!("")),
338                            ("attfdwoptions", Value::Null),
339                            ("attoptions", Value::Null),
340                            ("attndims", json!(0)),
341                            ("attbyval", json!(false)),
342                            ("attalign", json!("i")),
343                            ("atthasmissing", json!(false)),
344                            ("attislocal", json!(true)),
345                            ("attinhcount", json!(0)),
346                        ])
347                    })
348                    .collect::<Vec<_>>()
349            })
350            .collect(),
351
352        // ── information_schema.tables / .columns — the standard-SQL view ────
353        // What JDBC's DatabaseMetaData and most BI tools read first.
354        "information_schema.tables" => colls
355            .iter()
356            .map(|c| {
357                row(vec![
358                    ("table_catalog", json!("nedb")),
359                    ("table_schema", json!("public")),
360                    ("table_name", json!(c)),
361                    ("table_type", json!("BASE TABLE")),
362                    ("self_referencing_column_name", Value::Null),
363                    ("reference_generation", Value::Null),
364                    ("user_defined_type_catalog", Value::Null),
365                    ("user_defined_type_schema", Value::Null),
366                    ("user_defined_type_name", Value::Null),
367                    ("is_insertable_into", json!("YES")),
368                    ("is_typed", json!("NO")),
369                    ("commit_action", Value::Null),
370                ])
371            })
372            .collect(),
373
374        "information_schema.columns" => colls
375            .iter()
376            .flat_map(|c| {
377                columns_of(db, c)
378                    .into_iter()
379                    .enumerate()
380                    .map(move |(i, (name, oid))| {
381                        row(vec![
382                            ("table_catalog", json!("nedb")),
383                            ("table_schema", json!("public")),
384                            ("table_name", json!(c)),
385                            ("column_name", json!(name)),
386                            ("ordinal_position", json!(i as i64 + 1)),
387                            ("column_default", Value::Null),
388                            // Always YES: any document may omit any field.
389                            ("is_nullable", json!("YES")),
390                            ("data_type", json!(type_name(oid))),
391                            ("character_maximum_length", Value::Null),
392                            ("numeric_precision", Value::Null),
393                            ("numeric_scale", Value::Null),
394                            ("datetime_precision", Value::Null),
395                            ("udt_catalog", json!("nedb")),
396                            ("udt_schema", json!("pg_catalog")),
397                            ("udt_name", json!(type_name(oid))),
398                            ("is_updatable", json!("YES")),
399                        ])
400                    })
401                    .collect::<Vec<_>>()
402            })
403            .collect(),
404
405        // ── pg_type — only the types this endpoint actually hands out ───────
406        // Listing Postgres's full type table would be inventing support for
407        // types the wire layer cannot encode.
408        "pg_type" => [
409            (16, "bool"), (20, "int8"), (21, "int2"), (23, "int4"),
410            (25, "text"), (700, "float4"), (701, "float8"), (1043, "varchar"),
411        ]
412        .iter()
413        .map(|(oid, name)| {
414            row(vec![
415                ("oid", json!(*oid as i64)),
416                ("typname", json!(name)),
417                ("typnamespace", json!(CATALOG_NS_OID)),
418                ("typowner", json!(OWNER_OID)),
419                ("typlen", json!(-1)),
420                ("typtype", json!("b")),
421                ("typcategory", json!("S")),
422                ("typelem", json!(0)),
423                ("typrelid", json!(0)),
424                // What `\dT` reads: no array types are advertised, so the
425                // NOT EXISTS over `typarray` finds nothing to hide; no
426                // domains, so `typbasetype` is 0 and `typtype` is never 'd'.
427                ("typarray", json!(0)),
428                ("typbasetype", json!(0)),
429                ("typtypmod", json!(-1)),
430                ("typcollation", json!(0)),
431                ("typnotnull", json!(false)),
432                ("typdefault", Value::Null),
433                ("typacl", Value::Null),
434                ("typndims", json!(0)),
435                ("typbyval", json!(false)),
436                ("typalign", json!("i")),
437                ("typstorage", json!("x")),
438                ("typinput", json!(0)),
439                ("typoutput", json!(0)),
440                ("tableoid", json!(1247)),
441            ])
442        })
443        .collect(),
444
445        // ── pg_database — the databases the server has open ─────────────────
446        "pg_database" => vec![row(vec![
447            ("oid", json!(oid_for("nedb"))),
448            ("datname", json!("nedb")),
449            ("datdba", json!(OWNER_OID)),
450            ("encoding", json!(6)), // 6 = UTF8 in Postgres's encoding table
451            ("datcollate", json!("C")),
452            ("datctype", json!("C")),
453            ("datlocprovider", json!("c")),
454            ("daticulocale", Value::Null),
455            ("daticurules", Value::Null),
456            ("datistemplate", json!(false)),
457            ("datallowconn", json!(true)),
458            ("datconnlimit", json!(-1)),
459            ("datacl", Value::Null),
460        ])],
461
462        "pg_am" => vec![row(vec![
463            ("oid", json!(2)),
464            ("amname", json!("heap")),
465            ("amhandler", json!(0)),
466            ("amtype", json!("t")),
467        ])],
468
469        "pg_roles" | "pg_user" => vec![row(vec![
470            ("oid", json!(OWNER_OID)),
471            ("rolname", json!("nedb")),
472            ("usename", json!("nedb")),
473            ("rolsuper", json!(true)),
474            ("usesuper", json!(true)),
475            ("rolcanlogin", json!(true)),
476            ("rolcreatedb", json!(true)),
477            ("rolvaliduntil", Value::Null),
478        ])],
479
480        // ── the ones that are genuinely EMPTY, and should say so ────────────
481        // An empty catalogue table is the truthful answer here: NEDB has no
482        // secondary indexes visible to SQL, no constraints, no comments and
483        // no tablespaces. Returning rows would fabricate structure; refusing
484        // the query would break generated SQL that only wants to find none.
485        "pg_index" | "pg_description" | "pg_constraint" | "pg_tablespace"
486        | "pg_settings" | "information_schema.key_column_usage"
487        | "information_schema.table_constraints" => vec![],
488
489        t if EMPTY_CATALOG.contains(&t) => vec![],
490
491        _ => return None,
492    })
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use tempfile::tempdir;
499
500    fn db_with(colls: &[(&str, &str, Value)]) -> (tempfile::TempDir, Arc<Db>) {
501        let dir = tempdir().unwrap();
502        let db = Arc::new(Db::open(dir.path(), None).unwrap());
503        for (coll, id, doc) in colls {
504            db.put(coll, id, doc.clone(), vec![], None, None).unwrap();
505        }
506        (dir, db)
507    }
508
509    fn names(rows: &[Value], field: &str) -> Vec<String> {
510        let mut v: Vec<String> = rows
511            .iter()
512            .filter_map(|r| r.get(field)?.as_str().map(str::to_string))
513            .collect();
514        v.sort();
515        v
516    }
517
518    #[test]
519    fn a_collection_appears_as_a_table_in_every_place_a_client_looks() {
520        // JDBC reads information_schema; psql reads pg_class. A collection
521        // visible in one and not the other is a database that looks half
522        // empty depending on the tool.
523        let (_t, db) = db_with(&[
524            ("orders", "1", json!({"total": 1})),
525            ("drivers", "d1", json!({"name": "Bob"})),
526        ]);
527        let d = Some(&db);
528        assert_eq!(names(&rows("pg_class", d).unwrap(), "relname"),
529                   vec!["drivers", "orders"]);
530        assert_eq!(names(&rows("information_schema.tables", d).unwrap(), "table_name"),
531                   vec!["drivers", "orders"]);
532    }
533
534    #[test]
535    fn internal_bookkeeping_is_not_presented_as_a_user_table() {
536        // `__links__` holds relation edges. Listing it in `\dt` would invite
537        // someone to query or trust it as their own data.
538        let (_t, db) = db_with(&[("orders", "1", json!({"a": 1}))]);
539        let db2 = Arc::clone(&db);
540        db2.link("orders:1", "rel", "orders:1").ok();
541        let got = names(&rows("pg_class", Some(&db)).unwrap(), "relname");
542        assert!(!got.iter().any(|n| n.starts_with("__")), "{:?}", got);
543    }
544
545    #[test]
546    fn a_columns_reported_type_is_the_type_the_WIRE_sends() {
547        // The catalogue and the protocol go through one typing function, so
548        // they cannot disagree. A column reported `bigint` here that arrived
549        // as text on the wire would be a self-contradiction a client is
550        // entitled to trust.
551        let (_t, db) = db_with(&[
552            ("t", "1", json!({"n": 7, "s": "x", "b": true, "f": 1.5})),
553        ]);
554        let cols = rows("information_schema.columns", Some(&db)).unwrap();
555        let by = |name: &str| -> String {
556            cols.iter()
557                .find(|r| r["column_name"] == json!(name))
558                .and_then(|r| r["data_type"].as_str())
559                .unwrap_or("<missing>").to_string()
560        };
561        assert_eq!(by("n"), "bigint");
562        assert_eq!(by("s"), "text");
563        assert_eq!(by("b"), "boolean");
564        assert_eq!(by("f"), "double precision");
565    }
566
567    #[test]
568    fn everything_is_nullable_because_a_schemaless_document_may_omit_anything() {
569        let (_t, db) = db_with(&[("t", "1", json!({"a": 1}))]);
570        let cols = rows("information_schema.columns", Some(&db)).unwrap();
571        assert!(cols.iter().all(|r| r["is_nullable"] == json!("YES")), "{:?}", cols);
572        let attrs = rows("pg_attribute", Some(&db)).unwrap();
573        assert!(attrs.iter().all(|r| r["attnotnull"] == json!(false)));
574    }
575
576    #[test]
577    fn pg_attribute_correlates_with_pg_class_by_oid() {
578        // A client JOINs these two on oid. If the synthetic oids did not
579        // match, every `\d`-style query would silently return no columns.
580        let (_t, db) = db_with(&[("orders", "1", json!({"total": 1}))]);
581        let d = Some(&db);
582        let rel = rows("pg_class", d).unwrap();
583        let oid = rel.iter().find(|r| r["relname"] == json!("orders")).unwrap()["oid"].clone();
584        let attrs = rows("pg_attribute", d).unwrap();
585        assert!(attrs.iter().any(|r| r["attrelid"] == oid),
586                "no pg_attribute row points at pg_class.oid {:?}", oid);
587        // …and attnum is 1-based, as Postgres numbers columns.
588        assert!(attrs.iter().all(|r| r["attnum"].as_i64().unwrap_or(0) >= 1));
589    }
590
591    #[test]
592    fn an_oid_is_stable_across_calls_so_a_join_holds() {
593        assert_eq!(oid_for("orders"), oid_for("orders"));
594        assert_ne!(oid_for("orders"), oid_for("drivers"));
595        // Above Postgres's reserved floor, so a synthetic oid cannot collide
596        // with a real one a client has hard-coded.
597        assert!(oid_for("orders") >= 16_384);
598        // And inside i32, because a Postgres OID is 32-bit.
599        assert!(oid_for("orders") < i32::MAX as i64);
600    }
601
602    #[test]
603    fn pg_namespace_answers_without_any_database_open() {
604        // psql sends catalogue queries on startup, sometimes before a database
605        // is selected. Refusing there is how "psql cannot connect" begins.
606        let ns = rows("pg_namespace", None).unwrap();
607        assert_eq!(names(&ns, "nspname"),
608                   vec!["information_schema", "pg_catalog", "public"]);
609    }
610
611    #[test]
612    fn the_tables_nedb_genuinely_has_nothing_for_are_EMPTY_not_absent() {
613        // Empty is the truthful answer: no SQL-visible indexes, no
614        // constraints, no comments, no tablespaces. Returning rows would
615        // fabricate structure; returning None would break generated SQL that
616        // only wants to find none.
617        for t in ["pg_index", "pg_constraint", "pg_description", "pg_tablespace",
618                  "information_schema.key_column_usage",
619                  "information_schema.table_constraints"] {
620            let r = rows(t, None).unwrap_or_else(|| panic!("{} must be served", t));
621            assert!(r.is_empty(), "{} should be empty, got {:?}", t, r);
622        }
623    }
624
625    #[test]
626    fn a_name_that_is_not_a_catalogue_relation_is_not_claimed() {
627        assert!(!is_catalog("orders"));
628        assert!(!is_catalog("tables"), "a bare `tables` is a user collection");
629        assert!(rows("orders", None).is_none());
630        // information_schema keeps its qualifier precisely so a user
631        // collection called `tables` cannot be shadowed by the catalogue.
632        assert!(is_catalog("information_schema.tables"));
633    }
634
635    #[test]
636    fn pg_type_lists_only_types_this_endpoint_can_actually_send() {
637        let t = names(&rows("pg_type", None).unwrap(), "typname");
638        assert!(t.contains(&"int8".to_string()));
639        assert!(t.contains(&"text".to_string()));
640        // Listing Postgres's whole type table would advertise encoders the
641        // wire layer does not have.
642        assert!(!t.contains(&"tsvector".to_string()));
643        assert!(!t.contains(&"jsonb".to_string()));
644    }
645}