Skip to content

Consider modelling scalar-domain aliases (int4↔integer, …) — needs implicit binary casts to be interchangeable #345

Description

@tobyhede

Summary

Now that the eql_v3 scalar domains use SQL-standard canonical names (integer/smallint/bigint/boolean/real/double — see #344), consider whether to also expose the Postgres-internal spellings (int4/int2/int8/bool/float4/float8) as aliases, so a caller can type their EQL column with whichever name matches their native column. This issue captures the design considerations from that discussion — chiefly the casting problem, which is what makes "alias" more than a rename.

The core constraint: Postgres domains can't be true aliases

integer/int4, smallint/int2, etc. are aliases in Postgres's native type system (one OID, resolved by the parser). But a DOMAIN is a user-defined type with exactly one name, and there is no CREATE SYNONYM for types. So any "alias" we add is a second, independent domain — distinct OID, distinct operator/cast resolution. This is the whole reason "alias" is a concept worth modelling rather than a free rename.

The casting issue (the crux)

Because each alias is a distinct type, a function or operator declared on eql_v3.integer will not accept an eql_v3.int4 value, and vice-versa:

-- with int4 added as a naive second domain:
WHERE eql_v3.eq(col_integer, x::eql_v3.int4)   -- fails to resolve

To make the two names actually interchangeable (a real alias, not two unrelated types), we'd need to emit an implicit binary cast between the pair. Both domains are jsonb-backed, so they're binary-compatible:

CREATE CAST (eql_v3.int4 AS eql_v3.integer) WITHOUT FUNCTION AS IMPLICIT;

Open questions on the cast:

  • Does casting to a domain re-run its CHECK? (It should — harmless here since the payload already satisfies it, but needs verifying.)
  • Direction/pairs: one implicit cast per direction, per domain variant (int4integer, int4_eqinteger_eq, int4_ordinteger_ord, …). That's 2 × variants casts per aliased family.

Without the cast, "alias" is a lie — you'd ship two silently-incompatible types and a foot-gun.

Related: the native-type-token casting gotcha (found in #344)

A distinct-but-adjacent casting hazard surfaced while landing the canonical rename: the domain token must not be assumed to be a valid native SQL type name. We named float8's domain eql_v3.double, but double is not a native Postgres type (only double precision is) — so x::double / CREATE TABLE (c double) fails with type "double" does not exist. In #344 this bit the SQLx harness's plaintext oracle column; fixed by deriving PLAINTEXT_SQL_TYPE from the cipherstash cast (→ double precision), the same PG_TYPE-vs-plaintext split timestamp uses (domain timestamp, native timestamp with time zone).

Relevance to aliases: if aliases are added, any place that maps a domain token to a native type (fixtures, oracles, docs, codegen) must keep "domain token" and "native SQL type name" separate. An alias like eql_v3.float8 would map back to native double precision too.

Crypto is name-independent (makes aliases cheap on the axis that matters)

Encryption is driven by the cipherstash Cast (int/small_int/big_int/real/double/boolean), keyed on ScalarKindnot the domain name. So an alias domain wraps byte-identical ciphertext: aliases cost nothing on the crypto/fixture axis (the expensive, creds-gated part) and full price only on the SQL surface (domains, extractors, wrappers, operators, casts), bindings, and matrix wiring.

Options (from the earlier discussion)

  1. Do nothing — ship only canonical names (current state after Rename eql_v3 scalar domains to SQL-standard names (int4→integer, etc.) #344). Cheapest; drops the int4 spelling.
  2. Naive extra DomainFamily per alias — works but produces silently non-interchangeable types. ❌ foot-gun without casts.
  3. Model alias as a first-class catalog concept — add aliases: &[&str] (or canonical + spellings) to DomainFamily; codegen emits the alias domain surface mechanically from the canonical, shares fixtures, and emits the implicit binary casts above so the two names are genuinely interchangeable. Bindings/docs pick the canonical struct name; the alias gets a doc note. This is the only option where "alias" is real.

Recommendation

Only pursue (3) if there's concrete demand for both spellings to be callable (e.g. so the EQL column name reads the same as the native column). Prototype the aliases field + generated implicit casts to size the real diff (and settle the CHECK-on-cast question) before committing. Otherwise (1) stands.

Out of scope / notes

  • json/jsonb: not an alias pair in Postgres (genuinely different types) — decide separately, not part of this.
  • timestamp: already carries the domain-token-vs-native-type split (timestamp domain, timestamp with time zone plaintext) — a working precedent for the pattern option (3) needs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions