diff --git a/ci/nightly/pipeline.template.yml b/ci/nightly/pipeline.template.yml index f25c31d5ef852..3e3fec1668155 100644 --- a/ci/nightly/pipeline.template.yml +++ b/ci/nightly/pipeline.template.yml @@ -3026,6 +3026,16 @@ steps: - group: Language key: language-tests steps: + - id: adbc + label: Arrow ADBC driver + depends_on: build-aarch64 + timeout_in_minutes: 30 + plugins: + - ./ci/plugins/mzcompose: + composition: adbc + agents: + queue: hetzner-aarch64-4cpu-8gb + - id: lang-csharp label: ":csharp:" depends_on: build-aarch64 diff --git a/console/types/materialize.d.ts b/console/types/materialize.d.ts index c47728f078583..25fc3e22c6e41 100644 --- a/console/types/materialize.d.ts +++ b/console/types/materialize.d.ts @@ -4019,6 +4019,7 @@ export interface MzTypePgMetadata { id: Generated; typinput: Generated; typreceive: Generated; + typsend: Generated; } export interface MzTypes { @@ -4260,6 +4261,7 @@ export interface PgTypeAllDatabases { typowner: number; typreceive: number; typrelid: number; + typsend: string; typtype: string; typtypmod: number; } diff --git a/src/adapter/src/catalog.rs b/src/adapter/src/catalog.rs index f21271b1d89f3..b29d44a2ad12f 100644 --- a/src/adapter/src/catalog.rs +++ b/src/adapter/src/catalog.rs @@ -3059,6 +3059,7 @@ mod tests { array: u32, input: u32, receive: u32, + send: u32, } struct PgOper { @@ -3098,7 +3099,7 @@ mod tests { let pg_type: BTreeMap<_, _> = query( &client, sql!( - "SELECT oid, typname, typtype::text, typelem, typarray, typinput::oid, typreceive::oid as typreceive FROM pg_type" + "SELECT oid, typname, typtype::text, typelem, typarray, typinput::oid, typreceive::oid as typreceive, typsend::oid as typsend FROM pg_type" ), &[], ) @@ -3114,6 +3115,7 @@ mod tests { array: row.get("typarray"), input: row.get("typinput"), receive: row.get("typreceive"), + send: row.get("typsend"), }; (oid, pg_type) }) @@ -3205,10 +3207,15 @@ mod tests { ty.oid, pg_ty.name, ty.name, ); - let (typinput_oid, typreceive_oid) = match &ty.details.pg_metadata { - None => (0, 0), - Some(pgmeta) => (pgmeta.typinput_oid, pgmeta.typreceive_oid), - }; + let (typinput_oid, typreceive_oid, typsend_oid) = + match &ty.details.pg_metadata { + None => (0, 0, 0), + Some(pgmeta) => ( + pgmeta.typinput_oid, + pgmeta.typreceive_oid, + pgmeta.typsend_oid, + ), + }; assert_eq!( typinput_oid, pg_ty.input, "type {} has typinput OID {:?} in mz but {:?} in pg", @@ -3219,6 +3226,15 @@ mod tests { "type {} has typreceive OID {:?} in mz but {:?} in pg", ty.name, typreceive_oid, pg_ty.receive, ); + // Unlike typinput and typreceive below, typsend is not also + // checked against `func_oids`. Nothing resolves a typsend OID + // to a name, so the corresponding `*send` functions are + // deliberately not registered as builtins. + assert_eq!( + typsend_oid, pg_ty.send, + "type {} has typsend OID {:?} in mz but {:?} in pg", + ty.name, typsend_oid, pg_ty.send, + ); if typinput_oid != 0 { assert!( func_oids.contains(&typinput_oid), diff --git a/src/adapter/src/catalog/builtin_table_updates.rs b/src/adapter/src/catalog/builtin_table_updates.rs index bb10328aabb56..b71258e3898f0 100644 --- a/src/adapter/src/catalog/builtin_table_updates.rs +++ b/src/adapter/src/catalog/builtin_table_updates.rs @@ -575,6 +575,7 @@ impl CatalogState { Datum::String(&id.to_string()), Datum::UInt32(pg_metadata.typinput_oid), Datum::UInt32(pg_metadata.typreceive_oid), + Datum::UInt32(pg_metadata.typsend_oid), ]), diff, )); diff --git a/src/adapter/src/catalog/open/builtin_schema_migration.rs b/src/adapter/src/catalog/open/builtin_schema_migration.rs index a96caf5281975..5ddc4839943e5 100644 --- a/src/adapter/src/catalog/open/builtin_schema_migration.rs +++ b/src/adapter/src/catalog/open/builtin_schema_migration.rs @@ -324,6 +324,15 @@ static MIGRATIONS: LazyLock> = LazyLock::new(|| { MZ_INTERNAL_SCHEMA, "mz_kafka_source_tables", ), + // `mz_type_pg_metadata` gained a trailing `typsend` column. See the NOTE + // above: this version must stay at the workspace's current dev version + // until the change ships. + MigrationStep::replacement( + "26.40.0-dev.0", + CatalogItemType::Table, + MZ_INTERNAL_SCHEMA, + "mz_type_pg_metadata", + ), // Converting the connection-detail builtin tables to materialized views // changes their catalog fingerprint, so each needs an explicit // replacement step. diff --git a/src/catalog/src/builtin.rs b/src/catalog/src/builtin.rs index a3dbda548f476..6fb54b7867f51 100644 --- a/src/catalog/src/builtin.rs +++ b/src/catalog/src/builtin.rs @@ -1655,6 +1655,112 @@ mod tests { use super::*; + /// Recomputes `mz_pgrepr::regproc::NAMES` from the builtin function registry + /// and fails when the checked-in copy has drifted. It is checked in as data + /// because `mz-pgrepr` sits below this crate in the dependency graph and so + /// cannot read the registry itself. + /// + /// Run with `REWRITE=1` to splice the recomputed table back into + /// `src/pgrepr-consts/src/regproc.rs`. + #[mz_ore::test] + #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux` + fn test_regproc_names_match_builtin_functions() { + // `effective_search_path` unconditionally prepends these two, so a + // uniquely named function in either resolves from its bare name. + const IMPLICITLY_SEARCHED: &[&str] = &[MZ_CATALOG_SCHEMA, PG_CATALOG_SCHEMA]; + + // A bare name only identifies one OID when exactly one impl anywhere in + // the registry carries it, so count impls across schemas. + let mut impls_per_name: BTreeMap<&str, usize> = BTreeMap::new(); + for func in BUILTINS::funcs() { + *impls_per_name.entry(func.name).or_default() += func.inner.func_impls().len(); + } + + let mut expected: BTreeMap = BTreeMap::new(); + for func in BUILTINS::funcs() { + // Mirrors PostgreSQL's `regprocout`, which qualifies a name that + // would not resolve back to this OID on its own. + let rendered = + if impls_per_name[func.name] == 1 && IMPLICITLY_SEARCHED.contains(&func.schema) { + func.name.to_string() + } else { + format!("{}.{}", func.schema, func.name) + }; + for details in func.inner.func_impls() { + let previous = expected.insert(details.oid, rendered.clone()); + assert_eq!( + previous, None, + "two builtin functions share OID {}", + details.oid + ); + } + } + + let table: String = expected + .iter() + .map(|(oid, name)| format!(" ({}, \"{}\"),\n", oid, name)) + .collect(); + + if std::env::var_os("REWRITE").is_some() { + rewrite_regproc_names(&table); + return; + } + + let actual: BTreeMap = mz_pgrepr::regproc::NAMES + .iter() + .map(|(oid, name)| (*oid, name.to_string())) + .collect(); + + if actual != expected { + panic!( + "mz_pgrepr::regproc::NAMES has drifted from the builtin function \ + registry. Regenerate it with:\n\n \ + REWRITE=1 cargo test -p mz-catalog \ + test_regproc_names_match_builtin_functions\n" + ); + } + } + + /// Replaces the generated region of `mz_pgrepr::regproc::NAMES` with + /// `table`, leaving every other byte of the file alone. + /// + /// The path is relative to this crate's directory, which is the working + /// directory `cargo test` runs in. + /// + /// A splice anchored anywhere but the table would clobber the lookup + /// functions below it, so both markers have to appear exactly once. + fn rewrite_regproc_names(table: &str) { + const PATH: &str = "../pgrepr-consts/src/regproc.rs"; + const BEGIN: &str = " // BEGIN GENERATED\n"; + const END: &str = " // END GENERATED\n"; + + let contents = + std::fs::read_to_string(PATH).unwrap_or_else(|e| panic!("reading '{PATH}': {e}")); + for marker in [BEGIN, END] { + let count = contents.matches(marker).count(); + assert_eq!( + count, + 1, + "'{}' appears {} times in '{}', expected exactly once", + marker.trim(), + count, + PATH + ); + } + let begin = contents.find(BEGIN).expect("checked above") + BEGIN.len(); + let end = contents.find(END).expect("checked above"); + assert!( + begin <= end, + "'{}' precedes '{}' in '{}'", + END.trim(), + BEGIN.trim(), + PATH + ); + + let rewritten = format!("{}{}{}", &contents[..begin], table, &contents[end..]); + std::fs::write(PATH, rewritten).unwrap_or_else(|e| panic!("writing '{PATH}': {e}")); + } + #[mz_ore::test] #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux` fn test_builtin_type_schema() { diff --git a/src/catalog/src/builtin/mz_catalog.rs b/src/catalog/src/builtin/mz_catalog.rs index 2179e7a62af82..3874ac8d7d177 100644 --- a/src/catalog/src/builtin/mz_catalog.rs +++ b/src/catalog/src/builtin/mz_catalog.rs @@ -194,6 +194,7 @@ pub const TYPE_MZ_ACL_ITEM_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; diff --git a/src/catalog/src/builtin/mz_internal.rs b/src/catalog/src/builtin/mz_internal.rs index 463df5d4168cd..123c870f2fce3 100644 --- a/src/catalog/src/builtin/mz_internal.rs +++ b/src/catalog/src/builtin/mz_internal.rs @@ -705,6 +705,10 @@ pub static MZ_TYPE_PG_METADATA: LazyLock = LazyLock::new(|| Builti .with_column("id", SqlScalarType::String.nullable(false)) .with_column("typinput", SqlScalarType::Oid.nullable(false)) .with_column("typreceive", SqlScalarType::Oid.nullable(false)) + // NOTE: `pg_type_all_databases` still needs `COALESCE` on this column, + // because its `LEFT JOIN` against this table yields NULLs for types with + // no PostgreSQL metadata. + .with_column("typsend", SqlScalarType::Oid.nullable(false)) .finish(), column_comments: BTreeMap::new(), is_retained_metrics_object: false, @@ -4753,6 +4757,7 @@ pub static PG_TYPE_ALL_DATABASES: LazyLock = LazyLock::new(|| { .with_column("typcollation", SqlScalarType::Oid.nullable(false)) .with_column("typdefault", SqlScalarType::String.nullable(true)) .with_column("database_name", SqlScalarType::String.nullable(true)) + .with_column("typsend", SqlScalarType::RegProc.nullable(false)) .finish(), column_comments: BTreeMap::new(), sql: " @@ -4821,7 +4826,8 @@ SELECT -- MZ doesn't support COLLATE so typcollation is filled with 0 0::pg_catalog.oid AS typcollation, NULL::pg_catalog.text AS typdefault, - d.name as database_name + d.name as database_name, + COALESCE(mz_internal.mz_type_pg_metadata.typsend, 0)::pg_catalog.regproc AS typsend FROM mz_catalog.mz_types LEFT JOIN mz_internal.mz_type_pg_metadata ON mz_catalog.mz_types.id = mz_internal.mz_type_pg_metadata.id diff --git a/src/catalog/src/builtin/pg_catalog.rs b/src/catalog/src/builtin/pg_catalog.rs index ffc6f2484cf50..78d914d54a86e 100644 --- a/src/catalog/src/builtin/pg_catalog.rs +++ b/src/catalog/src/builtin/pg_catalog.rs @@ -52,6 +52,7 @@ pub const TYPE_BOOL: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1242, typreceive_oid: 2436, + typsend_oid: 2437, }), }, }; @@ -66,6 +67,7 @@ pub const TYPE_BYTEA: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1244, typreceive_oid: 2412, + typsend_oid: 2413, }), }, }; @@ -80,6 +82,7 @@ pub const TYPE_INT8: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 460, typreceive_oid: 2408, + typsend_oid: 2409, }), }, }; @@ -94,6 +97,7 @@ pub const TYPE_INT4: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 42, typreceive_oid: 2406, + typsend_oid: 2407, }), }, }; @@ -108,6 +112,7 @@ pub const TYPE_TEXT: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 46, typreceive_oid: 2414, + typsend_oid: 2415, }), }, }; @@ -122,6 +127,7 @@ pub const TYPE_OID: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1798, typreceive_oid: 2418, + typsend_oid: 2419, }), }, }; @@ -136,6 +142,7 @@ pub const TYPE_FLOAT4: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 200, typreceive_oid: 2424, + typsend_oid: 2425, }), }, }; @@ -150,6 +157,7 @@ pub const TYPE_FLOAT8: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 214, typreceive_oid: 2426, + typsend_oid: 2427, }), }, }; @@ -166,6 +174,7 @@ pub const TYPE_BOOL_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -182,6 +191,7 @@ pub const TYPE_BYTEA_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -198,6 +208,7 @@ pub const TYPE_INT4_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -214,6 +225,7 @@ pub const TYPE_TEXT_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -230,6 +242,7 @@ pub const TYPE_INT8_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -246,6 +259,7 @@ pub const TYPE_FLOAT4_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -262,6 +276,7 @@ pub const TYPE_FLOAT8_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -278,6 +293,7 @@ pub const TYPE_OID_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -292,6 +308,7 @@ pub const TYPE_DATE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1084, typreceive_oid: 2468, + typsend_oid: 2469, }), }, }; @@ -306,6 +323,7 @@ pub const TYPE_TIME: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1143, typreceive_oid: 2470, + typsend_oid: 2471, }), }, }; @@ -320,6 +338,7 @@ pub const TYPE_TIMESTAMP: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1312, typreceive_oid: 2474, + typsend_oid: 2475, }), }, }; @@ -336,6 +355,7 @@ pub const TYPE_TIMESTAMP_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -352,6 +372,7 @@ pub const TYPE_DATE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -368,6 +389,7 @@ pub const TYPE_TIME_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -382,6 +404,7 @@ pub const TYPE_TIMESTAMPTZ: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1150, typreceive_oid: 2476, + typsend_oid: 2477, }), }, }; @@ -398,6 +421,7 @@ pub const TYPE_TIMESTAMPTZ_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -412,6 +436,7 @@ pub const TYPE_INTERVAL: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1160, typreceive_oid: 2478, + typsend_oid: 2479, }), }, }; @@ -428,6 +453,7 @@ pub const TYPE_INTERVAL_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -442,6 +468,7 @@ pub const TYPE_NAME: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 34, typreceive_oid: 2422, + typsend_oid: 2423, }), }, }; @@ -458,6 +485,7 @@ pub const TYPE_NAME_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -472,6 +500,7 @@ pub const TYPE_NUMERIC: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1701, typreceive_oid: 2460, + typsend_oid: 2461, }), }, }; @@ -488,6 +517,7 @@ pub const TYPE_NUMERIC_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -502,6 +532,7 @@ pub const TYPE_RECORD: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2290, typreceive_oid: 2402, + typsend_oid: 2403, }), }, }; @@ -518,6 +549,7 @@ pub const TYPE_RECORD_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -532,6 +564,7 @@ pub const TYPE_UUID: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2952, typreceive_oid: 2961, + typsend_oid: 2962, }), }, }; @@ -548,6 +581,7 @@ pub const TYPE_UUID_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -562,6 +596,7 @@ pub const TYPE_JSONB: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3806, typreceive_oid: 3805, + typsend_oid: 3803, }), }, }; @@ -578,6 +613,7 @@ pub const TYPE_JSONB_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -592,6 +628,7 @@ pub const TYPE_ANY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2294, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -606,6 +643,7 @@ pub const TYPE_ANYARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2296, typreceive_oid: 2502, + typsend_oid: 2503, }), }, }; @@ -620,6 +658,7 @@ pub const TYPE_ANYELEMENT: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2312, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -634,6 +673,7 @@ pub const TYPE_ANYNONARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2777, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -648,6 +688,7 @@ pub const TYPE_ANYRANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3832, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -662,6 +703,7 @@ pub const TYPE_CHAR: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1245, typreceive_oid: 2434, + typsend_oid: 2435, }), }, }; @@ -676,6 +718,7 @@ pub const TYPE_VARCHAR: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1046, typreceive_oid: 2432, + typsend_oid: 2433, }), }, }; @@ -690,6 +733,7 @@ pub const TYPE_INT2: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 38, typreceive_oid: 2404, + typsend_oid: 2405, }), }, }; @@ -706,6 +750,7 @@ pub const TYPE_INT2_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -720,6 +765,7 @@ pub const TYPE_BPCHAR: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1044, typreceive_oid: 2430, + typsend_oid: 2431, }), }, }; @@ -736,6 +782,7 @@ pub const TYPE_CHAR_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -752,6 +799,7 @@ pub const TYPE_VARCHAR_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -768,6 +816,7 @@ pub const TYPE_BPCHAR_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -782,6 +831,7 @@ pub const TYPE_REGPROC: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 44, typreceive_oid: 2444, + typsend_oid: 2445, }), }, }; @@ -798,6 +848,7 @@ pub const TYPE_REGPROC_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -812,6 +863,7 @@ pub const TYPE_REGTYPE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2220, typreceive_oid: 2454, + typsend_oid: 2455, }), }, }; @@ -828,6 +880,7 @@ pub const TYPE_REGTYPE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -842,6 +895,7 @@ pub const TYPE_REGCLASS: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2218, typreceive_oid: 2452, + typsend_oid: 2453, }), }, }; @@ -858,6 +912,7 @@ pub const TYPE_REGCLASS_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -872,6 +927,7 @@ pub const TYPE_INT2_VECTOR: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 40, typreceive_oid: 2410, + typsend_oid: 2411, }), }, }; @@ -888,6 +944,7 @@ pub const TYPE_INT2_VECTOR_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -902,6 +959,7 @@ pub const TYPE_ANYCOMPATIBLE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 5086, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -916,6 +974,7 @@ pub const TYPE_ANYCOMPATIBLEARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 5088, typreceive_oid: 5090, + typsend_oid: 5091, }), }, }; @@ -930,6 +989,7 @@ pub const TYPE_ANYCOMPATIBLENONARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 5092, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -944,6 +1004,7 @@ pub const TYPE_ANYCOMPATIBLERANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 5094, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -960,6 +1021,7 @@ pub const TYPE_INT4_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -976,6 +1038,7 @@ pub const TYPE_INT4_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -992,6 +1055,7 @@ pub const TYPE_INT8_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -1008,6 +1072,7 @@ pub const TYPE_INT8_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1024,6 +1089,7 @@ pub const TYPE_DATE_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -1040,6 +1106,7 @@ pub const TYPE_DATE_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1056,6 +1123,7 @@ pub const TYPE_NUM_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -1072,6 +1140,7 @@ pub const TYPE_NUM_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1088,6 +1157,7 @@ pub const TYPE_TS_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -1104,6 +1174,7 @@ pub const TYPE_TS_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1120,6 +1191,7 @@ pub const TYPE_TSTZ_RANGE: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 3834, typreceive_oid: 3836, + typsend_oid: 3837, }), }, }; @@ -1136,6 +1208,7 @@ pub const TYPE_TSTZ_RANGE_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1150,6 +1223,7 @@ pub const TYPE_ACL_ITEM: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 1031, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -1166,6 +1240,7 @@ pub const TYPE_ACL_ITEM_ARRAY: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 750, typreceive_oid: 2400, + typsend_oid: 2401, }), }, }; @@ -1180,6 +1255,7 @@ pub const TYPE_INTERNAL: BuiltinType = BuiltinType { pg_metadata: Some(CatalogTypePgMetadata { typinput_oid: 2304, typreceive_oid: 0, + typsend_oid: 0, }), }, }; @@ -1492,17 +1568,22 @@ pub static PG_TYPE: LazyLock = LazyLock::new(|| BuiltinView { .with_column("typelem", SqlScalarType::Oid.nullable(false)) .with_column("typarray", SqlScalarType::Oid.nullable(false)) .with_column("typinput", SqlScalarType::RegProc.nullable(true)) - .with_column("typreceive", SqlScalarType::Oid.nullable(false)) + // The underlying `mz_internal.pg_type_all_databases` keeps `typreceive` + // as `oid` because it backs a builtin index, and resolving a `regproc` to + // a name reads `current_database()`, which is unmaterializable. + .with_column("typreceive", SqlScalarType::RegProc.nullable(false)) .with_column("typnotnull", SqlScalarType::Bool.nullable(false)) .with_column("typbasetype", SqlScalarType::Oid.nullable(false)) .with_column("typtypmod", SqlScalarType::Int32.nullable(false)) .with_column("typcollation", SqlScalarType::Oid.nullable(false)) .with_column("typdefault", SqlScalarType::String.nullable(true)) + .with_column("typsend", SqlScalarType::RegProc.nullable(false)) .finish(), column_comments: BTreeMap::new(), sql: "SELECT oid, typname, typnamespace, typowner, typlen, typtype, typcategory, typdelim, typrelid, typelem, - typarray, typinput, typreceive, typnotnull, typbasetype, typtypmod, typcollation, typdefault + typarray, typinput, typreceive::pg_catalog.regproc AS typreceive, typnotnull, typbasetype, + typtypmod, typcollation, typdefault, typsend FROM mz_internal.pg_type_all_databases WHERE database_name IS NULL OR database_name = pg_catalog.current_database();", access: vec![PUBLIC_SELECT], diff --git a/src/interchange/src/json.rs b/src/interchange/src/json.rs index 34a9c1affd438..36470fc7b02dc 100644 --- a/src/interchange/src/json.rs +++ b/src/interchange/src/json.rs @@ -117,6 +117,10 @@ impl ToJson for TypedDatum<'_> { SqlScalarType::Int32 => json!(datum.unwrap_int32()), SqlScalarType::Int64 => json!(datum.unwrap_int64()), SqlScalarType::UInt16 => json!(datum.unwrap_uint16()), + // NOTE: `regproc` stays a number here even though the pgwire text + // encoding resolves it to a function name. `build_row_schema_json` + // below declares these columns as 4-byte unsigned integers, so a + // name would contradict the schema this module publishes for them. SqlScalarType::UInt32 | SqlScalarType::Oid | SqlScalarType::RegClass diff --git a/src/pgrepr-consts/src/lib.rs b/src/pgrepr-consts/src/lib.rs index a9fa3f684635c..b69d78d247031 100644 --- a/src/pgrepr-consts/src/lib.rs +++ b/src/pgrepr-consts/src/lib.rs @@ -10,3 +10,4 @@ //! Constants used in the representation of and serialization for PostgreSQL datums. pub mod oid; +pub mod regproc; diff --git a/src/pgrepr-consts/src/regproc.rs b/src/pgrepr-consts/src/regproc.rs new file mode 100644 index 0000000000000..d3ad9c9c76674 --- /dev/null +++ b/src/pgrepr-consts/src/regproc.rs @@ -0,0 +1,730 @@ +// Copyright Materialize, Inc. and contributors. All rights reserved. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0. + +//! Text renderings of `regproc` values. +//! +//! PostgreSQL renders a `regproc` in text format through `regprocout`, which +//! resolves the OID to the function's name. Materialize has no +//! `CREATE FUNCTION`, so every function is a builtin and the OID to name +//! mapping is fixed when the binary is built. That is what makes a static +//! table viable: the text encoder has no catalog access and cannot gain any, +//! because the catalog crates depend on `mz-pgrepr` rather than the reverse, +//! and some encode paths run in `clusterd`, which has no catalog at all. +//! +//! [`NAMES`] is derived from Materialize's builtin function registry, reached +//! through `mz_catalog::builtin::BUILTINS::funcs()`, which is the authoritative +//! definition of every function OID. `mz_catalog::builtin`'s +//! `test_regproc_names_match_builtin_functions` recomputes the whole table from +//! that registry and fails when the two have drifted, so do not hand-edit +//! entries here. Add the function to the registry, then regenerate the table: +//! +//! ```shell +//! REWRITE=1 cargo test -p mz-catalog test_regproc_names_match_builtin_functions +//! ``` +//! +//! The OIDs are not invented by Materialize wherever PostgreSQL already assigns +//! one. `mz_sql::func`'s `PG_CATALOG_BUILTINS` records that its literal OIDs +//! were read out of a PostgreSQL 13 `pg_proc`, and points at +//! +//! for the same values. Everything else draws from [`crate::oid`], which carves +//! out `FIRST_UNPINNED_OID` upward for functions PostgreSQL ships without a +//! pinned OID, and `FIRST_MATERIALIZE_OID` upward for functions that exist only +//! in Materialize. + +/// Every builtin function OID paired with the text `regproc` renders it as, +/// sorted by OID. +/// +/// The name is schema-qualified whenever the bare name would not resolve back to +/// this OID, which is the rule `regprocout` applies. +/// +/// NOTE: The rendering assumes the default search path, which is all a static +/// table can do. A session with `mz_internal` or `mz_unsafe` on its +/// `search_path` makes those bare names resolvable, so the SQL cast from +/// `regproc` to `text` renders them bare where this table still qualifies them. +/// +/// NOTE: Reserved words are left unquoted here, so `left` renders as `left` +/// where PostgreSQL's `regprocout` renders `"left"`. This matches the SQL cast +/// from `regproc` to `text`, and the unquoted form still resolves because +/// `regprocin` accepts a reserved word either way. +pub const NAMES: &[(u32, &str)] = &[ + // BEGIN GENERATED + (34, "namein"), + (38, "int2in"), + (40, "int2vectorin"), + (42, "int4in"), + (44, "regprocin"), + (46, "textin"), + (89, "version"), + (200, "float4in"), + (214, "float8in"), + (376, "pg_catalog.string_to_array"), + (383, "array_cat"), + (384, "pg_catalog.array_to_string"), + (394, "pg_catalog.string_to_array"), + (395, "pg_catalog.array_to_string"), + (460, "int8in"), + (720, "pg_catalog.octet_length"), + (721, "get_byte"), + (723, "get_bit"), + (745, "current_user"), + (746, "session_user"), + (750, "array_in"), + (849, "position"), + (861, "current_database"), + (868, "strpos"), + (870, "pg_catalog.lower"), + (871, "pg_catalog.upper"), + (872, "initcap"), + (873, "pg_catalog.lpad"), + (875, "pg_catalog.ltrim"), + (876, "pg_catalog.rtrim"), + (877, "pg_catalog.substr"), + (878, "translate"), + (879, "pg_catalog.lpad"), + (881, "pg_catalog.ltrim"), + (882, "pg_catalog.rtrim"), + (883, "pg_catalog.substr"), + (884, "pg_catalog.btrim"), + (885, "pg_catalog.btrim"), + (936, "pg_catalog.substring"), + (937, "pg_catalog.substring"), + (938, "pg_catalog.generate_series"), + (939, "pg_catalog.generate_series"), + (940, "pg_catalog.mod"), + (941, "pg_catalog.mod"), + (947, "pg_catalog.mod"), + (1026, "pg_catalog.timezone"), + (1031, "aclitemin"), + (1044, "bpcharin"), + (1046, "varcharin"), + (1066, "pg_catalog.generate_series"), + (1067, "pg_catalog.generate_series"), + (1068, "pg_catalog.generate_series"), + (1069, "pg_catalog.generate_series"), + (1081, "format_type"), + (1084, "date_in"), + (1143, "time_in"), + (1150, "timestamptz_in"), + (1158, "to_timestamp"), + (1159, "pg_catalog.timezone"), + (1160, "interval_in"), + (1171, "pg_catalog.date_part"), + (1172, "pg_catalog.date_part"), + (1175, "justify_hours"), + (1178, "pg_catalog.date"), + (1192, "generate_subscripts"), + (1193, "pg_catalog.array_fill"), + (1194, "pg_catalog.log10"), + (1199, "pg_catalog.age"), + (1215, "obj_description"), + (1216, "col_description"), + (1217, "pg_catalog.date_trunc"), + (1218, "pg_catalog.date_trunc"), + (1242, "boolin"), + (1244, "byteain"), + (1245, "charin"), + (1268, "pg_catalog.parse_ident"), + (1269, "pg_column_size"), + (1282, "quote_ident"), + (1286, "pg_catalog.array_fill"), + (1295, "justify_days"), + (1299, "now"), + (1312, "timestamp_in"), + (1317, "pg_catalog.length"), + (1340, "pg_catalog.log"), + (1341, "pg_catalog.ln"), + (1342, "pg_catalog.round"), + (1343, "pg_catalog.trunc"), + (1344, "pg_catalog.sqrt"), + (1345, "cbrt"), + (1346, "pow"), + (1347, "pg_catalog.exp"), + (1365, "makeaclitem"), + (1368, "pg_catalog.power"), + (1374, "pg_catalog.octet_length"), + (1375, "pg_catalog.octet_length"), + (1381, "char_length"), + (1385, "pg_catalog.date_part"), + (1387, "pg_catalog.pg_get_constraintdef"), + (1394, "pg_catalog.abs"), + (1395, "pg_catalog.abs"), + (1396, "pg_catalog.abs"), + (1397, "pg_catalog.abs"), + (1398, "pg_catalog.abs"), + (1402, "current_schema"), + (1403, "current_schemas"), + (1481, "pg_catalog.log10"), + (1573, "pg_catalog.pg_get_ruledef"), + (1597, "pg_encoding_to_char"), + (1600, "asin"), + (1601, "acos"), + (1602, "atan"), + (1604, "sin"), + (1605, "cos"), + (1606, "tan"), + (1607, "cot"), + (1608, "degrees"), + (1609, "radians"), + (1619, "pg_typeof"), + (1620, "ascii"), + (1621, "chr"), + (1622, "repeat"), + (1637, "like_escape"), + (1640, "pg_catalog.pg_get_viewdef"), + (1641, "pg_catalog.pg_get_viewdef"), + (1642, "pg_get_userbyid"), + (1643, "pg_catalog.pg_get_indexdef"), + (1689, "aclexplode"), + (1701, "numeric_in"), + (1705, "pg_catalog.abs"), + (1707, "pg_catalog.round"), + (1708, "pg_catalog.round"), + (1710, "pg_catalog.trunc"), + (1711, "pg_catalog.ceil"), + (1712, "pg_catalog.floor"), + (1713, "pg_catalog.length"), + (1714, "convert_from"), + (1716, "pg_catalog.pg_get_expr"), + (1728, "pg_catalog.mod"), + (1730, "pg_catalog.sqrt"), + (1732, "pg_catalog.exp"), + (1734, "pg_catalog.ln"), + (1736, "pg_catalog.log"), + (1741, "pg_catalog.log"), + (1770, "pg_catalog.to_char"), + (1798, "oidin"), + (1810, "pg_catalog.bit_length"), + (1811, "pg_catalog.bit_length"), + (1922, "pg_catalog.has_table_privilege"), + (1923, "pg_catalog.has_table_privilege"), + (1924, "pg_catalog.has_table_privilege"), + (1925, "pg_catalog.has_table_privilege"), + (1926, "pg_catalog.has_table_privilege"), + (1927, "pg_catalog.has_table_privilege"), + (1928, "pg_stat_get_numscans"), + (1946, "encode"), + (1947, "decode"), + (2010, "pg_catalog.length"), + (2020, "pg_catalog.date_trunc"), + (2021, "pg_catalog.date_part"), + (2026, "pg_backend_pid"), + (2029, "pg_catalog.date"), + (2037, "pg_catalog.timezone"), + (2038, "pg_catalog.timezone"), + (2049, "pg_catalog.to_char"), + (2058, "pg_catalog.age"), + (2069, "pg_catalog.timezone"), + (2070, "pg_catalog.timezone"), + (2077, "pg_catalog.current_setting"), + (2079, "pg_table_is_visible"), + (2080, "pg_type_is_visible"), + (2081, "pg_function_is_visible"), + (2087, "replace"), + (2088, "split_part"), + (2091, "array_lower"), + (2092, "array_upper"), + (2100, "pg_catalog.avg"), + (2101, "pg_catalog.avg"), + (2102, "pg_catalog.avg"), + (2104, "pg_catalog.avg"), + (2105, "pg_catalog.avg"), + (2106, "pg_catalog.avg"), + (2107, "pg_catalog.sum"), + (2108, "pg_catalog.sum"), + (2109, "pg_catalog.sum"), + (2110, "pg_catalog.sum"), + (2111, "pg_catalog.sum"), + (2113, "pg_catalog.sum"), + (2114, "pg_catalog.sum"), + (2115, "pg_catalog.max"), + (2116, "pg_catalog.max"), + (2117, "pg_catalog.max"), + (2119, "pg_catalog.max"), + (2120, "pg_catalog.max"), + (2122, "pg_catalog.max"), + (2123, "pg_catalog.max"), + (2126, "pg_catalog.max"), + (2127, "pg_catalog.max"), + (2128, "pg_catalog.max"), + (2129, "pg_catalog.max"), + (2131, "pg_catalog.min"), + (2132, "pg_catalog.min"), + (2133, "pg_catalog.min"), + (2135, "pg_catalog.min"), + (2136, "pg_catalog.min"), + (2138, "pg_catalog.min"), + (2139, "pg_catalog.min"), + (2142, "pg_catalog.min"), + (2143, "pg_catalog.min"), + (2144, "pg_catalog.min"), + (2145, "pg_catalog.min"), + (2147, "pg_catalog.count"), + (2148, "pg_catalog.variance"), + (2149, "pg_catalog.variance"), + (2150, "pg_catalog.variance"), + (2151, "pg_catalog.variance"), + (2152, "pg_catalog.variance"), + (2154, "pg_catalog.stddev"), + (2155, "pg_catalog.stddev"), + (2156, "pg_catalog.stddev"), + (2157, "pg_catalog.stddev"), + (2158, "pg_catalog.stddev"), + (2167, "pg_catalog.ceiling"), + (2169, "pg_catalog.power"), + (2171, "pg_cancel_backend"), + (2176, "array_length"), + (2218, "regclassin"), + (2220, "regtypein"), + (2244, "pg_catalog.max"), + (2245, "pg_catalog.min"), + (2250, "pg_catalog.has_database_privilege"), + (2251, "pg_catalog.has_database_privilege"), + (2252, "pg_catalog.has_database_privilege"), + (2253, "pg_catalog.has_database_privilege"), + (2254, "pg_catalog.has_database_privilege"), + (2255, "pg_catalog.has_database_privilege"), + (2268, "pg_catalog.has_schema_privilege"), + (2269, "pg_catalog.has_schema_privilege"), + (2270, "pg_catalog.has_schema_privilege"), + (2271, "pg_catalog.has_schema_privilege"), + (2272, "pg_catalog.has_schema_privilege"), + (2273, "pg_catalog.has_schema_privilege"), + (2284, "pg_catalog.regexp_replace"), + (2285, "pg_catalog.regexp_replace"), + (2290, "record_in"), + (2294, "any_in"), + (2296, "anyarray_in"), + (2304, "internal_in"), + (2308, "pg_catalog.ceil"), + (2309, "pg_catalog.floor"), + (2311, "pg_catalog.md5"), + (2312, "anyelement_in"), + (2320, "pg_catalog.ceiling"), + (2321, "pg_catalog.md5"), + (2325, "pg_catalog.pg_relation_size"), + (2331, "mz_catalog.unnest"), + (2332, "pg_catalog.pg_relation_size"), + (2335, "pg_catalog.array_agg"), + (2400, "array_recv"), + (2402, "record_recv"), + (2404, "int2recv"), + (2406, "int4recv"), + (2408, "int8recv"), + (2410, "int2vectorrecv"), + (2412, "bytearecv"), + (2414, "textrecv"), + (2418, "oidrecv"), + (2422, "namerecv"), + (2424, "float4recv"), + (2426, "float8recv"), + (2430, "bpcharrecv"), + (2432, "varcharrecv"), + (2434, "charrecv"), + (2436, "boolrecv"), + (2444, "regprocrecv"), + (2452, "regclassrecv"), + (2454, "regtyperecv"), + (2460, "numeric_recv"), + (2462, "sinh"), + (2463, "cosh"), + (2464, "tanh"), + (2465, "asinh"), + (2466, "acosh"), + (2467, "atanh"), + (2468, "date_recv"), + (2470, "time_recv"), + (2474, "timestamp_recv"), + (2476, "timestamptz_recv"), + (2478, "interval_recv"), + (2502, "anyarray_recv"), + (2504, "pg_catalog.pg_get_ruledef"), + (2505, "pg_catalog.pg_get_viewdef"), + (2506, "pg_catalog.pg_get_viewdef"), + (2507, "pg_catalog.pg_get_indexdef"), + (2508, "pg_catalog.pg_get_constraintdef"), + (2509, "pg_catalog.pg_get_expr"), + (2517, "bool_and"), + (2518, "bool_or"), + (2560, "pg_postmaster_start_time"), + (2641, "pg_catalog.var_samp"), + (2642, "pg_catalog.var_samp"), + (2643, "pg_catalog.var_samp"), + (2644, "pg_catalog.var_samp"), + (2645, "pg_catalog.var_samp"), + (2705, "pg_catalog.pg_has_role"), + (2706, "pg_catalog.pg_has_role"), + (2707, "pg_catalog.pg_has_role"), + (2708, "pg_catalog.pg_has_role"), + (2709, "pg_catalog.pg_has_role"), + (2710, "pg_catalog.pg_has_role"), + (2711, "justify_interval"), + (2712, "pg_catalog.stddev_samp"), + (2713, "pg_catalog.stddev_samp"), + (2714, "pg_catalog.stddev_samp"), + (2715, "pg_catalog.stddev_samp"), + (2716, "pg_catalog.stddev_samp"), + (2718, "pg_catalog.var_pop"), + (2719, "pg_catalog.var_pop"), + (2720, "pg_catalog.var_pop"), + (2721, "pg_catalog.var_pop"), + (2722, "pg_catalog.var_pop"), + (2724, "pg_catalog.stddev_pop"), + (2725, "pg_catalog.stddev_pop"), + (2726, "pg_catalog.stddev_pop"), + (2727, "pg_catalog.stddev_pop"), + (2728, "pg_catalog.stddev_pop"), + (2763, "pg_catalog.regexp_matches"), + (2764, "pg_catalog.regexp_matches"), + (2765, "pg_catalog.regexp_split_to_table"), + (2766, "pg_catalog.regexp_split_to_table"), + (2767, "pg_catalog.regexp_split_to_array"), + (2768, "pg_catalog.regexp_split_to_array"), + (2777, "anynonarray_in"), + (2803, "pg_catalog.count"), + (2952, "uuid_in"), + (2961, "uuid_recv"), + (3058, "concat"), + (3059, "concat_ws"), + (3060, "left"), + (3061, "right"), + (3062, "reverse"), + (3100, "row_number"), + (3101, "rank"), + (3102, "dense_rank"), + (3106, "pg_catalog.lag"), + (3107, "pg_catalog.lag"), + (3108, "pg_catalog.lag"), + (3109, "pg_catalog.lead"), + (3110, "pg_catalog.lead"), + (3111, "pg_catalog.lead"), + (3112, "first_value"), + (3113, "last_value"), + (3138, "mz_catalog.has_type_privilege"), + (3139, "mz_catalog.has_type_privilege"), + (3140, "mz_catalog.has_type_privilege"), + (3141, "mz_catalog.has_type_privilege"), + (3142, "mz_catalog.has_type_privilege"), + (3143, "mz_catalog.has_type_privilege"), + (3159, "pg_catalog.pg_get_viewdef"), + (3166, "pg_size_pretty"), + (3167, "array_remove"), + (3207, "jsonb_array_length"), + (3208, "jsonb_each"), + (3210, "jsonb_typeof"), + (3219, "jsonb_array_elements"), + (3262, "jsonb_strip_nulls"), + (3267, "jsonb_agg"), + (3270, "jsonb_object_agg"), + (3271, "pg_catalog.jsonb_build_array"), + (3272, "pg_catalog.jsonb_build_array"), + (3273, "pg_catalog.jsonb_build_object"), + (3274, "pg_catalog.jsonb_build_object"), + (3277, "pg_catalog.array_position"), + (3278, "pg_catalog.array_position"), + (3294, "pg_catalog.current_setting"), + (3306, "jsonb_pretty"), + (3396, "pg_catalog.regexp_match"), + (3397, "pg_catalog.regexp_match"), + (3419, "sha224"), + (3420, "sha256"), + (3421, "sha384"), + (3422, "sha512"), + (3461, "make_timestamp"), + (3465, "jsonb_array_elements_text"), + (3538, "pg_catalog.string_agg"), + (3545, "pg_catalog.string_agg"), + (3696, "starts_with"), + (3778, "pg_tablespace_location"), + (3787, "to_jsonb"), + (3805, "jsonb_recv"), + (3806, "jsonb_in"), + (3810, "pg_is_in_recovery"), + (3832, "anyrange_in"), + (3834, "range_in"), + (3836, "range_recv"), + (3840, "pg_catalog.int4range"), + (3841, "pg_catalog.int4range"), + (3844, "pg_catalog.numrange"), + (3845, "pg_catalog.numrange"), + (3848, "pg_catalog.lower"), + (3849, "pg_catalog.upper"), + (3850, "isempty"), + (3851, "lower_inc"), + (3852, "upper_inc"), + (3853, "lower_inf"), + (3854, "upper_inf"), + (3931, "jsonb_object_keys"), + (3932, "jsonb_each_text"), + (3933, "pg_catalog.tsrange"), + (3934, "pg_catalog.tsrange"), + (3937, "pg_catalog.tstzrange"), + (3938, "pg_catalog.tstzrange"), + (3941, "pg_catalog.daterange"), + (3942, "pg_catalog.daterange"), + (3945, "pg_catalog.int8range"), + (3946, "pg_catalog.int8range"), + (4053, "pg_catalog.array_agg"), + (4350, "normalize"), + (5086, "anycompatible_in"), + (5088, "anycompatiblearray_in"), + (5090, "anycompatiblearray_recv"), + (5092, "anycompatiblenonarray_in"), + (5094, "anycompatiblerange_in"), + (6163, "bit_count"), + (6177, "pg_catalog.date_bin"), + (6178, "pg_catalog.date_bin"), + (6199, "pg_catalog.extract"), + (6200, "pg_catalog.extract"), + (6202, "pg_catalog.extract"), + (6203, "pg_catalog.extract"), + (6204, "pg_catalog.extract"), + (12000, "information_schema._pg_expandarray"), + (12001, "pg_catalog.digest"), + (12002, "pg_catalog.digest"), + (12003, "pg_catalog.hmac"), + (12004, "pg_catalog.hmac"), + (16386, "pg_catalog.ceil"), + (16387, "concat_agg"), + (16388, "csv_extract"), + (16389, "current_timestamp"), + (16390, "pg_catalog.floor"), + (16392, "list_append"), + (16393, "list_cat"), + (16394, "list_length_max"), + (16395, "list_length"), + (16396, "list_n_layers"), + (16397, "list_prepend"), + (16398, "pg_catalog.max"), + (16399, "pg_catalog.min"), + (16400, "mz_unsafe.mz_all"), + (16401, "mz_unsafe.mz_any"), + (16403, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16404, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16405, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16407, "mz_environment_id"), + (16409, "mz_logical_timestamp"), + (16410, "mz_internal.mz_render_typmod"), + (16411, "mz_version"), + (16412, "regexp_extract"), + (16413, "repeat_row"), + (16414, "pg_catalog.round"), + (16416, "mz_catalog.unnest"), + (16434, "mz_unsafe.mz_sleep"), + (16435, "mz_internal.mz_session_id"), + (16436, "mz_uptime"), + (16440, "mz_row_size"), + (16441, "pg_catalog.max"), + (16442, "pg_catalog.min"), + (16443, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16444, "list_agg"), + (16445, "mz_unsafe.mz_error_if_null"), + (16446, "pg_catalog.date_bin"), + (16447, "pg_catalog.date_bin"), + (16448, "list_remove"), + (16449, "pg_catalog.date_bin_hopping"), + (16450, "pg_catalog.date_bin_hopping"), + (16451, "pg_catalog.date_bin_hopping"), + (16452, "pg_catalog.date_bin_hopping"), + (16453, "mz_internal.mz_type_name"), + (16456, "map_length"), + (16457, "mz_unsafe.mz_panic"), + (16458, "mz_version_num"), + (16459, "pg_catalog.trunc"), + (16496, "pg_catalog.max"), + (16497, "pg_catalog.max"), + (16498, "pg_catalog.max"), + (16499, "pg_catalog.min"), + (16500, "pg_catalog.min"), + (16501, "pg_catalog.min"), + (16502, "pg_catalog.sum"), + (16503, "pg_catalog.sum"), + (16504, "pg_catalog.sum"), + (16505, "pg_catalog.avg"), + (16506, "pg_catalog.avg"), + (16507, "pg_catalog.avg"), + (16508, "pg_catalog.mod"), + (16509, "pg_catalog.mod"), + (16510, "pg_catalog.mod"), + (16511, "pg_catalog.stddev"), + (16512, "pg_catalog.stddev"), + (16513, "pg_catalog.stddev"), + (16514, "pg_catalog.stddev_pop"), + (16515, "pg_catalog.stddev_pop"), + (16516, "pg_catalog.stddev_pop"), + (16517, "pg_catalog.stddev_samp"), + (16518, "pg_catalog.stddev_samp"), + (16519, "pg_catalog.stddev_samp"), + (16520, "pg_catalog.variance"), + (16521, "pg_catalog.variance"), + (16522, "pg_catalog.variance"), + (16523, "pg_catalog.var_pop"), + (16524, "pg_catalog.var_pop"), + (16525, "pg_catalog.var_pop"), + (16526, "pg_catalog.var_samp"), + (16527, "pg_catalog.var_samp"), + (16528, "pg_catalog.var_samp"), + (16550, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16551, "mz_unsafe.mz_avg_promotion_internal_v1"), + (16560, "mz_now"), + (16561, "pg_catalog.max"), + (16562, "pg_catalog.min"), + (16563, "pg_catalog.date"), + (16564, "pg_catalog.ceiling"), + (16565, "uuid_generate_v5"), + (16570, "mz_internal.make_mz_aclitem"), + (16571, "mz_internal.mz_aclitem_grantor"), + (16572, "mz_internal.mz_aclitem_grantee"), + (16573, "mz_internal.mz_aclitem_privileges"), + (16574, "mz_internal.is_rbac_enabled"), + (16575, "mz_internal.mz_acl_item_contains_privilege"), + (16576, "mz_internal.mz_validate_privileges"), + (16577, "mz_internal.mz_role_oid"), + (16578, "mz_internal.mz_minimal_name_qualification"), + (16581, "mz_internal.mz_resolve_object_name"), + (16582, "mz_internal.mz_global_id_to_name"), + (16584, "pg_catalog.parse_ident"), + (16585, "mz_internal.mz_database_oid"), + (16586, "mz_internal.mz_schema_oid"), + (16587, "mz_catalog.has_cluster_privilege"), + (16588, "mz_catalog.has_cluster_privilege"), + (16589, "mz_catalog.has_cluster_privilege"), + (16591, "mz_catalog.has_connection_privilege"), + (16592, "mz_catalog.has_connection_privilege"), + (16593, "mz_catalog.has_connection_privilege"), + (16594, "mz_catalog.has_connection_privilege"), + (16595, "mz_catalog.has_connection_privilege"), + (16596, "mz_catalog.has_connection_privilege"), + (16597, "mz_catalog.has_secret_privilege"), + (16598, "mz_catalog.has_secret_privilege"), + (16599, "mz_catalog.has_secret_privilege"), + (16600, "mz_catalog.has_secret_privilege"), + (16601, "mz_catalog.has_secret_privilege"), + (16602, "mz_catalog.has_secret_privilege"), + (16603, "mz_internal.mz_normalize_object_name"), + (16604, "mz_catalog.datediff"), + (16605, "mz_catalog.datediff"), + (16606, "mz_catalog.datediff"), + (16607, "mz_catalog.datediff"), + (16608, "mz_catalog.has_system_privilege"), + (16609, "mz_catalog.has_system_privilege"), + (16610, "mz_catalog.has_system_privilege"), + (16611, "try_parse_monotonic_iso8601_timestamp"), + (16612, "mz_internal.mz_role_oid_memberships"), + (16613, "mz_internal.mz_validate_role_privilege"), + (16614, "mz_is_superuser"), + (16615, "mz_internal.mz_format_privileges"), + (16616, "mz_internal.aclitem_grantor"), + (16617, "mz_internal.aclitem_grantee"), + (16618, "mz_internal.aclitem_privileges"), + (16619, "mz_internal.mz_aclexplode"), + (16620, "mz_catalog.has_role"), + (16621, "mz_catalog.has_role"), + (16622, "mz_catalog.has_role"), + (16623, "mz_catalog.has_role"), + (16624, "mz_catalog.has_role"), + (16625, "mz_catalog.has_role"), + (16626, "mz_unsafe.mz_avg_promotion"), + (16627, "mz_unsafe.mz_avg_promotion"), + (16628, "mz_unsafe.mz_avg_promotion"), + (16629, "mz_unsafe.mz_avg_promotion"), + (16630, "mz_unsafe.mz_avg_promotion"), + (16631, "mz_unsafe.mz_avg_promotion"), + (16632, "mz_unsafe.mz_avg_promotion"), + (16633, "mz_unsafe.mz_avg_promotion"), + (16634, "mz_unsafe.mz_avg_promotion"), + (16635, "mz_catalog.avg_internal_v1"), + (16636, "mz_catalog.avg_internal_v1"), + (16637, "mz_catalog.avg_internal_v1"), + (16638, "mz_catalog.avg_internal_v1"), + (16639, "mz_catalog.avg_internal_v1"), + (16640, "mz_catalog.avg_internal_v1"), + (16641, "mz_catalog.avg_internal_v1"), + (16642, "mz_catalog.avg_internal_v1"), + (16643, "mz_catalog.avg_internal_v1"), + (16644, "mz_catalog.constant_time_eq"), + (16645, "mz_catalog.constant_time_eq"), + (16646, "timezone_offset"), + (16647, "mz_catalog.pretty_sql"), + (16648, "mz_catalog.pretty_sql"), + (16649, "mz_internal.mz_name_rank"), + (16650, "mz_internal.mz_connection_oid"), + (16651, "mz_internal.mz_secret_oid"), + (16652, "map_build"), + (16653, "map_agg"), + (16654, "mz_catalog.unnest"), + (16655, "mz_internal.mz_normalize_schema_name"), + (16957, "current_catalog"), + (16958, "current_role"), + (16959, "user"), + (16987, "mz_catalog.crc32"), + (16988, "mz_catalog.crc32"), + (16989, "mz_catalog.kafka_murmur2"), + (16990, "mz_catalog.kafka_murmur2"), + (16991, "mz_catalog.seahash"), + (16992, "mz_catalog.seahash"), + (17069, "mz_internal.parse_catalog_id"), + (17070, "mz_internal.parse_catalog_privileges"), + (17073, "mz_internal.parse_catalog_create_sql"), + (17074, "mz_internal.redact_sql"), + (17075, "repeat_row_non_negative"), + (17086, "mz_internal.mz_session_role_memberships"), + (17090, "mz_internal.parse_catalog_acl_mode"), + (17093, "mz_unsafe.generate_series_unoptimized"), + (17094, "mz_unsafe.generate_series_unoptimized"), + (17100, "mz_internal.parse_catalog_audit_log_details"), + (17105, "mz_internal.parse_postgres_source_details"), + (17106, "mz_internal.parse_kafka_source_details"), + (17111, "mz_internal.parse_source_export_details"), + (17112, "mz_internal.parse_connection_details"), + (17113, "mz_aws_account_id"), + (17114, "mz_aws_external_id_prefix"), + (17115, "mz_aws_connection_role_arn"), + // END GENERATED +]; + +/// Returns the text that `regproc` OID `oid` renders as, or `None` when no +/// builtin function has that OID. +/// +/// PostgreSQL renders an unmatched OID in decimal and OID 0 as `-`. Neither is +/// in [`NAMES`], so callers handle both. +pub fn name(oid: u32) -> Option<&'static str> { + NAMES + .binary_search_by_key(&oid, |(entry_oid, _)| *entry_oid) + .ok() + .map(|i| NAMES[i].1) +} + +/// Returns the OID whose [`NAMES`] rendering is `name`. +/// +/// `Err` distinguishes no match from an ambiguous one. A rendering is ambiguous +/// when several overloads share it, in which case PostgreSQL's `regprocin` +/// likewise refuses to resolve it. The scan is linear because reads of this +/// direction only happen while decoding text-format input, which is rare +/// compared to encoding. +pub fn oid(name: &str) -> Result { + let mut found = None; + for (entry_oid, entry_name) in NAMES { + if *entry_name == name { + if found.is_some() { + return Err(NameLookupError::Ambiguous); + } + found = Some(*entry_oid); + } + } + found.ok_or(NameLookupError::NotFound) +} + +/// Why [`oid`] could not resolve a name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NameLookupError { + /// No builtin function renders as the given name. + NotFound, + /// Several builtin functions render as the given name. + Ambiguous, +} diff --git a/src/pgrepr/src/lib.rs b/src/pgrepr/src/lib.rs index 9e540f8ac3e1c..4d0c58a3d6c2f 100644 --- a/src/pgrepr/src/lib.rs +++ b/src/pgrepr/src/lib.rs @@ -23,6 +23,7 @@ mod types; mod value; pub mod oid; +pub mod regproc; pub use types::{ ANYCOMPATIBLELIST, ANYCOMPATIBLEMAP, LIST, MAP, Type, TypeConversionError, TypeFromOidError, diff --git a/src/pgrepr/src/regproc.rs b/src/pgrepr/src/regproc.rs new file mode 100644 index 0000000000000..438dd72ddaa6e --- /dev/null +++ b/src/pgrepr/src/regproc.rs @@ -0,0 +1,63 @@ +// Copyright Materialize, Inc. and contributors. All rights reserved. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0. + +#![allow(missing_docs)] + +pub use mz_pgrepr_consts::regproc::*; + +#[cfg(test)] +mod tests { + use super::*; + + /// [`name`] binary searches [`NAMES`], so the table must be sorted by OID + /// and free of duplicate OIDs. + #[mz_ore::test] + fn names_is_sorted_by_oid() { + for window in NAMES.windows(2) { + assert!( + window[0].0 < window[1].0, + "NAMES is not sorted by OID: {} precedes {}", + window[0].0, + window[1].0, + ); + } + } + + #[mz_ore::test] + // `oid` scans the table, so covering every entry is quadratic and far too + // slow interpreted. + #[cfg_attr(miri, ignore)] + fn lookups_round_trip() { + for (entry_oid, entry_name) in NAMES { + assert_eq!(name(*entry_oid), Some(*entry_name)); + // Overloads sharing a rendering can only resolve back to one OID, + // so ambiguity is the expected outcome for those. + match oid(entry_name) { + Ok(resolved) => assert_eq!(resolved, *entry_oid), + Err(err) => assert_eq!(err, NameLookupError::Ambiguous), + } + } + assert_eq!(name(0), None); + assert_eq!(oid("no_such_function"), Err(NameLookupError::NotFound)); + } + + /// Spot-checks the `pg_type` I/O function renderings, so a regeneration that + /// dropped them fails loudly. + #[mz_ore::test] + fn type_io_functions_resolve() { + for (func_oid, expected) in [ + (1242, "boolin"), + (2436, "boolrecv"), + (2400, "array_recv"), + (2414, "textrecv"), + ] { + assert_eq!(name(func_oid), Some(expected), "regproc {func_oid}"); + } + } +} diff --git a/src/pgrepr/src/value.rs b/src/pgrepr/src/value.rs index aaf01b100dfe0..bbb20f23f8d0f 100644 --- a/src/pgrepr/src/value.rs +++ b/src/pgrepr/src/value.rs @@ -35,7 +35,7 @@ use uuid::Uuid; use crate::types::{NumericConstraints, UINT2, UINT4, UINT8}; use crate::value::error::{IntoDatumError, NulCharacterError}; -use crate::{Interval, Jsonb, Numeric, Type, UInt2, UInt4, UInt8}; +use crate::{Interval, Jsonb, Numeric, Type, UInt2, UInt4, UInt8, regproc}; pub mod error; pub mod interval; @@ -92,6 +92,12 @@ pub enum Value { Numeric(Numeric), /// An object identifier. Oid(u32), + /// An object identifier naming a function. + /// + /// Kept distinct from [`Value::Oid`] because the text encoding resolves the + /// OID to the function's name, matching PostgreSQL's `regprocout`. The + /// binary encoding is identical to an `oid`, as it is in PostgreSQL. + RegProc(u32), /// A sequence of heterogeneous values. Record(Vec>), /// A time. @@ -141,8 +147,12 @@ impl Value { (Datum::UInt8(c), SqlScalarType::PgLegacyChar) => Some(Value::Char(c)), (Datum::UInt16(u), SqlScalarType::UInt16) => Some(Value::UInt2(UInt2(u))), (Datum::UInt32(oid), SqlScalarType::Oid) => Some(Value::Oid(oid)), + (Datum::UInt32(oid), SqlScalarType::RegProc) => Some(Value::RegProc(oid)), + // NOTE: `regclass` and `regtype` collapse into `Value::Oid`, so they + // render as digits where PostgreSQL renders a name. Unlike `regproc` + // they can name objects a user created, which no static table can + // know. Resolution for those two lives in the SQL cast to `text`. (Datum::UInt32(oid), SqlScalarType::RegClass) => Some(Value::Oid(oid)), - (Datum::UInt32(oid), SqlScalarType::RegProc) => Some(Value::Oid(oid)), (Datum::UInt32(oid), SqlScalarType::RegType) => Some(Value::Oid(oid)), (Datum::UInt32(u), SqlScalarType::UInt32) => Some(Value::UInt4(UInt4(u))), (Datum::UInt64(u), SqlScalarType::UInt64) => Some(Value::UInt8(UInt8(u))), @@ -305,7 +315,7 @@ impl Value { }) })? } - Value::Oid(oid) => Datum::UInt32(oid), + Value::Oid(oid) | Value::RegProc(oid) => Datum::UInt32(oid), Value::Record(_) => { // This situation is handled gracefully by Value::decode; if we // wind up here it's a programming error. @@ -422,6 +432,14 @@ impl Value { }) .expect("provided closure never fails"), Value::Oid(oid) => strconv::format_uint32(buf, *oid), + // Mirrors PostgreSQL's `regprocout`. + Value::RegProc(oid) => match *oid { + 0 => strconv::format_string(buf, REGPROC_NULL), + oid => match regproc::name(oid) { + Some(name) => strconv::format_string(buf, name), + None => strconv::format_uint32(buf, oid), + }, + }, Value::Record(elems) => strconv::format_record(buf, elems, |buf, elem| match elem { None => Ok::<_, ()>(buf.write_null()), Some(elem) => Ok(elem.encode_text(buf.nonnull_buffer(), settings)), @@ -544,7 +562,9 @@ impl Value { Err("binary encoding of map types is not implemented".into()) } Value::Name(s) => s.to_sql(&PgType::NAME, buf), - Value::Oid(i) => i.to_sql(&PgType::OID, buf), + // PostgreSQL's `regprocsend` is `int4send`, so binary is identical to + // an `oid`. + Value::Oid(i) | Value::RegProc(i) => i.to_sql(&PgType::OID, buf), Value::Record(fields) => { let nfields = pg_len("record field length", fields.len())?; buf.put_i32(nfields); @@ -734,9 +754,8 @@ impl Value { strconv::parse_numeric(s)?, constraints.as_ref(), )?)), - Type::Oid | Type::RegClass | Type::RegProc | Type::RegType => { - Value::Oid(strconv::parse_oid(s)?) - } + Type::Oid | Type::RegClass | Type::RegType => Value::Oid(strconv::parse_oid(s)?), + Type::RegProc => Value::RegProc(parse_regproc(s)?), Type::Record(_) => { return Err("input of anonymous composite types is not implemented".into()); } @@ -844,9 +863,10 @@ impl Value { strconv::parse_numeric(s)?, constraints.as_ref(), )?)), - Type::Oid | Type::RegClass | Type::RegProc | Type::RegType => { + Type::Oid | Type::RegClass | Type::RegType => { packer.push(Datum::UInt32(strconv::parse_oid(s)?)) } + Type::RegProc => packer.push(Datum::UInt32(parse_regproc(s)?)), Type::Record(_) => { return Err("input of anonymous composite types is not implemented".into()); } @@ -933,9 +953,10 @@ impl Value { constraints.as_ref(), )?))) } - Type::Oid | Type::RegClass | Type::RegProc | Type::RegType => { + Type::Oid | Type::RegClass | Type::RegType => { u32::from_sql(ty.inner(), raw).map(Value::Oid) } + Type::RegProc => u32::from_sql(ty.inner(), raw).map(Value::RegProc), Type::Record(_) => Err("input of anonymous composite types is not implemented".into()), Type::Text => decode_binary_string(ty, raw).map(Value::Text), Type::BpChar { .. } => decode_binary_string(ty, raw).map(Value::BpChar), @@ -986,6 +1007,44 @@ impl Value { } } +/// The text PostgreSQL's `regprocout` emits for OID 0, and that `regprocin` +/// reads back as OID 0. +const REGPROC_NULL: &str = "-"; + +/// Parses the text format of a `regproc`, matching PostgreSQL's `regprocin`. +/// +/// Accepting names is what keeps a `COPY ... TO` rendering loadable by +/// `COPY ... FROM`, since the text encoding emits names. An overloaded name +/// renders schema-qualified and names every one of its impls, so it does not +/// round trip, which is true of PostgreSQL as well. +fn parse_regproc(s: &str) -> Result> { + if s == REGPROC_NULL { + return Ok(0); + } + match regproc::oid(s) { + Ok(oid) => Ok(oid), + // PostgreSQL refuses the same input rather than picking an overload. + Err(regproc::NameLookupError::Ambiguous) => { + Err(format!("more than one function named \"{}\"", s).into()) + } + Err(regproc::NameLookupError::NotFound) => match strconv::parse_oid(s) { + Ok(oid) => Ok(oid), + // Number-shaped input keeps its numeric diagnosis, which is what + // reports an out of range OID. Anything else was meant as a name, so + // report a missing function, matching PostgreSQL. + Err(err) if is_number_shaped(s) => Err(err.into()), + Err(_) => Err(format!("function \"{}\" does not exist", s).into()), + }, + } +} + +/// Whether `s` is shaped like a decimal number, ignoring whether it is in range. +fn is_number_shaped(s: &str) -> bool { + let digits = s.trim(); + let digits = digits.strip_prefix(['+', '-']).unwrap_or(digits); + !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) +} + /// Session settings that affect how values are encoded as text. /// /// PostgreSQL's text output for some types depends on session state. Encoders @@ -1158,6 +1217,71 @@ mod tests { }); } + /// A `regproc` renders as the function's name in text format and as a bare + /// OID in binary, which is what PostgreSQL's `regprocout` and + /// `regprocsend` do. + #[mz_ore::test] + fn regproc_text_encoding_resolves_names() { + // 1242 is `boolin`, uniquely named. 1398 is one of six `abs` overloads, + // so PostgreSQL qualifies it. 99999 names no function. The qualified + // overload does not round trip, because its rendering names all six. + for (oid, expected, round_trips) in [ + (0, "-", true), + (1242, "boolin", true), + (1398, "pg_catalog.abs", false), + (99999, "99999", true), + (u32::MAX, "4294967295", true), + ] { + let mut buf = BytesMut::new(); + Value::from_datum(Datum::UInt32(oid), &SqlScalarType::RegProc) + .expect("a non-null datum encodes") + .encode_text(&mut buf, TextEncodeSettings::STABLE); + assert_eq!(str::from_utf8(&buf).unwrap(), expected, "regproc {oid}"); + + if round_trips { + let Value::RegProc(decoded) = Value::decode_text(&Type::RegProc, &buf).unwrap() + else { + panic!("decoding a regproc must yield Value::RegProc"); + }; + assert_eq!(decoded, oid, "text rendering {expected} did not round trip"); + } + + let mut binary = BytesMut::new(); + Value::RegProc(oid) + .encode_binary(&Type::RegProc, &mut binary) + .expect("regproc has a binary encoding"); + assert_eq!(&binary[..], &oid.to_be_bytes()[..], "regproc {oid} binary"); + } + } + + /// An overloaded name is an error rather than an arbitrary choice, which is + /// what PostgreSQL does too. + #[mz_ore::test] + fn regproc_text_decoding_rejects_ambiguous_names() { + assert_eq!( + Value::decode_text(&Type::RegProc, b"pg_catalog.abs") + .map(|_| ()) + .map_err(|e| e.to_string()) + .unwrap_err(), + "more than one function named \"pg_catalog.abs\"", + ); + assert_eq!( + Value::decode_text(&Type::RegProc, b"no_such_function") + .map(|_| ()) + .map_err(|e| e.to_string()) + .unwrap_err(), + "function \"no_such_function\" does not exist", + ); + let out_of_range = Value::decode_text(&Type::RegProc, b"99999999999999") + .map(|_| ()) + .map_err(|e| e.to_string()) + .unwrap_err(); + assert!( + out_of_range.starts_with("invalid input syntax for type oid"), + "unexpected error for an out of range OID: {out_of_range}", + ); + } + /// [`format_float_limited`] must match C's `%.*g`, which PostgreSQL's /// `float4out`/`float8out` use when `extra_float_digits` is zero or /// negative. diff --git a/src/sql/src/catalog.rs b/src/sql/src/catalog.rs index 18cc768faea9e..705a2f98816ec 100644 --- a/src/sql/src/catalog.rs +++ b/src/sql/src/catalog.rs @@ -1083,6 +1083,8 @@ pub struct CatalogTypePgMetadata { pub typinput_oid: u32, /// The OID of the `typreceive` function in PostgreSQL. pub typreceive_oid: u32, + /// The OID of the `typsend` function in PostgreSQL. + pub typsend_oid: u32, } /// Represents a reference to type in the catalog diff --git a/src/sql/src/plan/typeconv.rs b/src/sql/src/plan/typeconv.rs index 6b69af3b8f78d..6f09ee1c5cec7 100644 --- a/src/sql/src/plan/typeconv.rs +++ b/src/sql/src/plan/typeconv.rs @@ -123,6 +123,8 @@ impl From<[UnaryFunc; N]> for CastTemplate { /// A reg* type represents a specific type of object by oid. /// /// Casting from a string to a reg*: +/// - Accepts `-`, the absent reference, as OID 0 in all cases, matching +/// PostgreSQL's `parseDashOrOid`. /// - Accepts a string that looks like an OID and converts the value to the /// specified reg* type. This is available in all cases except explicitly /// casting text values to regclass (e.g. `SELECT '2'::text::regclass`) @@ -138,6 +140,9 @@ const STRING_REG_CAST_TEMPLATE: &str = " (SELECT CASE WHEN $1 IS NULL THEN NULL +-- Handle the absent reference, which the reg* text output spells `-`. PostgreSQL's +-- `parseDashOrOid` accepts it for every reg* type, including text to regclass. + WHEN $1 = '-' THEN 0::pg_catalog.oid::pg_catalog.{0} -- Handle OID-like input, if available via {2} WHEN {2} AND pg_catalog.substring($1, 1, 1) BETWEEN '0' AND '9' THEN $1::pg_catalog.oid::pg_catalog.{0} @@ -203,9 +208,16 @@ static STRING_TO_REGTYPE: LazyLock = LazyLock::new(|| { .to_string() }); +// OID 0 renders as `-` to match PostgreSQL's `regprocout`, `regclassout` and +// `regtypeout`, which all spell the absent reference that way. A nonzero OID +// that names nothing still renders as its digits, also as PostgreSQL does. const REG_STRING_CAST_TEMPLATE: &str = "( SELECT - COALESCE(mz_internal.mz_global_id_to_name(o.id), CAST($1 AS pg_catalog.oid)::pg_catalog.text) + CASE + WHEN CAST($1 AS pg_catalog.oid) = 0::pg_catalog.oid THEN '-' + ELSE + COALESCE(mz_internal.mz_global_id_to_name(o.id), CAST($1 AS pg_catalog.oid)::pg_catalog.text) + END AS text FROM ( diff --git a/test/adbc/mzcompose b/test/adbc/mzcompose new file mode 100755 index 0000000000000..1f866645dabc8 --- /dev/null +++ b/test/adbc/mzcompose @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Copyright Materialize, Inc. and contributors. All rights reserved. +# +# Use of this software is governed by the Business Source License +# included in the LICENSE file at the root of this repository. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0. +# +# mzcompose — runs Docker Compose with Materialize customizations. + +exec "$(dirname "$0")"/../../bin/pyactivate -m materialize.cli.mzcompose "$@" diff --git a/test/adbc/mzcompose.py b/test/adbc/mzcompose.py new file mode 100644 index 0000000000000..4a63167ef5255 --- /dev/null +++ b/test/adbc/mzcompose.py @@ -0,0 +1,38 @@ +# Copyright Materialize, Inc. and contributors. All rights reserved. +# +# Use of this software is governed by the Business Source License +# included in the LICENSE file at the root of this repository. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0. + +""" +Tests the Apache Arrow ADBC PostgreSQL driver against Materialize, and hands +the resulting Arrow tables to DuckDB. +""" + +from materialize.mzcompose.composition import Composition +from materialize.mzcompose.service import Service +from materialize.mzcompose.services.materialized import Materialized + +SERVICES = [ + Materialized(), + Service( + name="adbc", + config={ + "image": "python:3.14.7-trixie", + "volumes": [ + "../../:/workdir", + ], + "environment": [ + "PGHOST=materialized", + ], + }, + ), +] + + +def workflow_default(c: Composition) -> None: + c.up("materialized") + c.run("adbc", "/workdir/test/adbc/test.sh") diff --git a/test/adbc/requirements.txt b/test/adbc/requirements.txt new file mode 100644 index 0000000000000..0d25792f3e17d --- /dev/null +++ b/test/adbc/requirements.txt @@ -0,0 +1,4 @@ +adbc-driver-manager==1.12.0 +adbc-driver-postgresql==1.12.0 +duckdb==1.5.5 +pyarrow==25.0.1 diff --git a/test/adbc/smoketest.py b/test/adbc/smoketest.py new file mode 100644 index 0000000000000..2b0e5d22da46d --- /dev/null +++ b/test/adbc/smoketest.py @@ -0,0 +1,187 @@ +# Copyright Materialize, Inc. and contributors. All rights reserved. +# +# Use of this software is governed by the Business Source License +# included in the LICENSE file at the root of this repository. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0. + +"""Apache Arrow ADBC PostgreSQL driver tests. + +The driver keys its Arrow type mapping off the *name* of each type's binary +receive function, which it reads from `pg_catalog.pg_type.typreceive`. A type it +cannot resolve degrades to `arrow.opaque` over binary rather than erroring, so +asserting the exact Arrow type per column is what catches a regression here. +""" + +import unittest + +# Pinned in `test/adbc/requirements.txt` and installed only inside this +# composition's test container, so they are absent from the repo-wide virtualenv +# that pyright resolves against. +import adbc_driver_postgresql.dbapi # pyright: ignore[reportMissingImports] +import duckdb # pyright: ignore[reportMissingImports] +import pyarrow as pa + +MATERIALIZED_URL = "postgresql://materialize@materialized:6875/materialize" + +# Every value is non-null so that a broken binary decoder shows up as a wrong +# value rather than as a column of nulls. +SCALAR_QUERY = """ +SELECT + true::bool AS c_bool, + 1::int2 AS c_int2, + 2::int4 AS c_int4, + 3::int8 AS c_int8, + 1.5::float4 AS c_float4, + 2.5::float8 AS c_float8, + 123.45::numeric AS c_numeric, + 'abc'::text AS c_text, + 'def'::varchar AS c_varchar, + '\\x0102'::bytea AS c_bytea, + '2024-01-01'::date AS c_date, + '12:34:56'::time AS c_time, + '2024-01-01 12:34:56'::timestamp AS c_timestamp, + '2024-01-01 12:34:56+00'::timestamptz AS c_timestamptz, + '1 day'::interval AS c_interval, + '00000000-0000-0000-0000-000000000001'::uuid AS c_uuid, + '{"a": 1}'::jsonb AS c_jsonb +""" + +# `numeric` and `uuid` have no native Arrow type the driver maps them onto, so it +# wraps them in an `arrow.opaque` extension type over string or binary storage. +EXPECTED_SCALAR_TYPES = { + "c_bool": pa.bool_(), + "c_int2": pa.int16(), + "c_int4": pa.int32(), + "c_int8": pa.int64(), + "c_float4": pa.float32(), + "c_float8": pa.float64(), + "c_numeric": pa.opaque(pa.string(), "numeric", "PostgreSQL"), + "c_text": pa.string(), + "c_varchar": pa.string(), + "c_bytea": pa.binary(), + "c_date": pa.date32(), + "c_time": pa.time64("us"), + "c_timestamp": pa.timestamp("us"), + "c_timestamptz": pa.timestamp("us", tz="UTC"), + "c_interval": pa.month_day_nano_interval(), + "c_uuid": pa.opaque(pa.binary(), "uuid", "PostgreSQL"), + "c_jsonb": pa.json_(pa.string()), +} + + +def query_arrow(sql: str) -> pa.Table: + with adbc_driver_postgresql.dbapi.connect(MATERIALIZED_URL) as conn: + with conn.cursor() as cur: + cur.execute(sql) + return cur.fetch_arrow_table() + + +class SmokeTest(unittest.TestCase): + def test_connect(self) -> None: + """Connecting runs the driver's type resolver, which reads the binary + send and receive functions of every type out of `pg_catalog.pg_type`. A + column missing there fails every connection before any user query, so + connecting at all covers that catalog contract.""" + with adbc_driver_postgresql.dbapi.connect(MATERIALIZED_URL) as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + table = cur.fetch_arrow_table() + self.assertEqual(table.num_rows, 1) + self.assertEqual(table.num_columns, 1) + + def test_scalar_types_resolve(self) -> None: + """Each scalar type must arrive as its own Arrow type.""" + table = query_arrow(SCALAR_QUERY) + self.assertEqual(table.num_rows, 1) + self.assertEqual( + sorted(table.schema.names), sorted(EXPECTED_SCALAR_TYPES.keys()) + ) + + for name, expected in EXPECTED_SCALAR_TYPES.items(): + actual = table.schema.field(name).type + self.assertEqual( + actual, + expected, + f"column {name} resolved to Arrow type {actual}, expected {expected}", + ) + + # `bytea` is the only column that is legitimately Arrow binary. Any other + # column landing there fell back to untyped bytes. + for field in table.schema: + if field.type == pa.binary(): + self.assertEqual( + field.name, + "c_bytea", + f"column {field.name} fell back to opaque binary", + ) + + row = table.to_pylist()[0] + self.assertEqual(row["c_bool"], True) + self.assertEqual(row["c_int4"], 2) + self.assertEqual(row["c_text"], "abc") + self.assertEqual(row["c_bytea"], b"\x01\x02") + + def test_array_type(self) -> None: + """An array must arrive as an Arrow list, not as an opaque blob.""" + table = query_arrow("SELECT ARRAY[1, 2, 3]::int4[] AS a") + actual = table.schema.field("a").type + self.assertEqual( + actual, + pa.list_(pa.int32()), + f"column a resolved to Arrow type {actual}, expected a list of int32", + ) + self.assertEqual(table.to_pylist(), [{"a": [1, 2, 3]}]) + + def test_null_handling(self) -> None: + """Nulls round-trip while keeping the column's resolved Arrow type.""" + table = query_arrow(""" + SELECT + NULL::int4 AS c_int4, + NULL::text AS c_text, + NULL::timestamptz AS c_timestamptz + """) + self.assertEqual(table.schema.field("c_int4").type, pa.int32()) + self.assertEqual(table.schema.field("c_text").type, pa.string()) + self.assertEqual( + table.schema.field("c_timestamptz").type, pa.timestamp("us", tz="UTC") + ) + self.assertEqual( + table.to_pylist(), + [{"c_int4": None, "c_text": None, "c_timestamptz": None}], + ) + + def test_duckdb_handoff(self) -> None: + """DuckDB reads the Arrow table Materialize produced directly, with no + intermediate file or object store hop.""" + table = query_arrow(""" + SELECT n, n * 2 AS doubled, 'row' || n::text AS label + FROM generate_series(1, 100) AS g(n) + """) + + con = duckdb.connect() + try: + con.register("t", table) + self.assertEqual( + con.execute("SELECT count(*), sum(n), sum(doubled) FROM t").fetchall(), + [(100, 5050, 10100)], + ) + self.assertEqual( + con.execute("SELECT sum(doubled) FROM t WHERE n > 50").fetchall(), + [(7550,)], + ) + self.assertEqual( + con.execute("SELECT label FROM t WHERE n = 7").fetchall(), + [("row7",)], + ) + finally: + con.close() + + def test_fetch_many_rows(self) -> None: + """The driver fetches results as a binary COPY stream. This covers a + result large enough to span multiple reads of that stream.""" + table = query_arrow("SELECT n FROM generate_series(1, 5000) AS g(n)") + self.assertEqual(table.num_rows, 5000) + self.assertEqual(table.num_columns, 1) diff --git a/test/adbc/test.sh b/test/adbc/test.sh new file mode 100755 index 0000000000000..8ca39aa74dcf8 --- /dev/null +++ b/test/adbc/test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Copyright Materialize, Inc. and contributors. All rights reserved. +# +# Use of this software is governed by the Business Source License +# included in the LICENSE file at the root of this repository. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0. +# +# test.sh — run Apache Arrow ADBC driver tests. + +set -euo pipefail + +cd "$(dirname "$0")/../.." + +. misc/shlib/shlib.bash + +cd test/adbc + +pip install -r requirements.txt + +python -m unittest smoketest diff --git a/test/lang/python/mzcompose.py b/test/lang/python/mzcompose.py index a65335fdf37fd..4a4b130079db8 100644 --- a/test/lang/python/mzcompose.py +++ b/test/lang/python/mzcompose.py @@ -20,7 +20,7 @@ Service( name="python", config={ - "image": "python:3.14.1-trixie", + "image": "python:3.14.7-trixie", "volumes": [ "../../../:/workdir", ], diff --git a/test/pgtest-mz/datums.pt b/test/pgtest-mz/datums.pt index 7a15d4a60a60d..50eac5dd24f35 100644 --- a/test/pgtest-mz/datums.pt +++ b/test/pgtest-mz/datums.pt @@ -23,7 +23,7 @@ ReadyForQuery {"status":"I"} CommandComplete {"tag":"CREATE TABLE"} ReadyForQuery {"status":"I"} RowDescription {"fields":[{"name":"rowid"},{"name":"_bool"},{"name":"_int16"},{"name":"_int32"},{"name":"_int64"},{"name":"_uint16"},{"name":"_uint32"},{"name":"_uint64"},{"name":"_float32"},{"name":"_float64"},{"name":"_numeric"},{"name":"_date"},{"name":"_time"},{"name":"_timestamp"},{"name":"_timestamp_"},{"name":"_timestamp__"},{"name":"_timestamptz"},{"name":"_timestamptz_"},{"name":"_timestamptz__"},{"name":"_interval"},{"name":"_pglegacychar"},{"name":"_bytes"},{"name":"_string"},{"name":"_char"},{"name":"_varchar"},{"name":"_jsonb"},{"name":"_uuid"},{"name":"_oid"},{"name":"_regproc"},{"name":"_regtype"},{"name":"_regclass"},{"name":"_int2vector"},{"name":"_mztimestamp"},{"name":"_mzaclitem"}]} -DataRow {"fields":["1","t","0","0","0","0","0","0","0","0","0","2000-01-01","00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00+00","1970-01-01 00:00:00+00","1970-01-01 00:00:00+00","00:00:00","\u0000","\\x",""," ","","true","00000000-0000-0000-0000-000000000000","0","0","0","0","NULL","0","=/p"]} +DataRow {"fields":["1","t","0","0","0","0","0","0","0","0","0","2000-01-01","00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00","1970-01-01 00:00:00+00","1970-01-01 00:00:00+00","1970-01-01 00:00:00+00","00:00:00","\u0000","\\x",""," ","","true","00000000-0000-0000-0000-000000000000","0","-","0","0","NULL","0","=/p"]} DataRow {"fields":["2","f","1","1","1","1","1","1","1","1","1","4714-11-24 BC","23:59:59.999999","4714-12-31 00:00:00 BC","4714-12-31 00:00:00 BC","4714-12-31 00:00:00 BC","4714-12-31 00:00:00+00 BC","4714-12-31 00:00:00+00 BC","4714-12-31 00:00:00+00 BC","1 mon 1 day 00:00:00.000001","[255]","\\x00"," ","'"," ","false","ffffffff-ffff-ffff-ffff-ffffffffffff","4294967295","4294967295","4294967295","4294967295","NULL","18446744073709551615","=arwdUCRBNP/p"]} DataRow {"fields":["3","NULL","-1","-1","-1","65535","4294967295","18446744073709551615","-1","-1","-1","262142-12-31","23:59:60","262142-12-31 23:59:59","262142-12-31 23:59:59","262142-12-31 23:59:59","262142-12-31 23:59:59+00","262142-12-31 23:59:59+00","262142-12-31 23:59:59+00","-1 mons -1 days -00:00:00.000001","NULL","\\xff","'","\"","'","null","NULL","NULL","NULL","NULL","NULL","NULL","NULL","u42=/p"]} DataRow {"fields":["4","NULL","-32768","-2147483648","-9223372036854775808","255","32767","2147483647","-3.4028235e+38","-1.7976931348623157e+308","-Infinity","NULL","NULL","1970-01-01 00:00:00.123457","1970-01-01 00:00:00.123457","1970-01-01 00:00:00.123457","1970-01-01 00:00:00.123457+00","1970-01-01 00:00:00.123457+00","1970-01-01 00:00:00.123457+00","1 mon","NULL","NULL","\"",".","\"","\"\"","NULL","NULL","NULL","NULL","NULL","NULL","NULL","u42=arwdUCRBNP/p"]} diff --git a/test/sqllogictest/catalog_server_explain.slt b/test/sqllogictest/catalog_server_explain.slt index b06437fb06918..40435e9417dbe 100644 --- a/test/sqllogictest/catalog_server_explain.slt +++ b/test/sqllogictest/catalog_server_explain.slt @@ -4323,8 +4323,8 @@ mz_internal.pg_type_all_databases: →Arrange (#0{id}) (#2{schema_id}) →Union →Map/Filter/Project - Project: #0..=#11 - Map: null, null, null + Project: #0..=#12 + Map: null, null, null, null →Consolidating Union →Negate Diffs →Differential Join %1[#0] » %0:l0[#0{id}] @@ -4338,7 +4338,7 @@ mz_internal.pg_type_all_databases: Project: #1, #2, #0, #3..=#8 →Arranged mz_catalog.mz_types →Fused with Child Map/Filter/Project - Project: #0..=#8, #0, #9, #10 + Project: #0..=#8, #0, #9..=#11 →Read l1 →Arrange (#0{id}) →Fused with Parent Map/Filter/Project @@ -4358,10 +4358,10 @@ mz_internal.pg_type_all_databases: →Read mz_catalog.mz_map_types →Read mz_catalog.mz_pseudo_types cte l3 = - →Differential Join %1:mz_databases[#0] » %0:l2[#13{database_id}] - →Arrange (#13{database_id}) + →Differential Join %1:mz_databases[#0] » %0:l2[#14{database_id}] + →Arrange (#14{database_id}) →Fused with Child Map/Filter/Project - Filter: (#13{database_id}) IS NOT NULL + Filter: (#14{database_id}) IS NOT NULL →Read l2 →Arrange (#0) →Fused with Parent Map/Filter/Project @@ -4372,18 +4372,18 @@ mz_internal.pg_type_all_databases: →Arrange (#5{owner_id}) →Union →Map/Filter/Project - Project: #0..=#24 + Project: #0..=#25 Map: null, null, null, null, null →Consolidating Union →Negate Diffs →Fused with Child Map/Filter/Project - Project: #0..=#11, #2, #12..=#16, #0, #17 + Project: #0..=#12, #2, #13..=#17, #0, #18 →Read l3 →Fused with Child Map/Filter/Project - Project: #0..=#11, #2, #12..=#16, #0, #17 + Project: #0..=#12, #2, #13..=#17, #0, #18 →Read l2 →Fused with Child Map/Filter/Project - Project: #0..=#11, #2, #12..=#16, #0, #17, #13, #18..=#21 + Project: #0..=#12, #2, #13..=#17, #0, #18, #14, #19..=#22 →Read l3 →Arranged mz_catalog.mz_roles cte l5 = @@ -4394,7 +4394,7 @@ mz_internal.pg_type_all_databases: →Fused with Child Map/Filter/Project Project: #0 →Arranged l5 - Key: (#0..=#29) + Key: (#0..=#30) cte l7 = →Arranged l6 cte l8 = @@ -4461,73 +4461,73 @@ mz_internal.pg_type_all_databases: Project: #0 →Read l15 →Return - →Delta Join [%0:l4[#0..=#29] » %1:l5[#0..=#29] » %2[#0] » %3[#0] » %4[#0]] [%1:l5[#0] » %0:l4[#0..=#29] » %2[#0] » %3[#0] » %4[#0]] [%2[#0] » %1:l5[#0] » %0:l4[#0..=#29] » %3[#0] » %4[#0]] [%3[#0] » %1:l5[#0] » %0:l4[#0..=#29] » %2[#0] » %4[#0]] [%4[#0] » %1:l5[#0] » %0:l4[#0..=#29] » %2[#0] » %3[#0]] + →Delta Join [%0:l4[#0..=#30] » %1:l5[#0..=#30] » %2[#0] » %3[#0] » %4[#0]] [%1:l5[#0] » %0:l4[#0..=#30] » %2[#0] » %3[#0] » %4[#0]] [%2[#0] » %1:l5[#0] » %0:l4[#0..=#30] » %3[#0] » %4[#0]] [%3[#0] » %1:l5[#0] » %0:l4[#0..=#30] » %2[#0] » %4[#0]] [%4[#0] » %1:l5[#0] » %0:l4[#0..=#30] » %2[#0] » %3[#0]] path %0: after %3: - Project: #0..=#5, #7..=#10, #12 - Map: coalesce(#6, #11, 0) + Project: #0..=#5, #7..=#11, #13 + Map: coalesce(#6, #12, 0) after %4: - Project: #1..=#10, #12 - Map: coalesce(#11, 0) + Project: #1..=#11, #13 + Map: coalesce(#12, 0) Final closure: - Project: #0..=#2, #4, #11, #5, #6, #12, #13, #9, #10, #7, #8, #14, #13, #15, #13, #16, #3 + Project: #0..=#2, #4, #12, #5, #6, #13, #14, #10, #11, #7, #8, #15, #14, #16, #14, #17, #3, #9 Map: null, 44, 0, false, -1, null path %1: after %0: - Project: #1, #3, #13, #22, #25, #0, #30..=#33 - Map: text_to_"char"(case when (#19{mztype} = "a") then "b" else #19{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0) + Project: #1, #3, #14, #23, #26, #0, #31..=#35 + Map: text_to_"char"(case when (#20{mztype} = "a") then "b" else #20{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0), oidtoregproc(coalesce(#12{typsend}, 0)) after %3: - Project: #1..=#5, #0, #7..=#10, #12 - Map: coalesce(#6, #11, 0) + Project: #1..=#5, #0, #7..=#11, #13 + Map: coalesce(#6, #12, 0) after %4: - Project: #1..=#10, #12 - Map: coalesce(#11, 0) + Project: #1..=#11, #13 + Map: coalesce(#12, 0) Final closure: - Project: #0..=#2, #4, #11, #5, #6, #12, #13, #9, #10, #7, #8, #14, #13, #15, #13, #16, #3 + Project: #0..=#2, #4, #12, #5, #6, #13, #14, #10, #11, #7, #8, #15, #14, #16, #14, #17, #3, #9 Map: null, 44, 0, false, -1, null path %2: after %0: - Project: #1, #3, #13, #22, #25, #0, #30..=#34 - Map: text_to_"char"(case when (#19{mztype} = "a") then "b" else #19{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0) + Project: #1, #3, #14, #23, #26, #0, #31..=#36 + Map: text_to_"char"(case when (#20{mztype} = "a") then "b" else #20{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0), oidtoregproc(coalesce(#12{typsend}, 0)) after %3: - Project: #1..=#5, #0, #7..=#10, #12 - Map: coalesce(#6, #11, 0) + Project: #1..=#5, #0, #7..=#11, #13 + Map: coalesce(#6, #12, 0) after %4: - Project: #1..=#10, #12 - Map: coalesce(#11, 0) + Project: #1..=#11, #13 + Map: coalesce(#12, 0) Final closure: - Project: #0..=#2, #4, #11, #5, #6, #12, #13, #9, #10, #7, #8, #14, #13, #15, #13, #16, #3 + Project: #0..=#2, #4, #12, #5, #6, #13, #14, #10, #11, #7, #8, #15, #14, #16, #14, #17, #3, #9 Map: null, 44, 0, false, -1, null path %3: after %0: - Project: #1, #3, #13, #22, #25, #0, #30..=#34 - Map: text_to_"char"(case when (#19{mztype} = "a") then "b" else #19{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0) + Project: #1, #3, #14, #23, #26, #0, #31..=#36 + Map: text_to_"char"(case when (#20{mztype} = "a") then "b" else #20{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0), oidtoregproc(coalesce(#12{typsend}, 0)) after %2: - Project: #1..=#5, #0, #7..=#10, #12 - Map: coalesce(#11, #6, 0) + Project: #1..=#5, #0, #7..=#11, #13 + Map: coalesce(#12, #6, 0) after %4: - Project: #1..=#10, #12 - Map: coalesce(#11, 0) + Project: #1..=#11, #13 + Map: coalesce(#12, 0) Final closure: - Project: #0..=#2, #4, #11, #5, #6, #12, #13, #9, #10, #7, #8, #14, #13, #15, #13, #16, #3 + Project: #0..=#2, #4, #12, #5, #6, #13, #14, #10, #11, #7, #8, #15, #14, #16, #14, #17, #3, #9 Map: null, 44, 0, false, -1, null path %4: after %0: - Project: #1, #3, #13, #22, #25, #0, #30..=#34 - Map: text_to_"char"(case when (#19{mztype} = "a") then "b" else #19{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0) + Project: #1, #3, #14, #23, #26, #0, #31..=#36 + Map: text_to_"char"(case when (#20{mztype} = "a") then "b" else #20{mztype} end), text_to_"char"(case when (#4{category} = "array") then "A" else case when (#4{category} = "bit-string") then "V" else case when (#4{category} = "boolean") then "B" else case when (#4{category} = "composite") then "C" else case when (#4{category} = "date-time") then "D" else case when (#4{category} = "enum") then "E" else case when (#4{category} = "geometric") then "G" else case when (#4{category} = "list") then "U" else case when (#4{category} = "network-address") then "I" else case when (#4{category} = "numeric") then "N" else case when (#4{category} = "pseudo") then "P" else case when (#4{category} = "string") then "S" else case when (#4{category} = "timespan") then "T" else case when (#4{category} = "user-defined") then "U" else case when (#4{category} = "unknown") then "X" else null end end end end end end end end end end end end end end end), oidtoregproc(#10{typinput}), coalesce(#11{typreceive}, 0), oidtoregproc(coalesce(#12{typsend}, 0)) after %3: - Project: #1..=#5, #7..=#11, #13 - Map: coalesce(#6, #12, 0) + Project: #1..=#5, #7..=#12, #14 + Map: coalesce(#6, #13, 0) Final closure: - Project: #0..=#2, #4, #11, #6, #7, #12, #13, #10, #5, #8, #9, #14, #13, #15, #13, #16, #3 + Project: #0..=#2, #4, #12, #6, #7, #13, #14, #11, #5, #8, #9, #15, #14, #16, #14, #17, #3, #10 Map: null, 44, 0, false, -1, null - →Arrange (#0..=#29) + →Arrange (#0..=#30) →Stream l4 - →Arrange (#0) (#0..=#29) + →Arrange (#0) (#0..=#30) →Fused with Child Map/Filter/Project Filter: (#5 = #5) →Arranged l5 - Key: (#0..=#29) + Key: (#0..=#30) →Arrange (#0) →Union →Stream l11 @@ -19687,8 +19687,9 @@ EXPLAIN SELECT * FROM "pg_catalog"."pg_type"; ---- Explained Query (fast path): →Map/Filter/Project - Project: #0..=#17 + Project: #0..=#11, #20, #13..=#17, #19 Filter: ((#18{database_name}) IS NULL OR (#18{database_name} = "materialize")) + Map: oidtoregproc(#12{typreceive}) →Indexed mz_internal.pg_type_all_databases (using mz_internal.pg_type_all_databases_ind) Used Indexes: diff --git a/test/sqllogictest/mz_catalog_server_index_accounting.slt b/test/sqllogictest/mz_catalog_server_index_accounting.slt index e8c638c152d60..c7c1fe1ac4bc1 100644 --- a/test/sqllogictest/mz_catalog_server_index_accounting.slt +++ b/test/sqllogictest/mz_catalog_server_index_accounting.slt @@ -904,6 +904,7 @@ mz_tables source_id mz_type_pg_metadata id mz_type_pg_metadata typinput mz_type_pg_metadata typreceive +mz_type_pg_metadata typsend mz_types category mz_types create_sql mz_types id @@ -1017,5 +1018,6 @@ pg_type_all_databases typnotnull pg_type_all_databases typowner pg_type_all_databases typreceive pg_type_all_databases typrelid +pg_type_all_databases typsend pg_type_all_databases typtype pg_type_all_databases typtypmod diff --git a/test/sqllogictest/regclass.slt b/test/sqllogictest/regclass.slt index 7257fab6c0108..7c961fb25171c 100644 --- a/test/sqllogictest/regclass.slt +++ b/test/sqllogictest/regclass.slt @@ -274,6 +274,38 @@ SELECT NULL::regclass::text ---- NULL +# OID 0 is the absent reference, rendered as `-` like PostgreSQL's `regclassout`. +# A nonzero OID that names no relation keeps its digits. +query T +SELECT 0::regclass::text +---- +- + +query T +SELECT 99999::regclass::text +---- +99999 + +# `-` round trips on input too. Spelled from a literal because feeding a +# cast-derived value into the string to reg* cast hits the evaluation order +# problem behind the cases commented out above. +query T +SELECT '-'::regclass::text +---- +- + +query T +SELECT '-'::regclass::oid +---- +0 + +# Unlike a numeric string, `-` is accepted even by the explicit text to regclass +# cast. +query T +SELECT '-'::text::regclass::oid +---- +0 + # ensure that all existing types can be cast to their respective names statement OK select oid, oid::regclass::text from (select oid from pg_catalog.pg_class) diff --git a/test/sqllogictest/regproc.slt b/test/sqllogictest/regproc.slt index c9bc16ff5eb64..6f1a31d6198b1 100644 --- a/test/sqllogictest/regproc.slt +++ b/test/sqllogictest/regproc.slt @@ -85,6 +85,31 @@ SELECT NULL::regproc::text ---- NULL +# OID 0 is the absent reference, rendered as `-` like PostgreSQL's `regprocout`. +# A nonzero OID that names no function keeps its digits. +query T +SELECT 0::regproc::text +---- +- + +query T +SELECT 99999::regproc::text +---- +99999 + +# `-` round trips on input too. Spelled from a literal because feeding a +# cast-derived value into the string to reg* cast hits the evaluation order +# problem behind the cases commented out above. +query T +SELECT '-'::regproc::text +---- +- + +query T +SELECT '-'::regproc::oid +---- +0 + query error more than one function named "max" SELECT 'max'::regproc @@ -192,6 +217,32 @@ true true true +# The pgwire text encoding resolves a regproc to its function name. These must be +# `simple` records: `query` records read results in the binary format, where a +# regproc is a bare OID. + +# 1398 is one of six abs overloads, so it is qualified. 99999 names no function. +simple +SELECT 1242::regproc, 1398::regproc, 99999::regproc, 0::regproc +---- +boolin,pg_catalog.abs,99999,- +COMPLETE 1 + +# Type I/O function names, which is what clients read to decode values +simple +SELECT typinput, typreceive FROM pg_type WHERE typname = 'bool' +---- +boolin,boolrecv +COMPLETE 1 + +# regclass and regtype keep rendering as digits, since they can name objects a +# user created +simple +SELECT 1082::regtype, 1242::regclass +---- +1082,1242 +COMPLETE 1 + # ensure that catalog functions can be resolved if the active database is invalid statement OK diff --git a/test/sqllogictest/regtype.slt b/test/sqllogictest/regtype.slt index 98aa4a37e3671..817cb6225b7ee 100644 --- a/test/sqllogictest/regtype.slt +++ b/test/sqllogictest/regtype.slt @@ -119,6 +119,31 @@ SELECT NULL::regtype::text ---- NULL +# OID 0 is the absent reference, rendered as `-` like PostgreSQL's `regtypeout`. +# A nonzero OID that names no type keeps its digits. +query T +SELECT 0::regtype::text +---- +- + +query T +SELECT 99999::regtype::text +---- +99999 + +# `-` round trips on input too. Spelled from a literal because feeding a +# cast-derived value into the string to reg* cast hits the evaluation order +# problem behind the cases commented out above. +query T +SELECT '-'::regtype::text +---- +- + +query T +SELECT '-'::regtype::oid +---- +0 + # ensure that all existing types can be cast to their respective names statement OK select oid, oid::regtype::text from (select oid from mz_catalog.mz_types) diff --git a/test/workload-replay/system_catalog_identifiers.txt b/test/workload-replay/system_catalog_identifiers.txt index 354f1df0e5662..02d57e04b7507 100644 --- a/test/workload-replay/system_catalog_identifiers.txt +++ b/test/workload-replay/system_catalog_identifiers.txt @@ -1572,6 +1572,7 @@ typnotnull typowner typreceive typrelid +typsend typtype typtypmod _uint2