Provider · docs/types/README.md

Core type mappings

Catalogue-driven PostgreSQL type mappings and extension points.

8 min read1,714 wordsSource synchronized
View source on GitHub
ON THIS PAGE

Core type mappings

BlueTusk registers PostgreSQL scalar types by catalogue OID. Simple queries decode the server’s text format. Extended queries prefer binary fields and transparently retry with text outside a transaction when a selected PostgreSQL type has no binary output function. Inside an explicit transaction they request text fields up front, avoiding a failed Bind that would abort the transaction.

PostgreSQL type OID Default CLR value Text Binary
bool 16 bool yes yes
bytea 17 byte[] yes yes
"char" 18 BlueTuskInternalChar yes yes
name 19 string yes yes
int8 20 long yes yes
int2 21 short yes yes
int4 23 int yes yes
text 25 string yes yes
oid 26 uint yes yes
json 114 string yes yes
xml 142 string yes yes
float4 700 float yes yes
float8 701 double yes yes
bpchar 1042 string yes yes
varchar 1043 string yes yes
date 1082 DateOnly yes yes
time 1083 TimeSpan yes yes
timestamp 1114 DateTime yes yes
timestamptz 1184 DateTimeOffset yes yes
numeric 1700 BlueTuskNumeric yes yes
uuid 2950 Guid yes yes
jsonb 3802 string yes yes

Numeric values

BlueTuskNumeric preserves PostgreSQL arbitrary precision as a BigInteger unscaled value and a scale up to 16,383. It also represents NaN, positive infinity, and negative infinity. GetDecimal and GetFieldValue<decimal> perform a checked conversion and fail when the value is special or outside decimal’s range.

Use BlueTuskParameter<BlueTuskNumeric> to send values that cannot fit in decimal:

var value = BlueTuskNumeric.Parse("123456789012345678901234567890.123456789");
command.Parameters.Add(new BlueTuskParameter<BlueTuskNumeric>(value));

Temporal values

PostgreSQL date infinity maps to DateOnly.MinValue and DateOnly.MaxValue. timestamp infinity maps to DateTime.MinValue and DateTime.MaxValue; timestamptz uses the corresponding DateTimeOffset values and normalizes finite results to UTC.

PostgreSQL time can represent 24:00:00, so its default CLR type is TimeSpan. GetFieldValue<TimeOnly> is available for values before 24:00. Binary temporal values preserve PostgreSQL’s microsecond precision.

BlueTuskInterval represents PostgreSQL finite intervals and the positive/negative infinity values supported by PostgreSQL 17 and later. PostgreSQL 15–16 reject interval infinity at the server boundary.

Binary and streaming behavior

UUID binary values use PostgreSQL network byte order, JSONB validates its version byte, and bytea accepts binary, hexadecimal text, and legacy escape text. UTF-8 decoding is strict so malformed server text is rejected rather than silently replaced.

The default data reader buffers complete result sets for random field access. A reader created with CommandBehavior.SequentialAccess instead uses an incremental portal and consumes fields directly from the transport: fields must be visited in ordinal order, binary bytea is exposed through GetStream, and text, JSON, and JSONB are decoded incrementally through GetTextReader. The default unlimited execution uses PostgreSQL’s unnamed portal and avoids suspension round trips; set BlueTuskCommand.SequentialFetchSize to a positive row count to use a generated named portal when server-side bounded fetches are preferable. Materializing a scalar value buffers only that field.

An unregistered OID is returned as BlueTuskUnknownValue, preserving its format and raw bytes.

Catalogue-discovered structured types

Each data source loads PostgreSQL type relationships from the system catalogues. Arrays, domains, enums, composites, records, ranges, and multiranges are composed from the codecs of their contained types in both text and binary formats. Runtime codecs can be registered by schema-qualified catalogue name, while MapEnum<TEnum> and MapComposite<T> provide typed mappings for user-defined enums and composites.

Applications that want compile-time composite member discovery can reference BlueTusk.SourceGeneration as an analyzer and annotate a top-level partial CLR type:

<PackageReference Include="BlueTusk.SourceGeneration"
                  PrivateAssets="all"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />
using BlueTusk.TypeSystem;

[BlueTuskComposite("app", "address")]
public sealed partial record Address(int HouseNumber, string Street);

Address.RegisterCodec(dataSourceBuilder.Types);

The generator follows the runtime mapper’s snake-case and [BlueTuskName] conventions. It emits typed member getters and CLR construction, while data-source initialization still resolves the PostgreSQL fields, OIDs, nested codecs, and array codec from the live catalogue. The generated CLR members must exactly cover the catalogue composite. MapComposite<T> remains the reflection-based fallback for types that do not opt in. See the source-generator package guide for supported CLR shapes and registration details.

Set BlueTuskParameter.PostgreSqlTypeName when a parameter—especially a null value—must select one of those catalogue-discovered types. Names are parsed using PostgreSQL identifier rules: unquoted identifiers fold to lowercase, quoted identifiers preserve case and may contain dots, and a trailing [] selects the discovered array type.

BlueTuskRange<T> keeps empty ranges distinct from ranges with one or two unbounded sides. Construct finite bounds with BlueTuskRangeBound.Inclusive(value) or BlueTuskRangeBound.Exclusive(value), and use BlueTuskRangeBound.Unbounded<T>() for an infinite side:

var finite = new BlueTuskRange<int>(1, 10); // [1,10)
var upperBounded = new BlueTuskRange<int>(
    BlueTuskRangeBound.Unbounded<int>(),
    BlueTuskRangeBound.Inclusive(10));
var empty = BlueTuskRange.Empty<int>();

BlueTuskMultirange<T> is an immutable ordered collection of BlueTuskRange<T> values. Range and multirange arrays are discovered and composed automatically, and all four forms participate in parameter type inference when the CLR mapping is unique.

Transaction catalogue values

PostgreSQL’s unsigned transaction identifiers have dedicated CLR values so their complete wire ranges are preserved:

PostgreSQL type CLR value
xid BlueTuskTransactionId
cid BlueTuskCommandId
xid8 BlueTuskFullTransactionId
pg_snapshot BlueTuskTransactionSnapshot
txid_snapshot BlueTuskTransactionSnapshot

BlueTuskTransactionSnapshot validates PostgreSQL’s ordered half-open snapshot invariants and defensively copies the in-progress transaction IDs. Modern pg_snapshot is the default parameter inference target. The deprecated txid_snapshot remains readable and writable by specifying its PostgreSQL OID explicitly. Catalogue-discovered arrays of all five types are composed automatically.

Object identifiers and catalogue vectors

PostgreSQL 19’s unsigned 64-bit oid8 maps to BlueTuskObjectIdentifier64, preserving the complete 0 through UInt64.MaxValue range in text and big-endian binary formats. Its array type is catalogue-composed like other built-ins. Earlier PostgreSQL releases do not advertise this type, so the runtime catalogue does not register it there.

The PostgreSQL reg* aliases use symbolic names in text and unsigned four-byte OIDs in binary. BlueTusk provides a distinct CLR wrapper for each alias so parameter inference remains unambiguous:

PostgreSQL type CLR value
regproc BlueTuskRegProc
regprocedure BlueTuskRegProcedure
regoper BlueTuskRegOper
regoperator BlueTuskRegOperator
regclass BlueTuskRegClass
regtype BlueTuskRegType
regconfig BlueTuskRegConfig
regdictionary BlueTuskRegDictionary
regnamespace BlueTuskRegNamespace
regrole BlueTuskRegRole
regcollation BlueTuskRegCollation
regdatabase (PostgreSQL 19+) BlueTuskRegDatabase

Construct an alias from either form. The returned Identifier preserves whether a text result was symbolic or numeric:

var relationByName = new BlueTuskRegClass("public.orders");
var relationByOid = new BlueTuskRegClass(16_384);

Symbolic values are sent as text so PostgreSQL resolves them using its normal namespace and search-path rules. Numeric values use binary. The same value-sensitive choice applies to catalogue-composed arrays.

The built-in type coverage acceptance test queries pg_catalog.pg_type on every supported server and requires a codec for every queryable base, range, and multirange type. This keeps new PostgreSQL built-ins visible as an executable compatibility failure rather than silently treating them as an unknown type. See PostgreSQL 19’s object identifier type documentation for oid8 and regdatabase semantics.

int2vector maps to the immutable BlueTuskInt16Vector; oidvector maps to BlueTuskObjectIdentifierVector. Their codecs enforce PostgreSQL’s one-dimensional, zero-based, null-free binary shape, including full unsigned OID values. PostgreSQL does not accept an empty vector through its binary receive function, so BlueTusk automatically uses text for an empty vector or an array containing one.

Runtime codecs that have the same value-dependent requirement can implement IBlueTuskWriteFormatSelector. Its DefaultWriteFormat also controls empty composed arrays, whose elements cannot provide a value-specific choice. Otherwise registered codec parameters continue to prefer binary.

JSONPath and text-like catalogue values

PostgreSQL type CLR value Notes
"char" BlueTuskInternalChar One raw byte; distinct from SQL char(n)
refcursor BlueTuskRefCursor PostgreSQL portal name
pg_node_tree BlueTuskNodeTree Opaque, decode-only expression tree
jsonpath BlueTuskJsonPath SQL/JSON path expression

BlueTuskInternalChar preserves all 256 values. Its text codec implements PostgreSQL’s empty representation for zero, single-byte ASCII representation, and backslash-plus-octal representation for values with the high bit set.

BlueTuskJsonPath deliberately preserves the expression as text and lets PostgreSQL parse, validate, and normalize it. Its binary codec validates the PostgreSQL JSONPath wire-version byte. refcursor values and arrays use their own CLR type so they do not collide with ordinary text inference.

PostgreSQL exposes pg_node_tree through system catalogues but rejects input values of that type. BlueTusk therefore decodes its text and binary forms into BlueTuskNodeTree, while attempts to encode one fail locally with NotSupportedException.

Text-only and opaque catalogue values

PostgreSQL type CLR value Behavior
aclitem BlueTuskAccessControlItem Text-only read/write; PostgreSQL validates ACL syntax
gtsvector BlueTuskGistTextSearchVector Text-only, decode-only GiST signature
pg_ndistinct BlueTuskNDistinctStatistics Decode-only opaque statistics payload
pg_dependencies BlueTuskDependencyStatistics Decode-only opaque statistics payload
pg_mcv_list BlueTuskMostCommonValueStatistics Decode-only opaque statistics payload
pg_brin_bloom_summary BlueTuskBrinBloomSummary Decode-only opaque BRIN summary
pg_brin_minmax_multi_summary BlueTuskBrinMinMaxMultiSummary Decode-only opaque BRIN summary

aclitem has no binary input or output functions. Its codec and catalogue-composed arrays therefore always use text, including empty arrays. gtsvector likewise has no binary output and PostgreSQL rejects input values.

The statistics and BRIN values preserve their exact field format and bytes through distinct immutable CLR types. Their internal representation is deliberately not interpreted: PostgreSQL owns that version-specific encoding and rejects client input even where a receive function appears in the catalogue. BlueTusk consequently exposes these values for lossless inspection and rejects attempts to encode them.