Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
003b6d4
catalog: add pg_type.typsend and type typreceive as regproc
claude Jul 27, 2026
a5af322
test/adbc: silence pyright reportMissingImports on container-only deps
claude Jul 27, 2026
908d4b3
pgrepr: render regproc as the resolved function name in text format
claude Jul 27, 2026
208d36b
pgrepr: do not assert an ambiguous regproc rendering round trips
claude Jul 27, 2026
ec48414
catalog: migrate mz_type_pg_metadata by replacement, not evolution
claude Jul 27, 2026
033eca3
pgrepr: document the regproc reserved-word quoting divergence
claude Jul 27, 2026
4f47bff
sqllogictest: regenerate catalog_server_explain.slt for pg_type typsend
claude Jul 27, 2026
8075a5e
catalog: tighten mz_type_pg_metadata.typsend and document OID provenance
claude Jul 27, 2026
8596cf0
all: trim comments that do not earn their keep
claude Jul 27, 2026
2d7aff0
sql: render reg-type OID 0 as `-` in text casts
claude Jul 27, 2026
836a109
Merge origin/main into adbc-pg-type-typsend
claude Jul 29, 2026
778fbfa
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Jul 29, 2026
f50cb79
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Aug 3, 2026
922263c
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Aug 4, 2026
de670d5
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Aug 6, 2026
6ffff65
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Aug 6, 2026
8734aeb
Merge remote-tracking branch 'origin/main' into adbc-pg-type-typsend
claude Aug 27, 2026
7ddce8f
test/adbc: bump ADBC to 1.12.0, pyarrow to 25.0.1, python image to 3.…
claude Aug 27, 2026
a6e0392
sql: accept `-` as OID 0 when casting a string to a reg* type
claude Aug 27, 2026
ff48413
pgrepr: regenerate the regproc name table with REWRITE=1
claude Aug 27, 2026
8822b54
sql: spell the reg* `-` round-trip tests with a literal input
claude Aug 27, 2026
6e7030d
ci: move ADBC test suite from the test pipeline to nightly
claude Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ci/test/pipeline.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,17 @@ steps:
agents:
queue: hetzner-aarch64-4cpu-8gb

- id: adbc
label: Arrow ADBC driver
depends_on: build-aarch64
timeout_in_minutes: 20
inputs: [test/adbc]
plugins:
- ./ci/plugins/mzcompose:
composition: adbc
agents:
queue: hetzner-aarch64-4cpu-8gb

- id: chbench-demo
topics: [debezium, kafka, mysql]
label: chbench smoke
Expand Down
2 changes: 2 additions & 0 deletions console/types/materialize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4004,6 +4004,7 @@ export interface MzTypePgMetadata {
id: Generated<string>;
typinput: Generated<number>;
typreceive: Generated<number>;
typsend: Generated<number | null>;
}

export interface MzTypes {
Expand Down Expand Up @@ -4245,6 +4246,7 @@ export interface PgTypeAllDatabases {
typowner: number;
typreceive: number;
typrelid: number;
typsend: string;
typtype: string;
typtypmod: number;
}
Expand Down
26 changes: 21 additions & 5 deletions src/adapter/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2928,6 +2928,7 @@ mod tests {
array: u32,
input: u32,
receive: u32,
send: u32,
}

struct PgOper {
Expand Down Expand Up @@ -2967,7 +2968,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"
),
&[],
)
Expand All @@ -2983,6 +2984,7 @@ mod tests {
array: row.get("typarray"),
input: row.get("typinput"),
receive: row.get("typreceive"),
send: row.get("typsend"),
};
(oid, pg_type)
})
Expand Down Expand Up @@ -3074,10 +3076,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",
Expand All @@ -3088,6 +3095,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),
Expand Down
1 change: 1 addition & 0 deletions src/adapter/src/catalog/builtin_table_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,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,
));
Expand Down
21 changes: 21 additions & 0 deletions src/adapter/src/catalog/open/builtin_schema_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,27 @@ static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
MZ_CATALOG_SCHEMA,
"mz_kafka_sources",
),
// `mz_type_pg_metadata` gained a trailing `typsend` column. Appending a
// column would be backward compatible enough for `Evolution`, but
// `Replacement` is the right mechanism for a builtin table anyway.
// `Coordinator::bootstrap_tables` retracts every system table's persist
// snapshot and re-derives the rows from the catalog on each boot, so a
// fresh shard loses nothing. More importantly, only `Replacement`
// reports the item in `MigrationResult::replaced_items`, which is what
// makes the read-only coordinator back-fill the new shard so dependent
// dataflows such as `pg_type_all_databases_ind` can hydrate. Under
// `Evolution` a read-only replica would instead have to read the old
// parts through the schema-migration path, which no builtin table has
// ever exercised.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you were right to push. "every system table" was false: bootstrap_tables filters through is_retained_across_restarts, which holds back mz_storage_usage_by_shard and mz_object_arrangement_size_history (coord.rs:3066-3076), and validate_migration_steps asserts against migrating those same two. The conclusion for mz_type_pg_metadata still holds, it just isn't one of the retained ones. Reworded to say exactly that and to name them.

While in there I also stopped asserting the rest and traced it instead: replaced_items is derived from MigrationRunResult::new_shards, which only migrate_replace populates, and it reaches the coordinator as migrated_storage_collections_0dt, which gates the read-only back-fill at coord.rs:2813. And I dropped "no builtin table has ever exercised" since it isn't checkable (the forced-migration knob can drive Evolution over anything); it now says the list's only Evolution step targets a source, which is verifiable from MIGRATIONS.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cut it. You're right that replacement needs no defending: 32 of the 33 steps in that list use it, so it is the convention and the module docs already carry the rule. The entry is down to one line saying what changed about the object, matching its neighbours. The fact that evolution failed 0dt-upgrade-smoke-test was the only durable bit in there, and the PR description already records it.

I took this as licence to audit the whole diff rather than just that entry, and cut 86 lines of comment net across 11 files. The test I applied: a comment stays only if it records a non-obvious constraint, a measured PostgreSQL behaviour, or a reason a tempting simplification is wrong. Things that went:

  • Enumerations of the match arms directly below them, for example the regprocout rendering rules in value.rs.
  • Motivation that belongs in the PR description, for example why clients care about typreceive names.
  • The same fact stated three times in one file (the overloaded-name ambiguity in value.rs).
  • "Appended rather than placed at PostgreSQL's ordinal position", duplicated in pg_type and pg_type_all_databases. Appending is what you would do anyway.
  • The typsend_oid field doc, now a one-liner matching typinput_oid and typreceive_oid instead of adding a caveat neither of them makes.
  • The json.rs note is 7 lines down to 4. It stays because it stops someone "fixing" that arm to emit names and contradicting the integer schema build_row_schema_json publishes below it.

What I kept, trimmed: the regproc vs oid asymmetry between pg_type and pg_type_all_databases, the reserved-word and search-path divergences from regprocout, the OID provenance you asked for, and the COALESCE note, since a LEFT JOIN making a non-nullable column nullable is genuinely surprising.

Comment-only, no golden moved. 8596cf05c4.


Generated by Claude Code

//
// See the NOTE above: this version must stay at the workspace's current
// dev version until the change ships.
MigrationStep::replacement(
"26.36.0-dev.0",
CatalogItemType::Table,
MZ_INTERNAL_SCHEMA,
"mz_type_pg_metadata",
),
]
});

Expand Down
58 changes: 58 additions & 0 deletions src/catalog/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,64 @@ mod tests {

use super::*;

/// `mz_pgrepr::regproc::NAMES` is a table of every builtin function OID and
/// the text a `regproc` renders it as. It has to be checked in as data
/// because `mz-pgrepr` sits below this crate in the dependency graph and so
/// cannot read the registry itself. This test is what keeps it honest: it
/// recomputes the whole table from the registry and, on drift, prints the
/// corrected table to paste 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<u32, String> = BTreeMap::new();
for func in BUILTINS::funcs() {
// The rule `regprocout` applies: print the bare name when it
// resolves back to this OID on its own, otherwise qualify it.
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 actual: BTreeMap<u32, String> = mz_pgrepr::regproc::NAMES
.iter()
.map(|(oid, name)| (*oid, name.to_string()))
.collect();

if actual != expected {
let table: String = expected
.iter()
.map(|(oid, name)| format!(" ({}, \"{}\"),\n", oid, name))
.collect();
panic!(
"mz_pgrepr::regproc::NAMES has drifted from the builtin function registry. \
Replace the entries of NAMES in src/pgrepr-consts/src/regproc.rs with:\n{}",
table
);
}
}

#[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() {
Expand Down
1 change: 1 addition & 0 deletions src/catalog/src/builtin/mz_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ pub const TYPE_MZ_ACL_ITEM_ARRAY: BuiltinType<NameReference> = BuiltinType {
pg_metadata: Some(CatalogTypePgMetadata {
typinput_oid: 750,
typreceive_oid: 2400,
typsend_oid: 2401,
}),
},
};
Expand Down
14 changes: 13 additions & 1 deletion src/catalog/src/builtin/mz_internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,14 @@ pub static MZ_TYPE_PG_METADATA: LazyLock<BuiltinTable> = LazyLock::new(|| Builti
.with_column("id", SqlScalarType::String.nullable(false))
.with_column("typinput", SqlScalarType::Oid.nullable(false))
.with_column("typreceive", SqlScalarType::Oid.nullable(false))
// `pack_type_update` always writes an OID here, so this declaration is
// wider than the data requires. Downstream `COALESCE`s on this column
// are there for the `LEFT JOIN` against this table, not for this.
//
// TODO: Tighten to `nullable(false)` to match `typinput` and
// `typreceive`. That also changes the descs of the views projecting the
// column.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we tighten it here and just handle the downstream views? We are already touching them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, typsend is now SqlScalarType::Oid.nullable(false) and the TODO is gone. Nothing downstream actually needed to move: both views already declared typsend as RegProc.nullable(false), and the COALESCE(..., 0) in pg_type_all_databases has to stay because it guards the LEFT JOIN against this table, not the column's declaration. typreceive is the precedent (already non-nullable in the table, still coalesced), and typinput shows the other half of it, non-nullable in the table but nullable(true) in the view because nothing coalesces it.

Only other change was console/types/materialize.d.ts, where typsend becomes Generated<number> to match typinput/typreceive. The existing MigrationStep::replacement covers the fingerprint change on its own, since BuiltinTable::fingerprint is just the desc and replacement rebuilds the shard.


Generated by Claude Code

.with_column("typsend", SqlScalarType::Oid.nullable(true))
.finish(),
column_comments: BTreeMap::new(),
is_retained_metrics_object: false,
Expand Down Expand Up @@ -4372,6 +4380,9 @@ pub static PG_TYPE_ALL_DATABASES: LazyLock<BuiltinView> = LazyLock::new(|| {
.with_column("typcollation", SqlScalarType::Oid.nullable(false))
.with_column("typdefault", SqlScalarType::String.nullable(true))
.with_column("database_name", SqlScalarType::String.nullable(true))
// Appended rather than placed at PostgreSQL's ordinal position, to
// keep the existing columns' positions stable.
.with_column("typsend", SqlScalarType::RegProc.nullable(false))
.finish(),
column_comments: BTreeMap::new(),
sql: "
Expand Down Expand Up @@ -4440,7 +4451,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
Expand Down
Loading
Loading