use std::sync::Arc;
use serde_json::{json, Map, Value};
use crate::db::Db;
const OWNER_OID: i64 = 10;
const PUBLIC_NS_OID: i64 = 2200; const CATALOG_NS_OID: i64 = 11;
const INFO_NS_OID: i64 = 13000;
const COLUMN_SAMPLE: usize = 200;
pub fn is_catalog(table: &str) -> bool {
matches!(
table,
"pg_class" | "pg_namespace" | "pg_attribute" | "pg_type" | "pg_database"
| "pg_am" | "pg_roles" | "pg_user" | "pg_settings" | "pg_index"
| "pg_description" | "pg_constraint" | "pg_tablespace"
| "information_schema.tables"
| "information_schema.columns"
| "information_schema.schemata"
| "information_schema.key_column_usage"
| "information_schema.table_constraints"
)
}
fn oid_for(name: &str) -> i64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in name.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x100_0000_01b3);
}
16_384 + (h % 2_000_000_000) as i64
}
fn collections(db: Option<&Arc<Db>>) -> Vec<String> {
let mut out = match db {
Some(db) => db.id_index.collections(),
None => vec![],
};
out.retain(|c| !c.starts_with("__") && !c.is_empty());
out.sort();
out
}
fn columns_of(db: Option<&Arc<Db>>, coll: &str) -> Vec<(String, i32)> {
let db = match db {
Some(db) => db,
None => return vec![],
};
let rows = match crate::nql::query(db, &format!("FROM {} LIMIT {}", coll, COLUMN_SAMPLE)) {
Ok((rows, _)) => rows,
Err(_) => return vec![],
};
let mut names: Vec<String> = vec![];
for r in &rows {
if let Value::Object(m) = r {
for k in m.keys() {
if !names.iter().any(|n| n == k) {
names.push(k.clone());
}
}
}
}
names.sort();
names
.into_iter()
.map(|n| {
let oid = crate::pgwire::oid_for_column(&rows, &n);
(n, oid)
})
.collect()
}
pub fn type_name_pub(oid: i32) -> &'static str {
type_name(oid)
}
fn type_name(oid: i32) -> &'static str {
match oid {
16 => "boolean",
20 => "bigint",
21 => "smallint",
23 => "integer",
700 => "real",
701 => "double precision",
1043 => "character varying",
_ => "text",
}
}
fn row(pairs: Vec<(&str, Value)>) -> Value {
let mut m = Map::new();
for (k, v) in pairs {
m.insert(k.to_string(), v);
}
Value::Object(m)
}
pub fn rows(table: &str, db: Option<&Arc<Db>>) -> Option<Vec<Value>> {
let colls = collections(db);
Some(match table {
"pg_namespace" | "information_schema.schemata" => {
let is_info = table.starts_with("information_schema");
[("public", PUBLIC_NS_OID), ("pg_catalog", CATALOG_NS_OID),
("information_schema", INFO_NS_OID)]
.iter()
.map(|(name, oid)| {
if is_info {
row(vec![
("catalog_name", json!("nedb")),
("schema_name", json!(name)),
("schema_owner", json!("nedb")),
("default_character_set_catalog", Value::Null),
("default_character_set_schema", Value::Null),
("default_character_set_name", Value::Null),
("sql_path", Value::Null),
])
} else {
row(vec![
("oid", json!(oid)),
("nspname", json!(name)),
("nspowner", json!(OWNER_OID)),
("nspacl", Value::Null),
])
}
})
.collect()
}
"pg_class" => colls
.iter()
.map(|c| {
row(vec![
("oid", json!(oid_for(c))),
("relname", json!(c)),
("relnamespace", json!(PUBLIC_NS_OID)),
("relkind", json!("r")),
("relowner", json!(OWNER_OID)),
("relam", json!(2)), ("reltuples", json!(-1.0)), ("relhasindex", json!(false)),
("relpersistence", json!("p")),
("reltablespace", json!(0)),
("relispartition", json!(false)),
("reltoastrelid", json!(0)),
("relnatts", json!(columns_of(db, c).len() as i64)),
])
})
.collect(),
"pg_attribute" => colls
.iter()
.flat_map(|c| {
let rel = oid_for(c);
columns_of(db, c)
.into_iter()
.enumerate()
.map(move |(i, (name, oid))| {
row(vec![
("attrelid", json!(rel)),
("attname", json!(name)),
("attnum", json!(i as i64 + 1)),
("atttypid", json!(oid as i64)),
("attlen", json!(-1)),
("atttypmod", json!(-1)),
("attnotnull", json!(false)),
("atthasdef", json!(false)),
("attisdropped", json!(false)),
("attidentity", json!("")),
("attgenerated", json!("")),
])
})
.collect::<Vec<_>>()
})
.collect(),
"information_schema.tables" => colls
.iter()
.map(|c| {
row(vec![
("table_catalog", json!("nedb")),
("table_schema", json!("public")),
("table_name", json!(c)),
("table_type", json!("BASE TABLE")),
("self_referencing_column_name", Value::Null),
("reference_generation", Value::Null),
("user_defined_type_catalog", Value::Null),
("user_defined_type_schema", Value::Null),
("user_defined_type_name", Value::Null),
("is_insertable_into", json!("YES")),
("is_typed", json!("NO")),
("commit_action", Value::Null),
])
})
.collect(),
"information_schema.columns" => colls
.iter()
.flat_map(|c| {
columns_of(db, c)
.into_iter()
.enumerate()
.map(move |(i, (name, oid))| {
row(vec![
("table_catalog", json!("nedb")),
("table_schema", json!("public")),
("table_name", json!(c)),
("column_name", json!(name)),
("ordinal_position", json!(i as i64 + 1)),
("column_default", Value::Null),
("is_nullable", json!("YES")),
("data_type", json!(type_name(oid))),
("character_maximum_length", Value::Null),
("numeric_precision", Value::Null),
("numeric_scale", Value::Null),
("datetime_precision", Value::Null),
("udt_catalog", json!("nedb")),
("udt_schema", json!("pg_catalog")),
("udt_name", json!(type_name(oid))),
("is_updatable", json!("YES")),
])
})
.collect::<Vec<_>>()
})
.collect(),
"pg_type" => [
(16, "bool"), (20, "int8"), (21, "int2"), (23, "int4"),
(25, "text"), (700, "float4"), (701, "float8"), (1043, "varchar"),
]
.iter()
.map(|(oid, name)| {
row(vec![
("oid", json!(*oid as i64)),
("typname", json!(name)),
("typnamespace", json!(CATALOG_NS_OID)),
("typowner", json!(OWNER_OID)),
("typlen", json!(-1)),
("typtype", json!("b")),
("typcategory", json!("S")),
("typelem", json!(0)),
("typrelid", json!(0)),
])
})
.collect(),
"pg_database" => vec![row(vec![
("oid", json!(oid_for("nedb"))),
("datname", json!("nedb")),
("datdba", json!(OWNER_OID)),
("encoding", json!(6)), ("datcollate", json!("C")),
("datctype", json!("C")),
("datlocprovider", json!("c")),
("daticulocale", Value::Null),
("daticurules", Value::Null),
("datistemplate", json!(false)),
("datallowconn", json!(true)),
("datconnlimit", json!(-1)),
("datacl", Value::Null),
])],
"pg_am" => vec![row(vec![
("oid", json!(2)),
("amname", json!("heap")),
("amhandler", json!(0)),
("amtype", json!("t")),
])],
"pg_roles" | "pg_user" => vec![row(vec![
("oid", json!(OWNER_OID)),
("rolname", json!("nedb")),
("usename", json!("nedb")),
("rolsuper", json!(true)),
("usesuper", json!(true)),
("rolcanlogin", json!(true)),
("rolcreatedb", json!(true)),
("rolvaliduntil", Value::Null),
])],
"pg_index" | "pg_description" | "pg_constraint" | "pg_tablespace"
| "pg_settings" | "information_schema.key_column_usage"
| "information_schema.table_constraints" => vec![],
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn db_with(colls: &[(&str, &str, Value)]) -> (tempfile::TempDir, Arc<Db>) {
let dir = tempdir().unwrap();
let db = Arc::new(Db::open(dir.path(), None).unwrap());
for (coll, id, doc) in colls {
db.put(coll, id, doc.clone(), vec![], None, None).unwrap();
}
(dir, db)
}
fn names(rows: &[Value], field: &str) -> Vec<String> {
let mut v: Vec<String> = rows
.iter()
.filter_map(|r| r.get(field)?.as_str().map(str::to_string))
.collect();
v.sort();
v
}
#[test]
fn a_collection_appears_as_a_table_in_every_place_a_client_looks() {
let (_t, db) = db_with(&[
("orders", "1", json!({"total": 1})),
("drivers", "d1", json!({"name": "Bob"})),
]);
let d = Some(&db);
assert_eq!(names(&rows("pg_class", d).unwrap(), "relname"),
vec!["drivers", "orders"]);
assert_eq!(names(&rows("information_schema.tables", d).unwrap(), "table_name"),
vec!["drivers", "orders"]);
}
#[test]
fn internal_bookkeeping_is_not_presented_as_a_user_table() {
let (_t, db) = db_with(&[("orders", "1", json!({"a": 1}))]);
let db2 = Arc::clone(&db);
db2.link("orders:1", "rel", "orders:1").ok();
let got = names(&rows("pg_class", Some(&db)).unwrap(), "relname");
assert!(!got.iter().any(|n| n.starts_with("__")), "{:?}", got);
}
#[test]
fn a_columns_reported_type_is_the_type_the_WIRE_sends() {
let (_t, db) = db_with(&[
("t", "1", json!({"n": 7, "s": "x", "b": true, "f": 1.5})),
]);
let cols = rows("information_schema.columns", Some(&db)).unwrap();
let by = |name: &str| -> String {
cols.iter()
.find(|r| r["column_name"] == json!(name))
.and_then(|r| r["data_type"].as_str())
.unwrap_or("<missing>").to_string()
};
assert_eq!(by("n"), "bigint");
assert_eq!(by("s"), "text");
assert_eq!(by("b"), "boolean");
assert_eq!(by("f"), "double precision");
}
#[test]
fn everything_is_nullable_because_a_schemaless_document_may_omit_anything() {
let (_t, db) = db_with(&[("t", "1", json!({"a": 1}))]);
let cols = rows("information_schema.columns", Some(&db)).unwrap();
assert!(cols.iter().all(|r| r["is_nullable"] == json!("YES")), "{:?}", cols);
let attrs = rows("pg_attribute", Some(&db)).unwrap();
assert!(attrs.iter().all(|r| r["attnotnull"] == json!(false)));
}
#[test]
fn pg_attribute_correlates_with_pg_class_by_oid() {
let (_t, db) = db_with(&[("orders", "1", json!({"total": 1}))]);
let d = Some(&db);
let rel = rows("pg_class", d).unwrap();
let oid = rel.iter().find(|r| r["relname"] == json!("orders")).unwrap()["oid"].clone();
let attrs = rows("pg_attribute", d).unwrap();
assert!(attrs.iter().any(|r| r["attrelid"] == oid),
"no pg_attribute row points at pg_class.oid {:?}", oid);
assert!(attrs.iter().all(|r| r["attnum"].as_i64().unwrap_or(0) >= 1));
}
#[test]
fn an_oid_is_stable_across_calls_so_a_join_holds() {
assert_eq!(oid_for("orders"), oid_for("orders"));
assert_ne!(oid_for("orders"), oid_for("drivers"));
assert!(oid_for("orders") >= 16_384);
assert!(oid_for("orders") < i32::MAX as i64);
}
#[test]
fn pg_namespace_answers_without_any_database_open() {
let ns = rows("pg_namespace", None).unwrap();
assert_eq!(names(&ns, "nspname"),
vec!["information_schema", "pg_catalog", "public"]);
}
#[test]
fn the_tables_nedb_genuinely_has_nothing_for_are_EMPTY_not_absent() {
for t in ["pg_index", "pg_constraint", "pg_description", "pg_tablespace",
"information_schema.key_column_usage",
"information_schema.table_constraints"] {
let r = rows(t, None).unwrap_or_else(|| panic!("{} must be served", t));
assert!(r.is_empty(), "{} should be empty, got {:?}", t, r);
}
}
#[test]
fn a_name_that_is_not_a_catalogue_relation_is_not_claimed() {
assert!(!is_catalog("orders"));
assert!(!is_catalog("tables"), "a bare `tables` is a user collection");
assert!(rows("orders", None).is_none());
assert!(is_catalog("information_schema.tables"));
}
#[test]
fn pg_type_lists_only_types_this_endpoint_can_actually_send() {
let t = names(&rows("pg_type", None).unwrap(), "typname");
assert!(t.contains(&"int8".to_string()));
assert!(t.contains(&"text".to_string()));
assert!(!t.contains(&"tsvector".to_string()));
assert!(!t.contains(&"jsonb".to_string()));
}
}