Extension SDK
Extensions register types and immutable feature descriptors through BlueTusk.Extensions.Abstractions. BlueTuskDataSourceBuilder.Build() snapshots both registries into the resulting data source. Later builder changes do not mutate an existing data source, and optional packages remain independently deployable without extension-specific dependencies in BlueTusk core packages.
What “supports PostgreSQL extensions” means
BlueTusk does not use a closed server-extension allowlist. Any extension
installed on the connected PostgreSQL server can be called through normal,
parameterized commands, and EF Core can own any extension’s installation with
modelBuilder.HasExtension("extension_name"). Runtime catalogue discovery
loads extension-provided OIDs instead of assuming a compiled-in type list.
The level of convenience depends on what an extension contributes:
| Extension surface | BlueTusk behavior |
|---|---|
| Functions, procedures, operators, access methods, and background workers | Available through normal SQL without a BlueTusk-specific package. |
| Domains, enums, composites, arrays, ranges, and multiranges over known types | Catalogue-discovered and composed automatically. |
| A new base wire type | Preserved as BlueTuskUnknownValue until an extension package registers a semantic codec. |
| Provider-specific CLR values, helpers, or LINQ translation | Supplied by an optional first-party or third-party BlueTusk package. |
This makes “all extensions” an interoperability contract, not a promise that every PostgreSQL extension has a bespoke CLR API. Server installation, binary compatibility, preloading, privileges, and operational safety remain properties of PostgreSQL and the extension itself.
The extension-authoring seam is compatibility-stable: the public surfaces of
BlueTusk.Extensions.Abstractions, BlueTusk.Extensions.Testing,
BlueTusk.TypeSystem, and the ADO.NET integration points have compiler-enforced
shipped API/nullability baselines. BlueTusk.Extensions.Citext supplied the
first executable compatibility slice and the packaged template exercises the
same contract. The registered first-party extension packages and their EF
integrations are included in the stable Provider 1.0.0 API freeze.
Start an extension package
BlueTusk.Templates provides a complete source-and-test skeleton:
dotnet new install BlueTusk.Templates
dotnet new bluetusk-extension `
-n Contoso.BlueTusk.Extensions.MyType `
--ExtensionName MyType `
--PostgreSqlTypeName my_type
The generated package keeps extension-specific SQL and CLR types outside the
core provider. It includes binary/text codec tests and a live contract test
using BlueTusk.Extensions.Testing.
The framework-neutral compatibility verifier checks four integration boundaries through a built data source: immutable feature retention, live catalogue type discovery, resolved CLR identity, and resolved codec identity. It briefly checks out a normal pooled connection; the caller continues to own and dispose the data source. Extension authors must also add representative value round trips, PostgreSQL behavioural tests, package-content inspection, and any separate EF translation/migration plug-in tests.
citext
Install citext in PostgreSQL, configure one long-lived data source, and use the extension-owned CLR value so runtime type inference remains unambiguous from ordinary PostgreSQL text:
CREATE EXTENSION IF NOT EXISTS citext;
using BlueTusk.Data;
using BlueTusk.Extensions.Citext;
var builder = new BlueTuskDataSourceBuilder(connectionString).UseCitext();
await using var dataSource = builder.Build();
await using var command = dataSource.CreateCommand(
"SELECT $1::citext = 'bluetusk'::citext, $1::citext");
command.Parameters.Add(new BlueTuskParameter<BlueTuskCitext>(new("BlueTusk")));
await using var reader = await command.ExecuteReaderAsync();
await reader.ReadAsync();
var equal = reader.GetBoolean(0); // true; comparison is performed by PostgreSQL
var value = reader.GetFieldValue<BlueTuskCitext>(1);
UseCitext("extensions") supports an extension installed into a non-default schema. Scalar and array values use PostgreSQL’s text/binary send and receive functions and the same runtime catalogue used by the rest of the data source.
EF integration is deliberately a second package,
BlueTusk.Extensions.Citext.EntityFrameworkCore. Register the codec on the data
source and the EF mapping on the provider independently:
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UseCitext()
.Build();
services.AddDbContext<AppDbContext>(options =>
options.UseBlueTusk(dataSource, provider => provider.UseCitext()));
This maps BlueTuskCitext and BlueTuskCitext[] to schema-qualified PostgreSQL
types. Normal EF equality queries remain parameterized and use PostgreSQL’s
case-insensitive citext operator semantics. The plug-in participates in EF’s
service-provider cache identity, so different installation schemas do not share
an incompatible singleton mapping.
Migrations that own installation of the PostgreSQL extension can use the
companion package’s compatibility helpers. The core EF provider also has a
generic typed PostgreSQL-extension lifecycle without taking a dependency on
citext or any other extension-specific package:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureCitext();
// Create citext-backed tables after this operation.
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Drop dependent tables before removing the extension.
migrationBuilder.DropCitext();
}
For model-owned installation and database-first round-tripping, configure the
generic lifecycle in OnModelCreating:
modelBuilder.HasExtension(
"citext",
extension => extension.UseSchema("extensions"));
This metadata orders installation before extension-backed schema objects and
removal after them. The citext CLR type, codec, query semantics, and
extension-specific service registration remain isolated in the companion
packages.
The immutable descriptor is available for integration code that must inspect configured optional behavior:
var feature = dataSource.Features.GetRequired<BlueTuskCitextFeature>(
BlueTuskCitextFeature.RegistryName);
No citext SQL, CLR type, or package reference is present in BlueTusk.Data,
BlueTusk.Client, BlueTusk.EntityFrameworkCore, or lower layers. The EF
mapping and migration SQL live only in the companion package. The authoring
template and compatibility harness establish the executable stable authoring
contract. Citext-specific and EF-specific convenience APIs are part of the
stable Provider 1.0.0 contract.
pgvector
BlueTusk.Extensions.PgVector provides executable support for pgvector’s
vector, halfvec, and sparsevec types. Install the extension before building
the data source, then register its schema-local types:
CREATE EXTENSION IF NOT EXISTS vector;
using BlueTusk.Data;
using BlueTusk.Extensions.PgVector;
await using var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UsePgVector()
.Build();
var embedding = new BlueTuskVector(1f, 2f, 3f);
await using var command = dataSource.CreateCommand(
"SELECT $1::vector, $1 <-> '[1,2,4]'::vector");
command.Parameters.Add(new BlueTuskParameter<BlueTuskVector>(embedding));
BlueTuskVector and BlueTuskHalfVector are immutable, structurally comparable,
restricted to finite elements, and enforce pgvector’s 1–16,000 dimension range.
BlueTuskSparseVector accepts zero-based CLR indices, stores up to 16,000 sorted
non-zero float elements across as many as one billion dimensions, and formats
the one-based SQL representation. Their codecs implement pgvector’s native
binary layouts and invariant text forms. Runtime catalogue composition supports
arrays of all three values. The live contract verifies scalar/array binary round
trips, distance execution, and pgvector’s Hamming/Jaccard operators over the
core provider’s BlueTuskBitString against the official PostgreSQL 18 image.
The separately packaged BlueTusk.Extensions.PgVector.EntityFrameworkCore
integration maps scalar and array properties for all three types, preserves
dimension-qualified store types such as vector(768), and translates
parameterized L2, negative-inner-product, cosine, and L1 distance calls to
<->, <#>, <=>, and <+>:
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseBlueTusk(dataSource, provider => provider.UsePgVector())
.Options;
var nearest = await context.Items
.OrderBy(item => EF.Functions.L2Distance(item.Embedding, probe))
.Take(10)
.ToListAsync();
The EF package also translates BlueTuskBitString Hamming and Jaccard distances
to <~> and <%>. Its live gate verifies EF writes, materialisation, array
round trips, dimension constraints, and parameterized distance execution for
the complete pgvector type family.
hstore
BlueTusk.Extensions.HStore maps PostgreSQL’s key/value type to an immutable
BlueTuskHStore value. Install and register it before building the data source:
CREATE EXTENSION IF NOT EXISTS hstore;
using BlueTusk.Data;
using BlueTusk.Extensions.HStore;
var attributes = new BlueTuskHStore(
new("owner", "BlueTusk"),
new("reviewed", null));
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UseHStore()
.Build();
The value preserves PostgreSQL’s distinction between a null value and the text
"NULL", uses ordinal key identity, and compares independently of pair order.
Its parser and formatter implement hstore quoting and backslash escaping. The
native codec validates pair counts, length prefixes, UTF-8, duplicate keys, and
trailing bytes. Runtime catalogue composition supplies BlueTuskHStore[] with
no array-specific registration. The live gate covers scalar and array binary
round trips plus key lookup, existence, and null-definition semantics.
ltree
BlueTusk.Extensions.LTree covers all three public ltree data types: label
paths, hierarchical patterns, and position-independent text patterns.
CREATE EXTENSION IF NOT EXISTS ltree;
using BlueTusk.Data;
using BlueTusk.Extensions.LTree;
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UseLTree()
.Build();
var path = new BlueTuskLTree("Top.Countries.Europe.Russia");
var query = new BlueTuskLQuery("Top.*{,2}.Europe.Russ@*");
BlueTuskLTree, BlueTuskLQuery, and BlueTuskLTxtQuery keep separate CLR
identities so parameter inference cannot confuse their PostgreSQL operators.
Their binary codecs implement PostgreSQL’s version byte followed by canonical
UTF-8 text. PostgreSQL remains the grammar authority because valid ltree label
characters depend on the database locale. The live gate resolves all three
catalogue types and exercises arrays, hierarchical matching,
position-independent matching, and path-level functions.
pg_trgm
pg_trgm contributes functions, operators, and index operator classes but no
wire type, so BlueTusk.Extensions.PgTrgm is intentionally a feature-only
plugin. Its typed ADO.NET helper executes the three similarity families and
threshold operators together:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
using BlueTusk.Data;
using BlueTusk.Extensions.PgTrgm;
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UsePgTrgm()
.Build();
var comparison = await dataSource.ComparePgTrgmAsync(
"BlueTusk",
"blue tusk");
BlueTuskPgTrgmComparison returns similarity, word similarity, strict-word
similarity, all three threshold decisions, and the query’s trigram set from one
round trip. Both input strings are typed parameters. Functions and operators
are schema-qualified, including quoted custom schemas. The live gate moves the
extension into a spaced identifier, executes the same behavior, and restores
it to public.
pg_durable
pg_durable contributes a background worker and a SQL workflow DSL over
PostgreSQL built-in types. BlueTusk.Extensions.PgDurable is a non-packable
preview adapter: catalogue discovery already covers its text, jsonb,
regrole, UUID, timestamp, and integer values. It is excluded from the V1
Provider manifest and stable package/API counts.
Microsoft labels pg_durable as preview and its official container as evaluation-only. Its published security review records unresolved production resource-governance and default-exposure gaps. The checked-in adapter and live gate prove BlueTusk compatibility with exactly 0.2.5; they do not approve the upstream extension or its container for production use.
For evaluation, the PostgreSQL operator must install pg_durable 0.2.5, add it to
shared_preload_libraries, create it in the database selected by
pg_durable.database (the postgres database by default), and grant the
application role explicitly:
CREATE EXTENSION pg_durable;
SELECT df.grant_usage('app_role');
using BlueTusk.Data;
using BlueTusk.Extensions.PgDurable;
await using var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UsePgDurable()
.Build();
var instanceId = await dataSource.StartPgDurableAsync(
"SELECT count(*) AS total FROM orders",
label: "count-orders");
var status = await dataSource.AwaitPgDurableAsync(instanceId);
var resultJson = await dataSource.GetPgDurableResultAsync(instanceId);
The helpers cover installed-version discovery, start, status, result, await,
cancel, external signals, and restricted system-wide aggregate metrics. The
metrics helper requires an explicit df.metrics() EXECUTE grant; the ordinary
application-role grant intentionally omits it. Workflow SQL remains
extension-owned; all non-workflow inputs are typed parameters, result JSON is
preserved as text, and unknown future status values degrade to Unknown. The
dedicated official pg_durable 0.2.5/PostgreSQL 17 evaluation-image gate verifies
a complete parameterized workflow lifecycle. Promotion requires fresh upstream
production guidance, a supported deployment artifact, closed security blockers,
explicit resource limits, and a new compatibility review.
PostGIS
BlueTusk.Extensions.PostGIS provides distinct geometry and geography
identities without introducing a geometry-model dependency into BlueTusk core:
CREATE EXTENSION IF NOT EXISTS postgis;
using BlueTusk.Data;
using BlueTusk.Extensions.PostGIS;
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UsePostGis()
.Build();
var point = BlueTuskGeometry.FromText(
"SRID=4326;POINT(-0.1276 51.5072)");
BlueTuskGeometry and BlueTuskGeography accept server-parseable WKT/EWKT or
immutable EWKB. The codecs select text for textual values and native binary for
EWKB; when an array contains text, existing binary elements fall back to
hexadecimal EWKB accepted by PostGIS. Binary results can be sent back without
conversion. The live PostgreSQL 18/PostGIS 3.6 gate verifies catalogue
resolution, geometry and geography parameters, arrays, SRIDs, geography
distance, spatial predicates, and text-to-binary conversion.
This transport slice deliberately leaves geometry parsing, coordinate-system
rules, topology, and algorithms to PostGIS. Rich geometry and EF support stays
separately packaged in BlueTusk.Extensions.PostGIS.EntityFrameworkCore, which
uses NetTopologySuite without adding spatial dependencies to BlueTusk core:
using NetTopologySuite.Geometries;
var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UsePostGis()
.Build();
services.AddDbContext<AppDbContext>(options =>
options.UseBlueTusk(dataSource, provider => provider.UsePostGis()));
modelBuilder.Entity<Place>()
.Property(place => place.Location)
.HasColumnType("geometry(Point,4326)");
The EF package maps the complete NetTopologySuite geometry hierarchy, exact geometry/geography typmods, and arrays through BlueTusk’s EWKB codecs. It translates typed predicates, distance and index-aware proximity, set operations, buffers, geometry members, transforms, validity repair, GeoJSON output, and the PostGIS bounding-box operator. Geography uses only its documented distance, intersection, covers/covered-by, area, length, and centroid surface; unsupported geometry-only calls produce focused translation errors. Conversion helpers preserve SRID and XY/Z/M ordinates, mutable geometries receive structural EF snapshots, and migration helpers own optional extension create/drop SQL. The PostgreSQL 18/PostGIS 3.6 live gate covers round trips, arrays, parameters, projections, spatial filters, and compiled queries.
TimescaleDB
TimescaleDB adds SQL behavior instead of a wire type, so
BlueTusk.Extensions.TimescaleDB is a feature-only package. It provides typed
ADO.NET operations for discovering the installed extension version, converting
an existing table to a range hypertable, obtaining approximate row counts, and
managing retention, Hypercore columnstore, and continuous-aggregate policies:
CREATE EXTENSION IF NOT EXISTS timescaledb;
using BlueTusk.Data;
using BlueTusk.Extensions.TimescaleDB;
using BlueTusk.TypeSystem;
await using var dataSource = new BlueTuskDataSourceBuilder(connectionString)
.UseTimescaleDb()
.Build();
var result = await dataSource.CreateHypertableAsync(
"public.metrics",
"recorded_at");
var jobId = await dataSource.AddRetentionPolicyAsync(
"public.metrics",
new BlueTuskInterval(months: 0, days: 30, microseconds: 0));
await dataSource.RemoveRetentionPolicyAsync("public.metrics");
Relations and time-column names are passed through PostgreSQL regclass and
name parameters, while the configured extension schema is safely delimited.
migrateData defaults to false: converting a populated table can take locks,
so callers must opt into that behavior. Continuous-aggregate refresh windows use
typed DateTimeOffset values and all policy intervals must be finite. The
columnstore helpers use TimescaleDB’s current Hypercore APIs, not the legacy
compression-policy names.
BlueTusk.Extensions.TimescaleDB.EntityFrameworkCore adds separately registered,
schema-qualified query translations and migration helpers:
services.AddDbContext<AppDbContext>(options =>
options.UseBlueTusk(dataSource, provider => provider.UseTimescaleDb()));
var hourly = context.Metrics
.GroupBy(metric => EF.Functions.TimeBucket(width, metric.RecordedAt))
.Select(group => new
{
group.Key,
First = EF.Functions.TimescaleFirst(
group.Select(metric => ValueTuple.Create(metric.Value, metric.RecordedAt))),
Histogram = EF.Functions.TimescaleHistogram(
group.Select(metric => metric.Value), 0, 100, 10),
});
Temporal buckets support timestamp-with-time-zone, timestamp, and date values,
including offset/origin forms and timezone-aware timestamp-with-time-zone
bucketing; smallint, integer, and bigint buckets are also typed. first, last,
and histogram preserve LINQ ordering, distinctness, and filters in aggregate
SQL. The PostgreSQL 17/TimescaleDB 2.29 gate verifies queries, compiled queries,
hypertable creation, approximate counts, and all documented policy/refresh
lifecycles.