diff --git a/Cargo.lock b/Cargo.lock index 93ffb59..b894fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,7 @@ name = "rupg-exec" version = "0.1.1" dependencies = [ "rupg-analyze", + "rupg-catalog", "rupg-common", "rupg-func", "rupg-pgcatalog", @@ -377,6 +378,7 @@ version = "0.1.1" dependencies = [ "md-5", "rupg-analyze", + "rupg-catalog", "rupg-common", "rupg-pgcatalog", "rupg-sql", diff --git a/crates/rupg-analyze/src/ddl/fkey.rs b/crates/rupg-analyze/src/ddl/fkey.rs index dff5b4c..c7622a7 100644 --- a/crates/rupg-analyze/src/ddl/fkey.rs +++ b/crates/rupg-analyze/src/ddl/fkey.rs @@ -6,7 +6,7 @@ use rupg_common::{Error, Result, SqlState}; use rupg_pgcatalog::builtin; use rupg_sql::nodes::Constraint; -use super::{Definer, Found, is_system, not_yet, relkind_detail, relname}; +use super::{Definer, Found, not_yet, relkind_detail, relname}; use crate::coerce::{Context, can_coerce}; use crate::typename::{names, place}; use crate::types; @@ -77,7 +77,7 @@ impl Definer<'_, '_> { format!("referenced relation \"{pk_name}\" is not a table"), )); } - if is_system(found) { + if self.is_system(found) { return Err(Error::new( SqlState::INSUFFICIENT_PRIVILEGE, format!("permission denied: \"{pk_name}\" is a system catalog"), diff --git a/crates/rupg-analyze/src/ddl/index.rs b/crates/rupg-analyze/src/ddl/index.rs index 8d13273..1e2e478 100644 --- a/crates/rupg-analyze/src/ddl/index.rs +++ b/crates/rupg-analyze/src/ddl/index.rs @@ -6,7 +6,7 @@ use rupg_pgcatalog::builtin; use rupg_sql::nodes::{IndexElem, IndexStmt, Node, SortByDir, SortByNulls}; use rupg_types::oid; -use super::{Definer, Found, is_mutable, is_system, not_yet, references, relkind_detail}; +use super::{Definer, Found, is_mutable, not_yet, references, relkind_detail}; use crate::agg::Kind; use crate::coerce::AtOpt; use crate::colname::figure_index_colname; @@ -71,7 +71,7 @@ impl Definer<'_, '_> { format!("must be owner of {what} {name}"), )); } - if is_system(found) { + if self.is_system(found) { return Err(Error::new( SqlState::INSUFFICIENT_PRIVILEGE, format!("permission denied: \"{name}\" is a system catalog"), diff --git a/crates/rupg-analyze/src/ddl/mod.rs b/crates/rupg-analyze/src/ddl/mod.rs index fba4414..cb78b3f 100644 --- a/crates/rupg-analyze/src/ddl/mod.rs +++ b/crates/rupg-analyze/src/ddl/mod.rs @@ -326,12 +326,23 @@ impl Definer<'_, '_> { }) } + /// `IsSystemRelation`: true for a relation of the catalog and for a TOAST table or its index. + fn is_system(&self, found: Found) -> bool { + match found { + Found::Builtin(row) => row.oid < rupg_catalog::FIRST_UNPINNED_OID, + Found::User(oid) => { + self.catalog.relation(oid).is_some_and(|r| r.namespace == PG_TOAST_NAMESPACE) + } + } + } + /// The kind of a relation as a `relkind` code. fn kind_of(&self, found: Found) -> u8 { match found { Found::User(oid) => match self.catalog.relation(oid).map(|r| r.kind) { Some(RelKind::Index) => b'i', Some(RelKind::Sequence) => b'S', + Some(RelKind::Toast) => b't', _ => b'r', }, Found::Builtin(row) => row.kind, @@ -404,11 +415,6 @@ impl Definer<'_, '_> { } } -/// True for a relation of the system catalogs, which a user cannot change: a built-in relation with an OID below `FirstUnpinnedObjectId`. -fn is_system(found: Found) -> bool { - matches!(found, Found::Builtin(row) if row.oid < rupg_catalog::FIRST_UNPINNED_OID) -} - /// The detail of `errdetail_relkind_not_supported`. fn relkind_detail(kind: u8) -> &'static str { match kind { diff --git a/crates/rupg-analyze/src/ddl/table.rs b/crates/rupg-analyze/src/ddl/table.rs index dda46b3..0052cbe 100644 --- a/crates/rupg-analyze/src/ddl/table.rs +++ b/crates/rupg-analyze/src/ddl/table.rs @@ -603,7 +603,7 @@ impl Definer<'_, '_> { self.add_checks(table, plan)?; self.add_not_nulls(table, plan)?; if needs_toast(&shapes) { - self.catalog.skip_oids(2); + self.catalog.create_toast(table)?; } Ok(table) } diff --git a/crates/rupg-analyze/src/ddl/tests.rs b/crates/rupg-analyze/src/ddl/tests.rs index 38338ea..5cd0b10 100644 --- a/crates/rupg-analyze/src/ddl/tests.rs +++ b/crates/rupg-analyze/src/ddl/tests.rs @@ -193,7 +193,7 @@ fn catalog_order() { let expected: Vec<(u32, String)> = expected.iter().map(|(o, n)| (*o, n.to_string())).collect(); assert_eq!(names, expected); - assert_eq!(catalog.depends().len(), 71); + assert_eq!(catalog.depends().len(), 74); }); } diff --git a/crates/rupg-catalog/src/lib.rs b/crates/rupg-catalog/src/lib.rs index 31f9be1..fd2e9bb 100644 --- a/crates/rupg-catalog/src/lib.rs +++ b/crates/rupg-catalog/src/lib.rs @@ -2,7 +2,7 @@ //! //! At M2 the catalog holds the schemas, tables, sequences, indexes, column defaults and constraints that DDL makes. Each object gets its OID in the order that PostgreSQL 19 gives OIDs, and each object without a name in the statement gets the name that PostgreSQL gives it. Thus after the same statements on a new cluster, the OIDs and the names are the same as in PostgreSQL. //! -//! The catalog does not read SQL. The analyzer turns a statement into calls of this crate in the order of `DefineRelation`, `DefineIndex` and `ATAddForeignKeyConstraint`, and it gives each expression as text that only the analyzer reads back. The catalog does not make TOAST tables or the triggers of foreign keys, but it uses their OIDs, so the OIDs of later objects agree with PostgreSQL. It records no `pg_depend` rows for these objects. The catalog also records the rows of `pg_depend` in the order that PostgreSQL inserts them. +//! The catalog does not read SQL. The analyzer turns a statement into calls of this crate in the order of `DefineRelation`, `DefineIndex` and `ATAddForeignKeyConstraint`, and it gives each expression as text that only the analyzer reads back. The catalog makes the TOAST tables that PostgreSQL makes, with their indexes, but they hold no data. It does not make the triggers of foreign keys, but it uses their OIDs, so the OIDs of later objects agree with PostgreSQL. It records no `pg_depend` rows for these triggers. The catalog also records the rows of `pg_depend` in the order that PostgreSQL inserts them. //! //! This crate first ships in milestone M2. See `spec/22-crate-layout.md` section 22.4 and `spec/23-milestones.md`. @@ -33,6 +33,18 @@ pub const PG_NAMESPACE: u32 = 2615; pub const PG_DATABASE: u32 = 1262; /// The OID of `pg_largeobject`. pub const PG_LARGEOBJECT: u32 = 2613; +/// The OID of the schema `pg_toast`. +pub const PG_TOAST_NAMESPACE: u32 = 99; +/// The OID of the type `oid`. +const OID: u32 = 26; +/// The OID of the type `int4`. +const INT4: u32 = 23; +/// The OID of the type `bytea`. +const BYTEA: u32 = 17; +/// The OID of the operator class `oid_ops` of `btree`. +const OID_OPS: u32 = 1981; +/// The OID of the operator class `int4_ops` of `btree`. +const INT4_OPS: u32 = 1978; /// The OID of the schema `public`. pub const PUBLIC: u32 = 2200; /// The OID of `pg_constraint`. @@ -64,6 +76,8 @@ pub enum RelKind { Index, /// A sequence, `S`. Sequence, + /// A TOAST table, `t`. + Toast, } impl RelKind { @@ -73,6 +87,7 @@ impl RelKind { RelKind::Table => 'r', RelKind::Index => 'i', RelKind::Sequence => 'S', + RelKind::Toast => 't', } } } @@ -161,7 +176,7 @@ pub struct SequenceInfo { pub cycle: bool, } -/// A relation: a table, an index or a sequence. +/// A relation: a table, an index, a sequence or a TOAST table. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Relation { /// The OID. @@ -180,6 +195,10 @@ pub struct Relation { pub columns: Vec, /// `relhasindex`: true after the first index on the table. pub has_index: bool, + /// `relhastriggers`: true after the first foreign key that refers to the table or that the table has. + pub has_triggers: bool, + /// `reltoastrelid`: the OID of the TOAST table, or 0. + pub toast: u32, /// The facts of an index. pub index: Option, /// The facts of a sequence. @@ -518,7 +537,7 @@ impl Catalog { self.next_oid = self.next_oid.max(oid); } - /// Uses `count` OIDs for objects that PostgreSQL makes and rupg does not, such as a TOAST table and its index. + /// Uses `count` OIDs for objects that PostgreSQL makes and rupg does not, such as the triggers of a foreign key. pub fn skip_oids(&mut self, count: u32) { for _ in 0..count { self.new_oid(); @@ -796,6 +815,8 @@ impl Catalog { row_type, columns: new.columns, has_index: false, + has_triggers: false, + toast: 0, index: None, sequence: None, }, @@ -810,9 +831,12 @@ impl Catalog { Ok(oid) } - /// `DefineSequence`. A sequence has no row type. - pub fn create_sequence(&mut self, new: NewRelation, info: SequenceInfo) -> Result { + /// `DefineSequence`. A sequence has no row type, and each column of a sequence is not null. + pub fn create_sequence(&mut self, mut new: NewRelation, info: SequenceInfo) -> Result { self.check_new_relation(new.namespace, &new.name, false)?; + for column in &mut new.columns { + column.not_null = true; + } let oid = self.new_oid(); self.relations.insert( oid, @@ -825,6 +849,8 @@ impl Catalog { row_type: 0, columns: new.columns, has_index: false, + has_triggers: false, + toast: 0, index: None, sequence: Some(info), }, @@ -1034,11 +1060,19 @@ impl Catalog { exprs: new.exprs, predicate: new.predicate, }; + let table_columns = self.relations.get(&new.table).map_or(&[][..], |r| &r.columns[..]); let columns = new .columns .iter() .zip(column_names) - .map(|(c, name)| Column::new(name, c.ty, c.typmod, c.collation)) + .map(|(c, name)| { + let mut column = Column::new(name, c.ty, c.typmod, c.collation); + let from = usize::try_from(c.key).ok().and_then(|k| k.checked_sub(1)); + if let Some(from) = from.and_then(|k| table_columns.get(k)) { + column.ndims = from.ndims; + } + column + }) .collect(); self.relations.insert( oid, @@ -1051,6 +1085,8 @@ impl Catalog { row_type: 0, columns, has_index: false, + has_triggers: false, + toast: 0, index: Some(info), sequence: None, }, @@ -1060,6 +1096,7 @@ impl Catalog { if let Some(kind) = new.constraint { let mut constraint = self.new_constraint(name, namespace, kind, new.table, keys); constraint.index = oid; + constraint.no_inherit = true; constraint.deferrable = new.deferrable; constraint.deferred = new.deferred; let referenced = ObjRef::new(PG_CONSTRAINT, constraint.oid); @@ -1106,6 +1143,7 @@ impl Catalog { let mut constraint = self.new_constraint(name, namespace, ConKind::Foreign, new.table, new.keys); constraint.index = new.index; + constraint.no_inherit = true; constraint.deferrable = new.deferrable; constraint.deferred = new.deferred; let oid = constraint.oid; @@ -1119,6 +1157,74 @@ impl Catalog { } self.depend(me, ObjRef::new(PG_CLASS, new.index), DepKind::Normal); self.skip_oids(FOREIGN_KEY_TRIGGERS); + self.relation_mut(new.table)?.has_triggers = true; + self.relation_mut(referenced)?.has_triggers = true; + Ok(oid) + } + + /// `create_toast_table`: the TOAST table of a table and its index, in the schema `pg_toast`. The TOAST table gets an OID, then its index. They have no row type. + pub fn create_toast(&mut self, table: u32) -> Result { + let owner = self.relations.get(&table).ok_or_else(|| no_object("relation", table))?.owner; + let oid = self.new_oid(); + let index = self.new_oid(); + let name = format!("pg_toast_{table}"); + let chunk_id = Column::new("chunk_id", OID, -1, 0); + let chunk_seq = Column::new("chunk_seq", INT4, -1, 0); + let chunk_data = Column::new("chunk_data", BYTEA, -1, 0); + self.relations.insert( + oid, + Relation { + oid, + name: name.clone(), + namespace: PG_TOAST_NAMESPACE, + kind: RelKind::Toast, + owner, + row_type: 0, + columns: vec![chunk_id.clone(), chunk_seq.clone(), chunk_data], + has_index: true, + has_triggers: false, + toast: 0, + index: None, + sequence: None, + }, + ); + let info = IndexInfo { + table: oid, + keys: vec![1, 2], + key_count: 2, + unique: true, + nulls_not_distinct: false, + primary: true, + immediate: true, + method: BTREE, + collations: vec![0, 0], + classes: vec![OID_OPS, INT4_OPS], + options: vec![0, 0], + exprs: None, + predicate: None, + }; + self.relations.insert( + index, + Relation { + oid: index, + name: format!("{name}_index"), + namespace: PG_TOAST_NAMESPACE, + kind: RelKind::Index, + owner, + row_type: 0, + columns: vec![chunk_id, chunk_seq], + has_index: false, + has_triggers: false, + toast: 0, + index: Some(info), + sequence: None, + }, + ); + self.relation_mut(table)?.toast = oid; + self.depend(ObjRef::new(PG_CLASS, oid), ObjRef::new(PG_CLASS, table), DepKind::Internal); + for column in 1..=2 { + self.depend(ObjRef::new(PG_CLASS, index), ObjRef::column(oid, column), DepKind::Auto); + } Ok(oid) } } diff --git a/crates/rupg-catalog/src/tests.rs b/crates/rupg-catalog/src/tests.rs index b98c88e..bc65fd4 100644 --- a/crates/rupg-catalog/src/tests.rs +++ b/crates/rupg-catalog/src/tests.rs @@ -194,7 +194,7 @@ fn postgres_order() { Shape { len: -1, align: b'i', storage: b'x', ty: TEXT, typmod: -1 }, ]; assert!(needs_toast(&shapes)); - cat.skip_oids(2); + assert_eq!(cat.create_toast(t).unwrap(), 16395); let (pkey, pkey_con) = cat.create_index(index(t, None, &[("a", 1, INT4)], Some(ConKind::Primary))).unwrap(); assert_eq!((pkey, pkey_con), (16397, Some(16398))); @@ -242,6 +242,15 @@ fn postgres_order() { ); let rel = cat.relation(t).unwrap(); assert!(rel.has_index && rel.columns[0].not_null && rel.columns[1].has_default); + assert!(rel.has_triggers && rel.toast == 16395); + let toast = cat.relation(16395).unwrap(); + assert_eq!( + (toast.name.as_str(), toast.kind.code(), toast.namespace), + ("pg_toast_16388", 't', 99) + ); + let toast_index = cat.relation(16396).unwrap(); + assert_eq!(toast_index.name, "pg_toast_16388_index"); + assert_eq!(toast_index.index.as_ref().map(|i| i.classes.clone()), Some(vec![1981, 1978])); let seq_name = cat.choose_relation_name("u", Some("x"), "seq", s, false); let (new, info) = serial_sequence(s, &seq_name); @@ -337,6 +346,9 @@ fn postgres_order() { (PG_CONSTRAINT, 16392, 0, PG_CLASS, 16388, 3, 'a'), (PG_CONSTRAINT, 16393, 0, PG_CLASS, 16388, 1, 'a'), (PG_CONSTRAINT, 16394, 0, PG_CLASS, 16388, 2, 'a'), + (PG_CLASS, 16395, 0, PG_CLASS, 16388, 0, 'i'), + (PG_CLASS, 16396, 0, PG_CLASS, 16395, 1, 'a'), + (PG_CLASS, 16396, 0, PG_CLASS, 16395, 2, 'a'), (PG_CLASS, 16397, 0, PG_CONSTRAINT, 16398, 0, 'i'), (PG_CONSTRAINT, 16398, 0, PG_CLASS, 16388, 1, 'a'), (PG_CLASS, 16399, 0, PG_CONSTRAINT, 16400, 0, 'i'), diff --git a/crates/rupg-exec/Cargo.toml b/crates/rupg-exec/Cargo.toml index e551730..0d69843 100644 --- a/crates/rupg-exec/Cargo.toml +++ b/crates/rupg-exec/Cargo.toml @@ -15,6 +15,7 @@ publish = false [dependencies] rupg-analyze.workspace = true +rupg-catalog.workspace = true rupg-common.workspace = true rupg-func.workspace = true rupg-pgcatalog.workspace = true diff --git a/crates/rupg-exec/src/lib.rs b/crates/rupg-exec/src/lib.rs index 0d44a34..16a59b0 100644 --- a/crates/rupg-exec/src/lib.rs +++ b/crates/rupg-exec/src/lib.rs @@ -925,6 +925,7 @@ fn relabel(value: Value, ty: u32) -> Value { mod agg; mod scan; mod sort; +mod user; #[cfg(test)] mod tests; diff --git a/crates/rupg-exec/src/scan.rs b/crates/rupg-exec/src/scan.rs index 398f56b..15432e0 100644 --- a/crates/rupg-exec/src/scan.rs +++ b/crates/rupg-exec/src/scan.rs @@ -1,4 +1,4 @@ -//! The scan of the tables of the catalog and the joins of `FROM`. +//! The scan of the tables of the catalog and the joins of `FROM`. A table of the catalog gives its static rows, then the rows of the user objects of the session. //! //! A tuple of the join is the row number of each relation of the query, or [`NONE`] for a relation that a row of an outer join does not have. The rows of each relation are read once, with only the columns that the query uses. @@ -125,13 +125,16 @@ pub(crate) fn read(query: &Query, session: &dyn Session) -> Result { } table.push(values); } + if let Some(user) = session.catalog() { + table.extend(crate::user::rows(catalog, user, session)?); + } rows.push(table); } Ok(Tables { rows, oids: query.relations.iter().map(|r| r.oid).collect() }) } /// The value of a row of a column of the catalog. -fn value(batch: &Batch, row: usize, ty: u32, session: &dyn Session) -> Result { +pub(crate) fn value(batch: &Batch, row: usize, ty: u32, session: &dyn Session) -> Result { if batch.is_null(row) { return Ok(Value::Null); } diff --git a/crates/rupg-exec/src/tests.rs b/crates/rupg-exec/src/tests.rs index 24765fd..a77af85 100644 --- a/crates/rupg-exec/src/tests.rs +++ b/crates/rupg-exec/src/tests.rs @@ -155,6 +155,10 @@ fn expressions() { assert_eq!(one("SELECT 7 / 2"), "3"); assert_eq!(one("SELECT 2 ^ 10 > 1000"), "t"); assert_eq!(one("SELECT -(3)"), "-3"); + assert_eq!( + row("SELECT '4294967295'::xid = -1, '3'::xid = 3, '3'::xid <> 3, '3'::xid = '3'::xid, '3'::xid <> '4'::xid").unwrap().unwrap(), + ["t", "t", "f", "t", "t"] + ); assert_eq!(one("SELECT 'abc' LIKE 'a%'"), "t"); assert_eq!(one("SELECT 'x' || 1"), "x1"); assert_eq!(one("SELECT 1 || true::text"), "1true"); @@ -997,3 +1001,138 @@ fn oid_alias_type_error_places() { assert_eq!(place("SELECT 1, to_regtype('varchar(0)')"), (None, None)); }); } + +/// The value of each column of the rows of the user objects has the type of the column. +#[test] +fn user_rows_have_the_column_types() { + use rupg_catalog::{ + Catalog, Column, ConKind, ForeignKey, NewForeignKey, NewIndex, NewIndexColumn, NewRelation, + ObjRef, SequenceInfo, + }; + use rupg_types::oid; + + fn kind_matches(ty: u32, value: &Value) -> bool { + match value { + Value::Null => true, + Value::Bool(_) => ty == oid::BOOL, + Value::Char(_) => ty == oid::CHAR, + Value::Int2(_) => ty == oid::INT2, + Value::Int4(_) => ty == oid::INT4, + Value::Int8(_) => ty == oid::INT8, + Value::Float4(_) => ty == oid::FLOAT4, + Value::Text(_) => matches!(ty, oid::NAME | oid::TEXT | oid::PG_NODE_TREE), + Value::Array(_) => { + matches!(ty, oid::INT2VECTOR | oid::OIDVECTOR | oid::INT2_ARRAY | oid::OID_ARRAY) + } + Value::Oid(_) => { + ty != oid::INT4 + && ty != oid::FLOAT4 + && rupg_pgcatalog::builtin::type_by_oid(ty).is_some_and(|t| t.len == 4) + } + _ => false, + } + } + + let mut catalog = Catalog::new(); + let s = catalog.create_schema("s", 10).unwrap(); + let new = |name: &str, columns: Vec| NewRelation { + namespace: s, + name: name.into(), + owner: 10, + columns, + }; + let int8 = |name: &str| Column::new(name, oid::INT8, -1, 0); + let info = SequenceInfo { + ty: oid::INT4, + start: 1, + increment: 1, + max: 9, + min: 1, + cache: 1, + cycle: false, + }; + let seq = catalog + .create_sequence(new("t_a_seq", vec![int8("last_value"), int8("log_cnt")]), info) + .unwrap(); + let t = catalog + .create_table(new( + "t", + vec![Column::new("a", oid::INT4, -1, 0), Column::new("b", oid::TEXT, -1, 100)], + )) + .unwrap(); + catalog + .add_default(t, 1, "nextval".into(), &[ObjRef::new(rupg_catalog::PG_CLASS, seq)]) + .unwrap(); + catalog.add_check(t, None, "a > 0".into(), vec![1], &[ObjRef::column(t, 1)], false).unwrap(); + catalog.create_toast(t).unwrap(); + let column = NewIndexColumn { + name: "a".into(), + key: 1, + ty: oid::INT4, + typmod: -1, + collation: 0, + class: 1978, + option: 0, + }; + let index = NewIndex { + table: t, + name: None, + columns: vec![column], + key_count: 1, + method: rupg_catalog::BTREE, + unique: true, + nulls_not_distinct: false, + constraint: Some(ConKind::Primary), + deferrable: false, + deferred: false, + exprs: None, + predicate: Some("a > 1".into()), + refs: Vec::new(), + }; + let (pkey, _) = catalog.create_index(index).unwrap(); + let foreign = ForeignKey { + table: t, + keys: vec![1], + update: 'a', + delete: 'a', + match_type: 's', + pf_eq: vec![96], + pp_eq: vec![96], + ff_eq: vec![96], + }; + let fkey = NewForeignKey { + table: t, + name: None, + keys: vec![1], + index: pkey, + deferrable: false, + deferred: false, + foreign, + }; + catalog.add_foreign_key(fkey).unwrap(); + + let session = TestSession { zone: Rc::new(FixedZone::utc()) }; + let tables = [ + "pg_namespace", + "pg_class", + "pg_type", + "pg_attribute", + "pg_attrdef", + "pg_constraint", + "pg_index", + "pg_sequence", + "pg_depend", + ]; + for name in tables { + let table = rupg_pgcatalog::catalog(name).unwrap(); + let rows = crate::user::rows(table, &catalog, &session).unwrap(); + assert!(!rows.is_empty(), "{name}"); + for row in rows { + assert_eq!(row.len(), table.columns.len(), "{name}"); + for (column, value) in table.columns.iter().zip(&row) { + assert!(kind_matches(column.type_oid, value), "{name}.{}: {value:?}", column.name); + assert!(!value.is_null() || !column.not_null, "{name}.{} is null", column.name); + } + } + } +} diff --git a/crates/rupg-exec/src/user.rs b/crates/rupg-exec/src/user.rs new file mode 100644 index 0000000..cab295b --- /dev/null +++ b/crates/rupg-exec/src/user.rs @@ -0,0 +1,412 @@ +//! The rows of the catalog for the user objects: the schemas, the relations, their row types and columns, the column defaults, the constraints, the indexes, the sequences and the dependencies between them. A scan of a table of the catalog gives these rows after the static rows. +//! +//! The columns of `pg_node_tree` hold the expressions in the form of the analyzer, not in the form of `nodeToString`. + +use rupg_catalog::{Catalog, ConKind, Constraint, RelKind, Relation}; +use rupg_common::{Error, Result}; +use rupg_func::Session; +use rupg_pgcatalog::{Catalog as Table, builtin}; +use rupg_types::{Array, Value}; + +use crate::scan::value; + +/// `FirstNormalTransactionId`, the `relfrozenxid` of the tables of the catalog. +const FIRST_NORMAL_XID: u32 = 3; +/// The OID of the access method `heap`. +const HEAP: u32 = 2; + +/// A row of a table of the catalog. +struct Row<'a> { + table: &'a Table, + values: Vec, +} + +impl<'a> Row<'a> { + /// A row with a null in each column. + fn new(table: &'a Table) -> Row<'a> { + Row { table, values: vec![Value::Null; table.columns.len()] } + } + + /// A copy of a static row. + fn copy(table: &'a Table, row: usize, session: &dyn Session) -> Result> { + let values = table + .columns + .iter() + .zip(table.rows) + .map(|(column, batch)| value(batch, row, column.type_oid, session)) + .collect::>()?; + Ok(Row { table, values }) + } + + /// Sets the value of a column. + fn set(&mut self, name: &str, value: Value) -> &mut Row<'a> { + if let Some(i) = self.table.columns.iter().position(|c| c.name == name) { + self.values[i] = value; + } + self + } +} + +/// The value of a static row of a column. +fn cell(table: &Table, row: usize, name: &str, session: &dyn Session) -> Result { + let i = table + .columns + .iter() + .position(|c| c.name == name) + .ok_or_else(|| Error::internal(format!("{} has no column {name}", table.name)))?; + value(&table.rows[i], row, table.columns[i].type_oid, session) +} + +fn oid(value: u32) -> Value { + Value::Oid(value) +} + +/// A `"char"` value from a code. +fn code(value: char) -> Value { + Value::Char(u8::try_from(value).unwrap_or(b' ')) +} + +/// An `int2[]`, or null for no element. +fn int2_array(values: &[i16]) -> Value { + if values.is_empty() { + return Value::Null; + } + Value::Array(Box::new(Array::one(values.iter().map(|&v| Some(Value::Int2(v))).collect()))) +} + +/// An `oid[]`, or null for no element. +fn oid_array(values: &[u32]) -> Value { + if values.is_empty() { + return Value::Null; + } + Value::Array(Box::new(Array::one(values.iter().map(|&v| Some(Value::Oid(v))).collect()))) +} + +/// An `int2vector`. +fn int2_vector(values: &[i16]) -> Value { + Value::Array(Box::new(Array::vector(values.iter().map(|&v| Some(Value::Int2(v))).collect()))) +} + +/// An `oidvector`. +fn oid_vector(values: &[u32]) -> Value { + Value::Array(Box::new(Array::vector(values.iter().map(|&v| Some(Value::Oid(v))).collect()))) +} + +/// A `pg_node_tree` value, or null. +fn tree(expr: Option<&String>) -> Value { + expr.map_or(Value::Null, |e| Value::text(e.clone())) +} + +/// The number of a column, from 1. +fn number(index: usize) -> i16 { + i16::try_from(index + 1).unwrap_or(i16::MAX) +} + +/// The rows of the user objects in a table of the catalog, with a value for each column. +/// +/// # Errors +/// +/// An internal error when a static row that a row copies is not there. +pub(crate) fn rows( + table: &Table, + catalog: &Catalog, + session: &dyn Session, +) -> Result>> { + let rows = match table.name { + "pg_namespace" => namespaces(table, catalog), + "pg_class" => classes(table, catalog), + "pg_type" => types(table, catalog, session)?, + "pg_attribute" => attributes(table, catalog, session)?, + "pg_attrdef" => defaults(table, catalog), + "pg_constraint" => constraints(table, catalog), + "pg_index" => indexes(table, catalog), + "pg_sequence" => sequences(table, catalog), + "pg_depend" => depends(table, catalog), + _ => Vec::new(), + }; + Ok(rows.into_iter().map(|row| row.values).collect()) +} + +fn namespaces<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .schemas() + .map(|schema| { + let mut row = Row::new(table); + row.set("oid", oid(schema.oid)) + .set("nspname", Value::text(schema.name.clone())) + .set("nspowner", oid(schema.owner)); + row + }) + .collect() +} + +fn classes<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .relations() + .map(|rel| { + let heap = matches!(rel.kind, RelKind::Table | RelKind::Toast); + let (am, pages, tuples) = match rel.kind { + RelKind::Table | RelKind::Toast => (HEAP, 0, -1.0), + RelKind::Index => (rel.index.as_ref().map_or(0, |i| i.method), 1, 0.0), + RelKind::Sequence => (0, 1, 1.0), + }; + let checks = catalog.constraints_of(rel.oid).filter(|c| c.kind == ConKind::Check); + let mut row = Row::new(table); + row.set("oid", oid(rel.oid)) + .set("relname", Value::text(rel.name.clone())) + .set("relnamespace", oid(rel.namespace)) + .set("reltype", oid(rel.row_type)) + .set("reloftype", oid(0)) + .set("relowner", oid(rel.owner)) + .set("relam", oid(am)) + .set("relfilenode", oid(rel.oid)) + .set("reltablespace", oid(0)) + .set("relpages", Value::Int4(pages)) + .set("reltuples", Value::Float4(tuples)) + .set("relallvisible", Value::Int4(0)) + .set("relallfrozen", Value::Int4(0)) + .set("reltoastrelid", oid(rel.toast)) + .set("relhasindex", Value::Bool(rel.has_index)) + .set("relisshared", Value::Bool(false)) + .set("relpersistence", code('p')) + .set("relkind", code(rel.kind.code())) + .set("relnatts", Value::Int2(number(rel.columns.len()) - 1)) + .set("relchecks", Value::Int2(i16::try_from(checks.count()).unwrap_or(i16::MAX))) + .set("relhasrules", Value::Bool(false)) + .set("relhastriggers", Value::Bool(rel.has_triggers)) + .set("relhassubclass", Value::Bool(false)) + .set("relrowsecurity", Value::Bool(false)) + .set("relforcerowsecurity", Value::Bool(false)) + .set("relispopulated", Value::Bool(true)) + .set("relreplident", code(if rel.kind == RelKind::Table { 'd' } else { 'n' })) + .set("relispartition", Value::Bool(false)) + .set("relrewrite", oid(0)) + .set("relfrozenxid", oid(if heap { FIRST_NORMAL_XID } else { 0 })) + .set("relminmxid", oid(u32::from(heap))); + row + }) + .collect() +} + +/// The static row of `pg_type` with the OID. +fn type_row(table: &Table, type_oid: u32, session: &dyn Session) -> Result { + for row in 0..table.len { + if cell(table, row, "oid", session)? == Value::Oid(type_oid) { + return Ok(row); + } + } + Err(Error::internal(format!("cache lookup failed for type {type_oid}"))) +} + +/// The row types and their array types. Each row copies the row of the row type of `pg_class` or of its array type, which have the same form. +fn types<'a>(table: &'a Table, catalog: &Catalog, session: &dyn Session) -> Result>> { + let row_type = rupg_pgcatalog::catalog("pg_class").map_or(0, |c| c.rowtype_oid); + let array_type = builtin::type_by_oid(row_type).map_or(0, |t| t.array); + let (row_template, array_template) = + (type_row(table, row_type, session)?, type_row(table, array_type, session)?); + let mut rows = Vec::new(); + for ty in catalog.types() { + let template = if ty.relation == 0 { array_template } else { row_template }; + let mut row = Row::copy(table, template, session)?; + row.set("oid", oid(ty.oid)) + .set("typname", Value::text(ty.name.clone())) + .set("typnamespace", oid(ty.namespace)) + .set("typowner", oid(ty.owner)) + .set("typrelid", oid(ty.relation)) + .set("typelem", oid(ty.element)) + .set("typarray", oid(ty.array)); + rows.push(row); + } + Ok(rows) +} + +/// The system columns of each table, sequence and TOAST table, which copy those of `pg_class`, and the columns of each relation. +fn attributes<'a>( + table: &'a Table, + catalog: &Catalog, + session: &dyn Session, +) -> Result>> { + let pg_class = rupg_pgcatalog::catalog("pg_class").map_or(0, |c| c.oid); + let mut system = Vec::new(); + for row in 0..table.len { + if cell(table, row, "attrelid", session)? == Value::Oid(pg_class) + && cell(table, row, "attnum", session)?.as_i64().is_some_and(|n| n < 0) + { + system.push(row); + } + } + let mut rows = Vec::new(); + for rel in catalog.relations() { + if rel.kind != RelKind::Index { + for &template in &system { + let mut row = Row::copy(table, template, session)?; + row.set("attrelid", oid(rel.oid)); + rows.push(row); + } + } + for (i, column) in rel.columns.iter().enumerate() { + rows.push(attribute(table, rel, i, column)); + } + } + Ok(rows) +} + +/// The `pg_attribute` row of a column. +fn attribute<'a>( + table: &'a Table, + rel: &Relation, + index: usize, + column: &rupg_catalog::Column, +) -> Row<'a> { + let (len, by_val, align, mut storage) = match builtin::type_by_oid(column.ty) { + Some(t) => (t.len, t.by_val, t.align, t.storage), + None => (-1, false, b'd', b'x'), + }; + if rel.kind == RelKind::Toast { + storage = b'p'; + } + let mut row = Row::new(table); + row.set("attrelid", oid(rel.oid)) + .set("attname", Value::text(column.name.clone())) + .set("atttypid", oid(column.ty)) + .set("attlen", Value::Int2(len)) + .set("attnum", Value::Int2(number(index))) + .set("atttypmod", Value::Int4(column.typmod)) + .set("attndims", Value::Int2(column.ndims)) + .set("attbyval", Value::Bool(by_val)) + .set("attalign", Value::Char(align)) + .set("attstorage", Value::Char(storage)) + .set("attcompression", Value::Char(0)) + .set("attnotnull", Value::Bool(column.not_null)) + .set("atthasdef", Value::Bool(column.has_default)) + .set("atthasmissing", Value::Bool(false)) + .set("attidentity", Value::Char(0)) + .set("attgenerated", Value::Char(0)) + .set("attisdropped", Value::Bool(false)) + .set("attislocal", Value::Bool(true)) + .set("attinhcount", Value::Int2(0)) + .set("attcollation", oid(column.collation)); + row +} + +fn defaults<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .defaults() + .map(|def| { + let mut row = Row::new(table); + row.set("oid", oid(def.oid)) + .set("adrelid", oid(def.relation)) + .set("adnum", Value::Int2(def.column)) + .set("adbin", Value::text(def.expr.clone())); + row + }) + .collect() +} + +fn constraints<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog.constraints().map(|con| constraint(table, con)).collect() +} + +/// The `pg_constraint` row of a constraint. +fn constraint<'a>(table: &'a Table, con: &Constraint) -> Row<'a> { + let foreign = con.foreign.as_ref(); + let mut row = Row::new(table); + row.set("oid", oid(con.oid)) + .set("conname", Value::text(con.name.clone())) + .set("connamespace", oid(con.namespace)) + .set("contype", code(con.kind.code())) + .set("condeferrable", Value::Bool(con.deferrable)) + .set("condeferred", Value::Bool(con.deferred)) + .set("conenforced", Value::Bool(true)) + .set("convalidated", Value::Bool(con.validated)) + .set("conrelid", oid(con.relation)) + .set("contypid", oid(0)) + .set("conindid", oid(con.index)) + .set("conparentid", oid(0)) + .set("confrelid", oid(foreign.map_or(0, |f| f.table))) + .set("confupdtype", code(foreign.map_or(' ', |f| f.update))) + .set("confdeltype", code(foreign.map_or(' ', |f| f.delete))) + .set("confmatchtype", code(foreign.map_or(' ', |f| f.match_type))) + .set("conislocal", Value::Bool(true)) + .set("coninhcount", Value::Int2(0)) + .set("connoinherit", Value::Bool(con.no_inherit)) + .set("conperiod", Value::Bool(false)) + .set("conkey", int2_array(&con.keys)) + .set("conbin", tree(con.expr.as_ref())); + if let Some(f) = foreign { + row.set("confkey", int2_array(&f.keys)) + .set("conpfeqop", oid_array(&f.pf_eq)) + .set("conppeqop", oid_array(&f.pp_eq)) + .set("conffeqop", oid_array(&f.ff_eq)); + } + row +} + +fn indexes<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .relations() + .filter_map(|rel| { + let info = rel.index.as_ref()?; + let mut row = Row::new(table); + row.set("indexrelid", oid(rel.oid)) + .set("indrelid", oid(info.table)) + .set("indnatts", Value::Int2(number(info.keys.len()) - 1)) + .set("indnkeyatts", Value::Int2(info.key_count)) + .set("indisunique", Value::Bool(info.unique)) + .set("indnullsnotdistinct", Value::Bool(info.nulls_not_distinct)) + .set("indisprimary", Value::Bool(info.primary)) + .set("indisexclusion", Value::Bool(false)) + .set("indimmediate", Value::Bool(info.immediate)) + .set("indisclustered", Value::Bool(false)) + .set("indisvalid", Value::Bool(true)) + .set("indcheckxmin", Value::Bool(false)) + .set("indisready", Value::Bool(true)) + .set("indislive", Value::Bool(true)) + .set("indisreplident", Value::Bool(false)) + .set("indkey", int2_vector(&info.keys)) + .set("indcollation", oid_vector(&info.collations)) + .set("indclass", oid_vector(&info.classes)) + .set("indoption", int2_vector(&info.options)) + .set("indexprs", tree(info.exprs.as_ref())) + .set("indpred", tree(info.predicate.as_ref())); + Some(row) + }) + .collect() +} + +fn sequences<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .relations() + .filter_map(|rel| { + let seq = rel.sequence.as_ref()?; + let mut row = Row::new(table); + row.set("seqrelid", oid(rel.oid)) + .set("seqtypid", oid(seq.ty)) + .set("seqstart", Value::Int8(seq.start)) + .set("seqincrement", Value::Int8(seq.increment)) + .set("seqmax", Value::Int8(seq.max)) + .set("seqmin", Value::Int8(seq.min)) + .set("seqcache", Value::Int8(seq.cache)) + .set("seqcycle", Value::Bool(seq.cycle)); + Some(row) + }) + .collect() +} + +fn depends<'a>(table: &'a Table, catalog: &Catalog) -> Vec> { + catalog + .depends() + .iter() + .map(|dep| { + let mut row = Row::new(table); + row.set("classid", oid(dep.object.class)) + .set("objid", oid(dep.object.oid)) + .set("objsubid", Value::Int4(dep.object.sub)) + .set("refclassid", oid(dep.referenced.class)) + .set("refobjid", oid(dep.referenced.oid)) + .set("refobjsubid", Value::Int4(dep.referenced.sub)) + .set("deptype", code(dep.kind.code())); + row + }) + .collect() +} diff --git a/crates/rupg-func/Cargo.toml b/crates/rupg-func/Cargo.toml index 446603b..a1d4971 100644 --- a/crates/rupg-func/Cargo.toml +++ b/crates/rupg-func/Cargo.toml @@ -16,6 +16,7 @@ publish = false [dependencies] md-5.workspace = true rupg-analyze.workspace = true +rupg-catalog.workspace = true rupg-common.workspace = true rupg-pgcatalog.workspace = true rupg-sql.workspace = true diff --git a/crates/rupg-func/src/compare.rs b/crates/rupg-func/src/compare.rs index 4336d2a..4c5bf77 100644 --- a/crates/rupg-func/src/compare.rs +++ b/crates/rupg-func/src/compare.rs @@ -317,15 +317,24 @@ pub(crate) fn by_operator(name: &str, left: u32, right: u32) -> Option { }) } +/// The number of an `xid`, or of the `int4` of `xideqint4` and `xidneqint4`, which compare the bits of the `int4` as an `xid`. +fn xid_number(value: &Value) -> Result { + match value { + Value::Oid(n) => Ok(*n), + Value::Int4(n) => Ok(n.cast_unsigned()), + _ => Err(bad_value()), + } +} + /// `xideq` and `xidneq` of `xid.c`: the values are equal when the numbers are equal. fn xid_eq(_: &Call<'_>, args: &[Value]) -> Result { - let [Value::Oid(a), Value::Oid(b)] = args else { return Err(bad_value()) }; - Ok(Value::Bool(a == b)) + let [a, b] = args else { return Err(bad_value()) }; + Ok(Value::Bool(xid_number(a)? == xid_number(b)?)) } fn xid_ne(_: &Call<'_>, args: &[Value]) -> Result { - let [Value::Oid(a), Value::Oid(b)] = args else { return Err(bad_value()) }; - Ok(Value::Bool(a != b)) + let [a, b] = args else { return Err(bad_value()) }; + Ok(Value::Bool(xid_number(a)? != xid_number(b)?)) } /// The kernel of `larger`, `smaller` and the B-tree `cmp` functions. diff --git a/crates/rupg-func/src/io.rs b/crates/rupg-func/src/io.rs index 6db63a7..a78b953 100644 --- a/crates/rupg-func/src/io.rs +++ b/crates/rupg-func/src/io.rs @@ -226,6 +226,8 @@ pub fn input(ty: u32, text: &str, typmod: i32, session: &dyn Session) -> Result< oid::INT4 => Value::Int4(types::int4_in(text).map_err(type_error)?), oid::INT8 => Value::Int8(types::int8_in(text).map_err(type_error)?), oid::OID => Value::Oid(types::oid_in(text).map_err(type_error)?), + XID => Value::Oid(types::uint32_in(text, "xid").map_err(type_error)?), + CID => Value::Oid(types::uint32_in(text, "cid").map_err(type_error)?), oid::TEXT | oid::UNKNOWN | oid::CSTRING => Value::text(text), oid::VARCHAR => Value::text(types::varchar_in(text, typmod).map_err(type_error)?), oid::BPCHAR => Value::text(types::bpchar_in(text, typmod).map_err(type_error)?), diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index 60229f0..0f6b841 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -74,6 +74,10 @@ pub trait Session { /// /// The errors of `SET` for the name and the value. fn set_setting(&self, name: &str, value: Option<&str>, local: bool) -> Result; + /// The catalog of the user objects that the statement sees, or `None` when the session has no catalog. + fn catalog(&self) -> Option<&rupg_catalog::Catalog> { + None + } } /// A call of a function. diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 5890308..45dd1a8 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1425,6 +1425,96 @@ mod tests { big_stack(definitions_cases); } + /// The queries of the catalog in `connection/catalog_rows.out`. + const CATALOG_QUERIES: [(&str, &str); 9] = [ + ( + "pg_namespace", + "SELECT oid, nspname, nspowner, nspacl FROM pg_namespace WHERE oid >= 16384 ORDER BY oid", + ), + ( + "pg_class", + "SELECT oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, relallfrozen, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid = 0, relminmxid, relacl, reloptions, relpartbound FROM pg_class WHERE oid >= 16384 ORDER BY oid", + ), + ( + "pg_type", + "SELECT oid, typname, typnamespace, typowner, typlen, typbyval, typtype, typcategory, typispreferred, typisdefined, typdelim, typrelid, typsubscript, typelem, typarray, typinput, typoutput, typreceive, typsend, typmodin, typmodout, typanalyze, typalign, typstorage, typnotnull, typbasetype, typtypmod, typndims, typcollation, typdefaultbin, typdefault, typacl FROM pg_type WHERE oid >= 16384 ORDER BY oid", + ), + ( + "pg_attribute", + "SELECT attrelid, attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval, attalign, attstorage, attcompression, attnotnull, atthasdef, atthasmissing, attidentity, attgenerated, attisdropped, attislocal, attinhcount, attcollation, attstattarget, attacl, attoptions, attfdwoptions, attmissingval FROM pg_attribute WHERE attrelid >= 16384 ORDER BY attrelid, attnum", + ), + ( + "pg_attrdef", + "SELECT oid, adrelid, adnum, adbin IS NOT NULL FROM pg_attrdef WHERE oid >= 16384 ORDER BY oid", + ), + ( + "pg_constraint", + "SELECT oid, conname, connamespace, contype, condeferrable, condeferred, conenforced, convalidated, conrelid, contypid, conindid, conparentid, confrelid, confupdtype, confdeltype, confmatchtype, conislocal, coninhcount, connoinherit, conperiod, conkey, confkey, conpfeqop, conppeqop, conffeqop, confdelsetcols, conexclop, conbin IS NOT NULL FROM pg_constraint WHERE oid >= 16384 ORDER BY oid", + ), + ( + "pg_index", + "SELECT indexrelid, indrelid, indnatts, indnkeyatts, indisunique, indnullsnotdistinct, indisprimary, indisexclusion, indimmediate, indisclustered, indisvalid, indcheckxmin, indisready, indislive, indisreplident, indkey, indcollation, indclass, indoption, indexprs IS NOT NULL, indpred IS NOT NULL FROM pg_index WHERE indexrelid >= 16384 ORDER BY indexrelid", + ), + ( + "pg_sequence", + "SELECT seqrelid, seqtypid, seqstart, seqincrement, seqmax, seqmin, seqcache, seqcycle FROM pg_sequence WHERE seqrelid >= 16384 ORDER BY seqrelid", + ), + ( + "pg_depend", + "SELECT classid, objid, objsubid, refclassid, refobjid, refobjsubid, deptype FROM pg_depend WHERE objid >= 16384 AND classid <> 2620 ORDER BY classid, objid, objsubid, refclassid, refobjid, refobjsubid, deptype", + ), + ]; + + /// The rows of the user objects in the catalog. `connection/catalog_rows.out` is the output of PostgreSQL 19 for the same statements and queries in a new database, in the unaligned format of `psql` with the field separator `,` and the null string `NULL`, with the OIDs moved to start at 16384. The queries leave out the expressions, which rupg keeps in its own form, and the value of `relfrozenxid`, which depends on the cluster. + fn catalog_rows_cases() { + let (mut c, _) = connect(); + let statements = [ + ("CREATE SCHEMA s", "CREATE SCHEMA"), + ( + "CREATE TABLE s.t (a int PRIMARY KEY, b text NOT NULL DEFAULT 'x', c numeric(5,2) CHECK (c > 0), d serial)", + "CREATE TABLE", + ), + ("CREATE INDEX ON s.t (lower(b)) WHERE a > 1", "CREATE INDEX"), + ("CREATE TABLE s.u (a int REFERENCES s.t, b varchar(10)[] UNIQUE)", "CREATE TABLE"), + ]; + for (sql, tag) in statements { + assert_eq!(send(&mut c, &query(sql)), [tag, "ready I"], "{sql}"); + } + let mut expected: Vec<(&str, Vec<&str>)> = Vec::new(); + for line in include_str!("connection/catalog_rows.out").lines() { + match expected.last_mut() { + Some((_, rows)) if !line.starts_with("pg_") => rows.push(line), + _ => expected.push((line, Vec::new())), + } + } + assert_eq!(expected.len(), CATALOG_QUERIES.len()); + for ((name, sql), (want_name, want)) in CATALOG_QUERIES.iter().zip(&expected) { + assert_eq!(name, want_name); + let lines = send(&mut c, &query(sql)); + let rows: Vec<&str> = lines.iter().filter_map(|l| l.strip_prefix("row ")).collect(); + for (i, (got, want)) in rows.iter().zip(want).enumerate() { + assert_eq!(got, want, "row {} of {name}", i + 1); + } + assert_eq!(rows.len(), want.len(), "{name}: {lines:?}"); + } + assert_eq!( + send(&mut c, &query("CREATE INDEX ON pg_toast.pg_toast_16386 (chunk_id)")), + ["ERROR 42501 permission denied: \"pg_toast_16386\" is a system catalog", "ready I"] + ); + assert_eq!( + send( + &mut c, + &query("CREATE TABLE v (a oid REFERENCES pg_toast.pg_toast_16386 (chunk_id))") + ), + ["ERROR 42809 referenced relation \"pg_toast_16386\" is not a table", "ready I"] + ); + } + + #[test] + fn catalog_rows() { + big_stack(catalog_rows_cases); + } + #[test] fn extended_selects() { big_stack(extended_selects_cases); diff --git a/crates/rupg-session/src/connection/catalog_rows.out b/crates/rupg-session/src/connection/catalog_rows.out new file mode 100644 index 0000000..0b8f3b4 --- /dev/null +++ b/crates/rupg-session/src/connection/catalog_rows.out @@ -0,0 +1,123 @@ +pg_namespace +16384,s,10,NULL +pg_class +16385,t_d_seq,16384,0,0,10,0,16385,0,1,1,0,0,0,f,f,p,S,3,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +16386,t,16384,16388,0,10,2,16386,0,0,-1,0,0,16395,t,f,p,r,4,1,f,t,f,f,f,t,d,f,0,f,1,NULL,NULL,NULL +16395,pg_toast_16386,99,0,0,10,2,16395,0,0,-1,0,0,0,t,f,p,t,3,0,f,f,f,f,f,t,n,f,0,f,1,NULL,NULL,NULL +16396,pg_toast_16386_index,99,0,0,10,403,16396,0,1,0,0,0,0,f,f,p,i,2,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +16397,t_pkey,16384,0,0,10,403,16397,0,1,0,0,0,0,f,f,p,i,1,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +16399,t_lower_idx,16384,0,0,10,403,16399,0,1,0,0,0,0,f,f,p,i,1,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +16400,u,16384,16402,0,10,2,16400,0,0,-1,0,0,16403,t,f,p,r,2,0,f,t,f,f,f,t,d,f,0,f,1,NULL,NULL,NULL +16403,pg_toast_16400,99,0,0,10,2,16403,0,0,-1,0,0,0,t,f,p,t,3,0,f,f,f,f,f,t,n,f,0,f,1,NULL,NULL,NULL +16404,pg_toast_16400_index,99,0,0,10,403,16404,0,1,0,0,0,0,f,f,p,i,2,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +16405,u_b_key,16384,0,0,10,403,16405,0,1,0,0,0,0,f,f,p,i,1,0,f,f,f,f,f,t,n,f,0,t,0,NULL,NULL,NULL +pg_type +16387,_t,16384,10,-1,f,b,A,f,t,,,0,array_subscript_handler,16388,0,array_in,array_out,array_recv,array_send,-,-,array_typanalyze,d,x,f,0,-1,0,0,NULL,NULL,NULL +16388,t,16384,10,-1,f,c,C,f,t,,,16386,-,0,16387,record_in,record_out,record_recv,record_send,-,-,-,d,x,f,0,-1,0,0,NULL,NULL,NULL +16401,_u,16384,10,-1,f,b,A,f,t,,,0,array_subscript_handler,16402,0,array_in,array_out,array_recv,array_send,-,-,array_typanalyze,d,x,f,0,-1,0,0,NULL,NULL,NULL +16402,u,16384,10,-1,f,c,C,f,t,,,16400,-,0,16401,record_in,record_out,record_recv,record_send,-,-,-,d,x,f,0,-1,0,0,NULL,NULL,NULL +pg_attribute +16385,tableoid,26,4,-6,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,cmax,29,4,-5,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,xmax,28,4,-4,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,cmin,29,4,-3,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,xmin,28,4,-2,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,ctid,27,6,-1,-1,0,f,s,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,last_value,20,8,1,-1,0,t,d,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,log_cnt,20,8,2,-1,0,t,d,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16385,is_called,16,1,3,-1,0,t,c,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,tableoid,26,4,-6,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,cmax,29,4,-5,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,xmax,28,4,-4,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,cmin,29,4,-3,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,xmin,28,4,-2,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,ctid,27,6,-1,-1,0,f,s,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,a,23,4,1,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,b,25,-1,2,-1,0,f,i,x,,t,t,f,,,f,t,0,100,NULL,NULL,NULL,NULL,NULL +16386,c,1700,-1,3,327686,0,f,i,m,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16386,d,23,4,4,-1,0,t,i,p,,t,t,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,tableoid,26,4,-6,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,cmax,29,4,-5,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,xmax,28,4,-4,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,cmin,29,4,-3,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,xmin,28,4,-2,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,ctid,27,6,-1,-1,0,f,s,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,chunk_id,26,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,chunk_seq,23,4,2,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16395,chunk_data,17,-1,3,-1,0,f,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16396,chunk_id,26,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16396,chunk_seq,23,4,2,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16397,a,23,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16399,lower,25,-1,1,-1,0,f,i,x,,f,f,f,,,f,t,0,100,NULL,NULL,NULL,NULL,NULL +16400,tableoid,26,4,-6,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,cmax,29,4,-5,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,xmax,28,4,-4,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,cmin,29,4,-3,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,xmin,28,4,-2,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,ctid,27,6,-1,-1,0,f,s,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,a,23,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16400,b,1015,-1,2,14,1,f,i,x,,f,f,f,,,f,t,0,100,NULL,NULL,NULL,NULL,NULL +16403,tableoid,26,4,-6,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,cmax,29,4,-5,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,xmax,28,4,-4,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,cmin,29,4,-3,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,xmin,28,4,-2,-1,0,t,i,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,ctid,27,6,-1,-1,0,f,s,p,,t,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,chunk_id,26,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,chunk_seq,23,4,2,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16403,chunk_data,17,-1,3,-1,0,f,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16404,chunk_id,26,4,1,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16404,chunk_seq,23,4,2,-1,0,t,i,p,,f,f,f,,,f,t,0,0,NULL,NULL,NULL,NULL,NULL +16405,b,1015,-1,1,14,1,f,i,x,,f,f,f,,,f,t,0,100,NULL,NULL,NULL,NULL,NULL +pg_attrdef +16389,16386,2,t +16390,16386,4,t +pg_constraint +16391,t_c_check,16384,c,f,f,t,t,16386,0,0,0,0, , , ,t,0,f,f,{3},NULL,NULL,NULL,NULL,NULL,NULL,t +16392,t_a_not_null,16384,n,f,f,t,t,16386,0,0,0,0, , , ,t,0,f,f,{1},NULL,NULL,NULL,NULL,NULL,NULL,f +16393,t_b_not_null,16384,n,f,f,t,t,16386,0,0,0,0, , , ,t,0,f,f,{2},NULL,NULL,NULL,NULL,NULL,NULL,f +16394,t_d_not_null,16384,n,f,f,t,t,16386,0,0,0,0, , , ,t,0,f,f,{4},NULL,NULL,NULL,NULL,NULL,NULL,f +16398,t_pkey,16384,p,f,f,t,t,16386,0,16397,0,0, , , ,t,0,t,f,{1},NULL,NULL,NULL,NULL,NULL,NULL,f +16406,u_b_key,16384,u,f,f,t,t,16400,0,16405,0,0, , , ,t,0,t,f,{2},NULL,NULL,NULL,NULL,NULL,NULL,f +16407,u_a_fkey,16384,f,f,f,t,t,16400,0,16397,0,16386,a,a,s,t,0,t,f,{1},{1},{96},{96},{96},NULL,NULL,f +pg_index +16396,16395,2,2,t,f,t,f,t,f,t,f,t,t,f,1 2,0 0,1981 1978,0 0,f,f +16397,16386,1,1,t,f,t,f,t,f,t,f,t,t,f,1,0,1978,0,f,f +16399,16386,1,1,f,f,f,f,t,f,t,f,t,t,f,0,100,3126,0,t,t +16404,16403,2,2,t,f,t,f,t,f,t,f,t,t,f,1 2,0 0,1981 1978,0 0,f,f +16405,16400,1,1,t,f,f,f,t,f,t,f,t,t,f,2,100,10000,0,f,f +pg_sequence +16385,23,1,1,2147483647,1,1,f +pg_depend +1247,16387,0,1247,16388,0,i +1247,16388,0,1259,16386,0,i +1247,16401,0,1247,16402,0,i +1247,16402,0,1259,16400,0,i +1259,16385,0,1259,16386,4,a +1259,16385,0,2615,16384,0,n +1259,16386,0,2615,16384,0,n +1259,16395,0,1259,16386,0,i +1259,16396,0,1259,16395,1,a +1259,16396,0,1259,16395,2,a +1259,16397,0,2606,16398,0,i +1259,16399,0,1259,16386,0,a +1259,16399,0,1259,16386,1,a +1259,16399,0,1259,16386,2,a +1259,16400,0,2615,16384,0,n +1259,16403,0,1259,16400,0,i +1259,16404,0,1259,16403,1,a +1259,16404,0,1259,16403,2,a +1259,16405,0,2606,16406,0,i +2604,16389,0,1259,16386,2,a +2604,16390,0,1259,16385,0,n +2604,16390,0,1259,16386,4,a +2606,16391,0,1259,16386,3,a +2606,16391,0,1259,16386,3,n +2606,16392,0,1259,16386,1,a +2606,16393,0,1259,16386,2,a +2606,16394,0,1259,16386,4,a +2606,16398,0,1259,16386,1,a +2606,16406,0,1259,16400,2,a +2606,16407,0,1259,16386,1,n +2606,16407,0,1259,16397,0,n +2606,16407,0,1259,16400,1,a diff --git a/crates/rupg-session/src/query.rs b/crates/rupg-session/src/query.rs index 1e49982..c94a8c4 100644 --- a/crates/rupg-session/src/query.rs +++ b/crates/rupg-session/src/query.rs @@ -259,6 +259,10 @@ impl rupg_func::Session for Reader<'_> { self.database } + fn catalog(&self) -> Option<&Catalog> { + Some(&self.catalog) + } + fn schemas(&self) -> Vec { self.path().into_iter().map(|(name, _)| name).collect() } diff --git a/crates/rupg-types/src/lib.rs b/crates/rupg-types/src/lib.rs index 978a4a6..13c1c45 100644 --- a/crates/rupg-types/src/lib.rs +++ b/crates/rupg-types/src/lib.rs @@ -84,7 +84,7 @@ pub use float::{float4_in, float4_out, float8_in, float8_out}; pub use generated::oids as oid; pub use json::json_in; pub use jsonb::{jsonb_in, jsonb_recv, jsonb_send}; -pub use number::{int_out, int2_in, int4_in, int8_in, oid_in, oid_out, u64_out}; +pub use number::{int_out, int2_in, int4_in, int8_in, oid_in, oid_out, u64_out, uint32_in}; pub use numeric::{ Numeric, NumericSign, decimal_out, decimal_send, numeric_in, numeric_out, numeric_recv, numeric_send, diff --git a/crates/rupg-types/src/number.rs b/crates/rupg-types/src/number.rs index 78ae05e..6d4058c 100644 --- a/crates/rupg-types/src/number.rs +++ b/crates/rupg-types/src/number.rs @@ -158,23 +158,28 @@ fn strtoul(b: &[u8]) -> (u64, usize, bool) { /// The text input of `oid`. A negative number from -2147483648 to -1 is the OID with the same bits, so `-1` is 4294967295. pub fn oid_in(s: &str) -> Result { + uint32_in(s, "oid") +} + +/// `uint32in_subr` for all of `s`: the text input of `oid`, `xid` and `cid`. The errors show `type_name`. +pub fn uint32_in(s: &str, type_name: &str) -> Result { let b = s.as_bytes(); let (value, mut end, overflow) = strtoul(b); if end == 0 { - return Err(TypeError::syntax("oid", s)); + return Err(TypeError::syntax(type_name, s)); } if overflow { - return Err(TypeError::range(s, "oid")); + return Err(TypeError::range(s, type_name)); } while end < b.len() && is_space(b[end]) { end += 1; } if end != b.len() { - return Err(TypeError::syntax("oid", s)); + return Err(TypeError::syntax(type_name, s)); } let result = value as u32; if value != u64::from(result) && value != i64::from(result as i32) as u64 { - return Err(TypeError::range(s, "oid")); + return Err(TypeError::range(s, type_name)); } Ok(result) } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index bc237d5..ba425ae 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -264,6 +264,7 @@ version = "0.1.1" dependencies = [ "md-5", "rupg-analyze", + "rupg-catalog", "rupg-common", "rupg-pgcatalog", "rupg-sql",