// Lazy PostgreSQL type introspection backed by system catalog queries.
///|
/// Query that retrieves the high-level shape of one PostgreSQL type OID.
let typeinfo_query = "select t.typname::text, t.typtype::text, t.typelem::oid, r.rngsubtype::oid, t.typbasetype::oid, n.nspname::text, t.typrelid::oid from pg_catalog.pg_type t left join pg_catalog.pg_range r on r.rngtypid = t.oid inner join pg_catalog.pg_namespace n on t.typnamespace = n.oid where t.oid = $1"
///|
/// Query that retrieves enum labels for one PostgreSQL enum type.
let typeinfo_enum_query = "select enumlabel::text from pg_catalog.pg_enum where enumtypid = $1 order by enumsortorder"
///|
/// Query that retrieves visible composite fields for one relation-backed type.
let typeinfo_composite_query = "select attname::text, atttypid::oid from pg_catalog.pg_attribute where attrelid = $1 and not attisdropped and attnum > 0 order by attnum"
///|
/// Return a cached `Type`, querying PostgreSQL catalogs only on cache misses.
async fn cached_type(shared : Shared, oid : @proto.Oid) -> Type {
match shared.types.get(oid) {
Some(type_) => type_
None => {
// Cache misses are resolved once and written back so later row/parameter
// decoding for the same OID stays local.
let type_ = lookup_type(shared, oid)
shared.types[oid] = type_
type_
}
}
}
///|
/// Fetch rich metadata for a PostgreSQL type OID.
///
/// The driver first checks built-ins, then falls back to catalog queries. If
/// those fail or return no rows, it preserves the OID inside an `Unknown`
/// descriptor so later error messages remain informative.
async fn lookup_type(shared : Shared, oid : @proto.Oid) -> Type {
match builtin_type(oid) {
Some(type_) => type_
None => {
let rows = query_oid_catalog(shared, typeinfo_query, oid) catch {
_ => []
}
// Catalog lookup is best-effort; falling back to `Unknown` preserves the
// raw OID for later diagnostics without blocking query execution.
if rows.is_empty() {
return Type::unknown(oid, name="oid_\{oid.to_string()}")
}
let row = rows[0]
let name : String = row.get(0)
let typtype : String = row.get(1)
let elem_oid : @proto.Oid = row.get(2)
let range_subtype : @proto.Oid? = row.get(3)
let base_oid : @proto.Oid = row.get(4)
let relid : @proto.Oid = row.get(6)
// Match PostgreSQL's richer type families first before falling back to a
// plain scalar descriptor.
let kind = if typtype == "e" {
Kind::Enum(query_enum_labels(shared, oid))
} else if typtype == "p" {
Pseudo
} else if base_oid != 0U {
Domain(base_oid)
} else if elem_oid != 0U {
Array(elem_oid)
} else if relid != 0U {
Composite(query_composite_fields(shared, relid))
} else {
match range_subtype {
Some(subtype) => Range(subtype)
None => Simple
}
}
{ oid, name, kind, }
}
}
}
///|
/// Run a catalog query that accepts a single OID parameter.
///
/// This helper builds a temporary `Client` facade around `Shared` so the type
/// cache can reuse the ordinary query path without special wiring.
async fn query_oid_catalog(
shared : Shared,
sql : String,
oid : @proto.Oid,
) -> Array[Row] {
let client : Client = { shared, }
// Reuse the ordinary query pipeline so the type cache does not maintain its
// own special-purpose protocol machinery.
let params : Array[&ToSql] = [oid]
let stream = client.query_typed(sql, [Type::oid_type()], params~)
stream.collect()
}
///|
/// Fetch enum labels for one enum type OID.
async fn query_enum_labels(shared : Shared, oid : @proto.Oid) -> Array[String] {
let rows = query_oid_catalog(shared, typeinfo_enum_query, oid) catch {
_ => []
}
rows.map(row => row.get(0))
}
///|
/// Fetch field metadata for one composite type.
async fn query_composite_fields(
shared : Shared,
oid : @proto.Oid,
) -> Array[Field] {
let rows = query_oid_catalog(shared, typeinfo_composite_query, oid) catch {
_ => []
}
rows.map(row => { name: row.get(0), type_oid: row.get(1), })
}
///|
/// Parse parameter OIDs from PostgreSQL's `ParameterDescription`.
fn parse_parameter_oids(
body : @backend.ParameterDescriptionBody,
) -> Array[@proto.Oid] raise {
let types : Array[@proto.Oid] = []
let params = body.parameters()
for param = params.next() {
match param {
None => break types
Some(oid) => {
types.push(oid)
continue params.next()
}
}
}
}
///|
/// Resolve a parameter OID list into cached `Type` descriptors.
async fn resolve_parameter_types(
shared : Shared,
oids : Array[@proto.Oid],
) -> Array[Type] {
let out : Array[Type] = []
for oid in oids {
out.push(cached_type(shared, oid))
}
out
}
///|
/// Replace placeholder column types with cached rich descriptors.
async fn resolve_columns(
shared : Shared,
columns : Array[Column],
) -> Array[Column] {
let out : Array[Column] = []
for column in columns {
// Preserve every other row-description field and swap only the placeholder
// type descriptor for its cached rich form.
out.push({ ..column, type_: cached_type(shared, column.type_.oid), })
}
out
}